cranpose_core/
runtime.rs

1use crate::collections::map::HashMap;
2use crate::collections::map::HashSet;
3use crate::MutableStateInner;
4use std::any::Any;
5use std::cell::{Cell, Ref, RefCell};
6use std::collections::{HashMap as StdHashMap, VecDeque};
7use std::future::Future;
8use std::pin::Pin;
9use std::rc::{Rc, Weak};
10use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
11use std::sync::{mpsc, Arc};
12use std::task::{Context, Poll, Waker};
13use std::thread::ThreadId;
14use std::thread_local;
15
16use crate::frame_clock::FrameClock;
17use crate::platform::RuntimeScheduler;
18use crate::{Applier, Command, FrameCallbackId, NodeError, RecomposeScopeInner, ScopeId};
19
20enum UiMessage {
21    Task(Box<dyn FnOnce() + Send + 'static>),
22    Invoke { id: u64, value: Box<dyn Any + Send> },
23}
24
25type UiContinuation = Box<dyn Fn(Box<dyn Any>) + 'static>;
26type UiContinuationMap = HashMap<u64, UiContinuation>;
27
28trait AnyStateCell {
29    fn as_any(&self) -> &dyn Any;
30}
31
32struct TypedStateCell<T: Clone + 'static> {
33    inner: MutableStateInner<T>,
34}
35
36impl<T: Clone + 'static> AnyStateCell for TypedStateCell<T> {
37    fn as_any(&self) -> &dyn Any {
38        &self.inner
39    }
40}
41
42#[allow(dead_code)]
43struct RawStateCell<T: 'static> {
44    value: T,
45}
46
47impl<T: 'static> AnyStateCell for RawStateCell<T> {
48    fn as_any(&self) -> &dyn Any {
49        &self.value
50    }
51}
52
53#[derive(Default)]
54pub(crate) struct StateArena {
55    cells: RefCell<Vec<Option<Box<dyn AnyStateCell>>>>,
56}
57
58impl StateArena {
59    pub(crate) fn alloc<T: Clone + 'static>(&self, value: T, runtime: RuntimeHandle) -> StateId {
60        let mut cells = self.cells.borrow_mut();
61        let id = StateId(cells.len() as u32);
62        let inner = MutableStateInner::new(value, runtime.clone());
63        inner.install_snapshot_observer(id);
64        let cell: Box<dyn AnyStateCell> = Box::new(TypedStateCell { inner });
65        cells.push(Some(cell));
66        id
67    }
68
69    #[allow(dead_code)]
70    pub(crate) fn alloc_raw<T: 'static>(&self, value: T) -> StateId {
71        let mut cells = self.cells.borrow_mut();
72        let id = StateId(cells.len() as u32);
73        let cell: Box<dyn AnyStateCell> = Box::new(RawStateCell { value });
74        cells.push(Some(cell));
75        id
76    }
77
78    fn get_cell(&self, id: StateId) -> Ref<'_, Box<dyn AnyStateCell>> {
79        Ref::map(self.cells.borrow(), |cells| {
80            cells
81                .get(id.0 as usize)
82                .and_then(|cell| cell.as_ref())
83                .expect("state cell missing")
84        })
85    }
86
87    pub(crate) fn get_typed<T: Clone + 'static>(
88        &self,
89        id: StateId,
90    ) -> Ref<'_, MutableStateInner<T>> {
91        Ref::map(self.get_cell(id), |cell| {
92            cell.as_any()
93                .downcast_ref::<MutableStateInner<T>>()
94                .expect("state cell type mismatch")
95        })
96    }
97
98    #[allow(dead_code)]
99    pub(crate) fn get_raw<T: 'static>(&self, id: StateId) -> Ref<'_, T> {
100        Ref::map(self.get_cell(id), |cell| {
101            cell.as_any()
102                .downcast_ref::<T>()
103                .expect("raw state cell type mismatch")
104        })
105    }
106
107    pub(crate) fn get_typed_opt<T: Clone + 'static>(
108        &self,
109        id: StateId,
110    ) -> Option<Ref<'_, MutableStateInner<T>>> {
111        Ref::filter_map(self.get_cell(id), |cell| {
112            cell.as_any().downcast_ref::<MutableStateInner<T>>()
113        })
114        .ok()
115    }
116}
117
118thread_local! {
119    #[allow(clippy::missing_const_for_thread_local)]
120    static RUNTIME_HANDLES: RefCell<StdHashMap<RuntimeId, RuntimeHandle>> =
121        RefCell::new(StdHashMap::new());
122}
123
124fn register_runtime_handle(handle: &RuntimeHandle) {
125    RUNTIME_HANDLES.with(|registry| {
126        registry.borrow_mut().insert(handle.id, handle.clone());
127    });
128    // Also set as LAST_RUNTIME so that mutableStateOf() can find it
129    // when called outside of a composition context.
130    LAST_RUNTIME.with(|slot| *slot.borrow_mut() = Some(handle.clone()));
131}
132
133pub(crate) fn runtime_handle_for(id: RuntimeId) -> Option<RuntimeHandle> {
134    RUNTIME_HANDLES.with(|registry| registry.borrow().get(&id).cloned())
135}
136
137#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
138pub struct StateId(pub(crate) u32);
139
140#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
141pub struct RuntimeId(u32);
142
143impl RuntimeId {
144    fn next() -> Self {
145        static NEXT_RUNTIME_ID: AtomicU32 = AtomicU32::new(1);
146        Self(NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed))
147    }
148}
149
150struct UiDispatcherInner {
151    scheduler: Arc<dyn RuntimeScheduler>,
152    tx: mpsc::Sender<UiMessage>,
153    pending: AtomicUsize,
154}
155
156impl UiDispatcherInner {
157    fn new(scheduler: Arc<dyn RuntimeScheduler>, tx: mpsc::Sender<UiMessage>) -> Self {
158        Self {
159            scheduler,
160            tx,
161            pending: AtomicUsize::new(0),
162        }
163    }
164
165    fn post(&self, task: impl FnOnce() + Send + 'static) {
166        self.pending.fetch_add(1, Ordering::SeqCst);
167        let _ = self.tx.send(UiMessage::Task(Box::new(task)));
168        self.scheduler.schedule_frame();
169    }
170
171    fn post_invoke(&self, id: u64, value: Box<dyn Any + Send>) {
172        self.pending.fetch_add(1, Ordering::SeqCst);
173        let _ = self.tx.send(UiMessage::Invoke { id, value });
174        self.scheduler.schedule_frame();
175    }
176
177    fn has_pending(&self) -> bool {
178        self.pending.load(Ordering::SeqCst) > 0
179    }
180}
181
182struct PendingGuard<'a> {
183    counter: &'a AtomicUsize,
184}
185
186impl<'a> PendingGuard<'a> {
187    fn new(counter: &'a AtomicUsize) -> Self {
188        Self { counter }
189    }
190}
191
192impl<'a> Drop for PendingGuard<'a> {
193    fn drop(&mut self) {
194        let previous = self.counter.fetch_sub(1, Ordering::SeqCst);
195        debug_assert!(previous > 0, "UI dispatcher pending count underflowed");
196    }
197}
198
199#[derive(Clone)]
200pub struct UiDispatcher {
201    inner: Arc<UiDispatcherInner>,
202}
203
204impl UiDispatcher {
205    fn new(inner: Arc<UiDispatcherInner>) -> Self {
206        Self { inner }
207    }
208
209    pub fn post(&self, task: impl FnOnce() + Send + 'static) {
210        self.inner.post(task);
211    }
212
213    pub fn post_invoke<T>(&self, id: u64, value: T)
214    where
215        T: Send + 'static,
216    {
217        self.inner.post_invoke(id, Box::new(value));
218    }
219
220    pub fn has_pending(&self) -> bool {
221        self.inner.has_pending()
222    }
223}
224
225struct RuntimeInner {
226    scheduler: Arc<dyn RuntimeScheduler>,
227    needs_frame: RefCell<bool>,
228    node_updates: RefCell<Vec<Command>>, // FUTURE(no_std): replace Vec with ring buffer.
229    invalid_scopes: RefCell<HashSet<ScopeId>>, // FUTURE(no_std): replace HashSet with sparse bitset.
230    scope_queue: RefCell<Vec<(ScopeId, Weak<RecomposeScopeInner>)>>, // FUTURE(no_std): use smallvec-backed queue.
231    frame_callbacks: RefCell<VecDeque<FrameCallbackEntry>>, // FUTURE(no_std): migrate to ring buffer.
232    next_frame_callback_id: Cell<u64>,
233    ui_dispatcher: Arc<UiDispatcherInner>,
234    ui_rx: RefCell<mpsc::Receiver<UiMessage>>,
235    local_tasks: RefCell<VecDeque<Box<dyn FnOnce() + 'static>>>,
236    ui_conts: RefCell<UiContinuationMap>,
237    next_cont_id: Cell<u64>,
238    ui_thread_id: ThreadId,
239    tasks: RefCell<Vec<TaskEntry>>, // FUTURE(no_std): migrate to smallvec-backed storage.
240    next_task_id: Cell<u64>,
241    task_waker: RefCell<Option<Waker>>,
242    state_arena: StateArena,
243    runtime_id: RuntimeId,
244}
245
246struct TaskEntry {
247    id: u64,
248    future: Pin<Box<dyn Future<Output = ()> + 'static>>,
249}
250
251impl RuntimeInner {
252    fn new(scheduler: Arc<dyn RuntimeScheduler>) -> Self {
253        let (tx, rx) = mpsc::channel();
254        let dispatcher = Arc::new(UiDispatcherInner::new(scheduler.clone(), tx));
255        Self {
256            scheduler,
257            needs_frame: RefCell::new(false),
258            node_updates: RefCell::new(Vec::new()),
259            invalid_scopes: RefCell::new(HashSet::default()),
260            scope_queue: RefCell::new(Vec::new()),
261            frame_callbacks: RefCell::new(VecDeque::new()),
262            next_frame_callback_id: Cell::new(1),
263            ui_dispatcher: dispatcher,
264            ui_rx: RefCell::new(rx),
265            local_tasks: RefCell::new(VecDeque::new()),
266            ui_conts: RefCell::new(UiContinuationMap::default()),
267            next_cont_id: Cell::new(1),
268            ui_thread_id: std::thread::current().id(),
269            tasks: RefCell::new(Vec::new()),
270            next_task_id: Cell::new(1),
271            task_waker: RefCell::new(None),
272            state_arena: StateArena::default(),
273            runtime_id: RuntimeId::next(),
274        }
275    }
276
277    fn init_task_waker(this: &Rc<Self>) {
278        let weak = Rc::downgrade(this);
279        let waker = RuntimeTaskWaker::new(weak).into_waker();
280        *this.task_waker.borrow_mut() = Some(waker);
281    }
282
283    fn schedule(&self) {
284        *self.needs_frame.borrow_mut() = true;
285        self.scheduler.schedule_frame();
286    }
287
288    fn enqueue_update(&self, command: Command) {
289        self.node_updates.borrow_mut().push(command);
290        self.schedule(); // Ensure frame is scheduled to process the command
291    }
292
293    fn take_updates(&self) -> Vec<Command> {
294        // FUTURE(no_std): return stack-allocated smallvec.
295        let updates = self.node_updates.borrow_mut().drain(..).collect::<Vec<_>>();
296        updates
297    }
298
299    fn has_updates(&self) -> bool {
300        !self.node_updates.borrow().is_empty() || self.has_invalid_scopes()
301    }
302
303    fn register_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
304        let mut invalid = self.invalid_scopes.borrow_mut();
305        if invalid.insert(id) {
306            self.scope_queue.borrow_mut().push((id, scope));
307            self.schedule();
308        }
309    }
310
311    fn mark_scope_recomposed(&self, id: ScopeId) {
312        self.invalid_scopes.borrow_mut().remove(&id);
313    }
314
315    fn take_invalidated_scopes(&self) -> Vec<(ScopeId, Weak<RecomposeScopeInner>)> {
316        // FUTURE(no_std): return iterator over small array storage.
317        let mut queue = self.scope_queue.borrow_mut();
318        if queue.is_empty() {
319            return Vec::new();
320        }
321        let pending: Vec<_> = queue.drain(..).collect();
322        drop(queue);
323        let invalid = self.invalid_scopes.borrow();
324        pending
325            .into_iter()
326            .filter(|(id, _)| invalid.contains(id))
327            .collect()
328    }
329
330    fn has_invalid_scopes(&self) -> bool {
331        !self.invalid_scopes.borrow().is_empty()
332    }
333
334    fn has_frame_callbacks(&self) -> bool {
335        !self.frame_callbacks.borrow().is_empty()
336    }
337
338    /// Queues a closure that is already bound to the UI thread's local queue.
339    ///
340    /// The closure may capture `Rc`/`RefCell` values because it never leaves the
341    /// runtime thread. Callers must only invoke this from the runtime thread.
342    fn enqueue_ui_task(&self, task: Box<dyn FnOnce() + 'static>) {
343        self.local_tasks.borrow_mut().push_back(task);
344        self.schedule();
345    }
346
347    fn spawn_ui_task(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) -> u64 {
348        let id = self.next_task_id.get();
349        self.next_task_id.set(id + 1);
350        self.tasks.borrow_mut().push(TaskEntry { id, future });
351        self.schedule();
352        id
353    }
354
355    fn cancel_task(&self, id: u64) {
356        let mut tasks = self.tasks.borrow_mut();
357        if tasks.iter().any(|entry| entry.id == id) {
358            tasks.retain(|entry| entry.id != id);
359        }
360    }
361
362    fn poll_async_tasks(&self) -> bool {
363        let waker = match self.task_waker.borrow().as_ref() {
364            Some(waker) => waker.clone(),
365            None => return false,
366        };
367        let mut cx = Context::from_waker(&waker);
368        let mut tasks_ref = self.tasks.borrow_mut();
369        let tasks = std::mem::take(&mut *tasks_ref);
370        drop(tasks_ref);
371        let mut pending = Vec::with_capacity(tasks.len());
372        let mut made_progress = false;
373        for mut entry in tasks.into_iter() {
374            match entry.future.as_mut().poll(&mut cx) {
375                Poll::Ready(()) => {
376                    made_progress = true;
377                }
378                Poll::Pending => {
379                    pending.push(entry);
380                }
381            }
382        }
383        if !pending.is_empty() {
384            self.tasks.borrow_mut().extend(pending);
385        }
386        made_progress
387    }
388
389    fn drain_ui(&self) {
390        loop {
391            let mut executed = false;
392
393            {
394                let rx = &mut *self.ui_rx.borrow_mut();
395                for message in rx.try_iter() {
396                    executed = true;
397                    let _guard = PendingGuard::new(&self.ui_dispatcher.pending);
398                    match message {
399                        UiMessage::Task(task) => {
400                            task();
401                        }
402                        UiMessage::Invoke { id, value } => {
403                            self.invoke_ui_cont(id, value);
404                        }
405                    }
406                }
407            }
408
409            loop {
410                let task = {
411                    let mut local = self.local_tasks.borrow_mut();
412                    local.pop_front()
413                };
414
415                match task {
416                    Some(task) => {
417                        executed = true;
418                        task();
419                    }
420                    None => break,
421                }
422            }
423
424            if self.poll_async_tasks() {
425                executed = true;
426            }
427
428            if !executed {
429                break;
430            }
431        }
432    }
433
434    fn has_pending_ui(&self) -> bool {
435        let local_pending = self
436            .local_tasks
437            .try_borrow()
438            .map(|tasks| !tasks.is_empty())
439            .unwrap_or(true);
440
441        let async_pending = self
442            .tasks
443            .try_borrow()
444            .map(|tasks| !tasks.is_empty())
445            .unwrap_or(true);
446
447        local_pending || self.ui_dispatcher.has_pending() || async_pending
448    }
449
450    fn register_ui_cont<T: 'static>(&self, f: impl FnOnce(T) + 'static) -> u64 {
451        debug_assert_eq!(
452            std::thread::current().id(),
453            self.ui_thread_id,
454            "UI continuation registered off the runtime thread",
455        );
456        let id = self.next_cont_id.get();
457        self.next_cont_id.set(id + 1);
458        let callback = RefCell::new(Some(f));
459        self.ui_conts.borrow_mut().insert(
460            id,
461            Box::new(move |value: Box<dyn Any>| {
462                let slot = callback
463                    .borrow_mut()
464                    .take()
465                    .expect("UI continuation invoked more than once");
466                let value = value
467                    .downcast::<T>()
468                    .expect("UI continuation type mismatch");
469                slot(*value);
470            }),
471        );
472        id
473    }
474
475    fn invoke_ui_cont(&self, id: u64, value: Box<dyn Any + Send>) {
476        debug_assert_eq!(
477            std::thread::current().id(),
478            self.ui_thread_id,
479            "UI continuation invoked off the runtime thread",
480        );
481        if let Some(callback) = self.ui_conts.borrow_mut().remove(&id) {
482            let value: Box<dyn Any> = value;
483            callback(value);
484        }
485    }
486
487    fn cancel_ui_cont(&self, id: u64) {
488        self.ui_conts.borrow_mut().remove(&id);
489    }
490
491    fn register_frame_callback(&self, callback: Box<dyn FnOnce(u64) + 'static>) -> FrameCallbackId {
492        let id = self.next_frame_callback_id.get();
493        self.next_frame_callback_id.set(id + 1);
494        self.frame_callbacks
495            .borrow_mut()
496            .push_back(FrameCallbackEntry {
497                id,
498                callback: Some(callback),
499            });
500        self.schedule();
501        id
502    }
503
504    fn cancel_frame_callback(&self, id: FrameCallbackId) {
505        let mut callbacks = self.frame_callbacks.borrow_mut();
506        if let Some(index) = callbacks.iter().position(|entry| entry.id == id) {
507            callbacks.remove(index);
508        }
509        let callbacks_empty = callbacks.is_empty();
510        drop(callbacks);
511        let local_pending = self
512            .local_tasks
513            .try_borrow()
514            .map(|tasks| !tasks.is_empty())
515            .unwrap_or(true);
516        let async_pending = self
517            .tasks
518            .try_borrow()
519            .map(|tasks| !tasks.is_empty())
520            .unwrap_or(true);
521        if !self.has_invalid_scopes()
522            && !self.has_updates()
523            && callbacks_empty
524            && !local_pending
525            && !self.ui_dispatcher.has_pending()
526            && !async_pending
527        {
528            *self.needs_frame.borrow_mut() = false;
529        }
530    }
531
532    fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
533        let mut callbacks = self.frame_callbacks.borrow_mut();
534        let mut pending: Vec<Box<dyn FnOnce(u64) + 'static>> = Vec::with_capacity(callbacks.len());
535        while let Some(mut entry) = callbacks.pop_front() {
536            if let Some(callback) = entry.callback.take() {
537                pending.push(callback);
538            }
539        }
540        drop(callbacks);
541
542        // Wrap ALL frame callbacks in a single mutable snapshot so state changes
543        // are properly applied to the global snapshot and visible to subsequent reads.
544        // Using a single snapshot for all callbacks avoids stack exhaustion from
545        // repeated snapshot creation in long-running animation loops.
546        if !pending.is_empty() {
547            let _ = crate::run_in_mutable_snapshot(|| {
548                for callback in pending {
549                    callback(frame_time_nanos);
550                }
551            });
552        }
553
554        if !self.has_invalid_scopes()
555            && !self.has_updates()
556            && !self.has_frame_callbacks()
557            && !self.has_pending_ui()
558        {
559            *self.needs_frame.borrow_mut() = false;
560        }
561    }
562}
563
564#[derive(Clone)]
565pub struct Runtime {
566    inner: Rc<RuntimeInner>, // FUTURE(no_std): replace Rc with arena-managed runtime storage.
567}
568
569impl Runtime {
570    pub fn new(scheduler: Arc<dyn RuntimeScheduler>) -> Self {
571        let inner = Rc::new(RuntimeInner::new(scheduler));
572        RuntimeInner::init_task_waker(&inner);
573        let runtime = Self { inner };
574        register_runtime_handle(&runtime.handle());
575        runtime
576    }
577
578    pub fn handle(&self) -> RuntimeHandle {
579        RuntimeHandle {
580            inner: Rc::downgrade(&self.inner),
581            dispatcher: UiDispatcher::new(self.inner.ui_dispatcher.clone()),
582            ui_thread_id: self.inner.ui_thread_id,
583            id: self.inner.runtime_id,
584        }
585    }
586
587    pub fn has_updates(&self) -> bool {
588        self.inner.has_updates()
589    }
590
591    pub fn needs_frame(&self) -> bool {
592        *self.inner.needs_frame.borrow() || self.inner.ui_dispatcher.has_pending()
593    }
594
595    pub fn set_needs_frame(&self, value: bool) {
596        *self.inner.needs_frame.borrow_mut() = value;
597    }
598
599    pub fn frame_clock(&self) -> FrameClock {
600        FrameClock::new(self.handle())
601    }
602}
603
604#[derive(Default)]
605pub struct DefaultScheduler;
606
607impl RuntimeScheduler for DefaultScheduler {
608    fn schedule_frame(&self) {}
609}
610
611#[cfg(test)]
612#[derive(Default)]
613pub struct TestScheduler;
614
615#[cfg(test)]
616impl RuntimeScheduler for TestScheduler {
617    fn schedule_frame(&self) {}
618}
619
620#[cfg(test)]
621pub struct TestRuntime {
622    runtime: Runtime,
623}
624
625#[cfg(test)]
626impl Default for TestRuntime {
627    fn default() -> Self {
628        Self::new()
629    }
630}
631
632#[cfg(test)]
633impl TestRuntime {
634    pub fn new() -> Self {
635        Self {
636            runtime: Runtime::new(Arc::new(TestScheduler)),
637        }
638    }
639
640    pub fn handle(&self) -> RuntimeHandle {
641        self.runtime.handle()
642    }
643}
644
645#[derive(Clone)]
646pub struct RuntimeHandle {
647    inner: Weak<RuntimeInner>,
648    dispatcher: UiDispatcher,
649    ui_thread_id: ThreadId,
650    id: RuntimeId,
651}
652
653pub struct TaskHandle {
654    id: u64,
655    runtime: RuntimeHandle,
656}
657
658impl RuntimeHandle {
659    pub fn id(&self) -> RuntimeId {
660        self.id
661    }
662
663    pub(crate) fn alloc_state<T: Clone + 'static>(&self, value: T) -> StateId {
664        self.with_state_arena(|arena| arena.alloc(value, self.clone()))
665    }
666
667    pub(crate) fn with_state_arena<R>(&self, f: impl FnOnce(&StateArena) -> R) -> R {
668        self.inner
669            .upgrade()
670            .map(|inner| f(&inner.state_arena))
671            .unwrap_or_else(|| panic!("runtime dropped"))
672    }
673
674    #[allow(dead_code)]
675    pub(crate) fn alloc_value<T: 'static>(&self, value: T) -> StateId {
676        self.with_state_arena(|arena| arena.alloc_raw(value))
677    }
678
679    #[allow(dead_code)]
680    pub(crate) fn with_value<T: 'static, R>(&self, id: StateId, f: impl FnOnce(&T) -> R) -> R {
681        self.with_state_arena(|arena| {
682            let value = arena.get_raw::<T>(id);
683            f(&value)
684        })
685    }
686
687    pub fn schedule(&self) {
688        if let Some(inner) = self.inner.upgrade() {
689            inner.schedule();
690        }
691    }
692
693    pub fn enqueue_node_update(&self, command: Command) {
694        if let Some(inner) = self.inner.upgrade() {
695            inner.enqueue_update(command);
696        }
697    }
698
699    /// Schedules work that must run on the runtime thread.
700    ///
701    /// The closure executes on the UI thread immediately when the runtime
702    /// drains its local queue, so it may capture `Rc`/`RefCell` values. Calling
703    /// this from any other thread is a logic error and will panic in debug
704    /// builds via the inner assertion.
705    pub fn enqueue_ui_task(&self, task: Box<dyn FnOnce() + 'static>) {
706        if let Some(inner) = self.inner.upgrade() {
707            inner.enqueue_ui_task(task);
708        } else {
709            task();
710        }
711    }
712
713    pub fn spawn_ui<F>(&self, fut: F) -> Option<TaskHandle>
714    where
715        F: Future<Output = ()> + 'static,
716    {
717        self.inner.upgrade().map(|inner| {
718            let id = inner.spawn_ui_task(Box::pin(fut));
719            TaskHandle {
720                id,
721                runtime: self.clone(),
722            }
723        })
724    }
725
726    pub fn cancel_task(&self, id: u64) {
727        if let Some(inner) = self.inner.upgrade() {
728            inner.cancel_task(id);
729        }
730    }
731
732    /// Enqueues work from any thread to run on the UI thread.
733    ///
734    /// The closure must be `Send` because it may cross threads before executing
735    /// on the runtime thread. Use this when posting from background work.
736    pub fn post_ui(&self, task: impl FnOnce() + Send + 'static) {
737        self.dispatcher.post(task);
738    }
739
740    pub fn register_ui_cont<T: 'static>(&self, f: impl FnOnce(T) + 'static) -> Option<u64> {
741        self.inner.upgrade().map(|inner| inner.register_ui_cont(f))
742    }
743
744    pub fn cancel_ui_cont(&self, id: u64) {
745        if let Some(inner) = self.inner.upgrade() {
746            inner.cancel_ui_cont(id);
747        }
748    }
749
750    pub fn drain_ui(&self) {
751        if let Some(inner) = self.inner.upgrade() {
752            inner.drain_ui();
753        }
754    }
755
756    pub fn has_pending_ui(&self) -> bool {
757        self.inner
758            .upgrade()
759            .map(|inner| inner.has_pending_ui())
760            .unwrap_or_else(|| self.dispatcher.has_pending())
761    }
762
763    pub fn register_frame_callback(
764        &self,
765        callback: impl FnOnce(u64) + 'static,
766    ) -> Option<FrameCallbackId> {
767        self.inner
768            .upgrade()
769            .map(|inner| inner.register_frame_callback(Box::new(callback)))
770    }
771
772    pub fn cancel_frame_callback(&self, id: FrameCallbackId) {
773        if let Some(inner) = self.inner.upgrade() {
774            inner.cancel_frame_callback(id);
775        }
776    }
777
778    pub fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
779        if let Some(inner) = self.inner.upgrade() {
780            inner.drain_frame_callbacks(frame_time_nanos);
781        }
782    }
783
784    pub fn frame_clock(&self) -> FrameClock {
785        FrameClock::new(self.clone())
786    }
787
788    pub fn set_needs_frame(&self, value: bool) {
789        if let Some(inner) = self.inner.upgrade() {
790            *inner.needs_frame.borrow_mut() = value;
791        }
792    }
793
794    pub(crate) fn take_updates(&self) -> Vec<Command> {
795        // FUTURE(no_std): return iterator over static buffer.
796        self.inner
797            .upgrade()
798            .map(|inner| inner.take_updates())
799            .unwrap_or_default()
800    }
801
802    pub fn has_updates(&self) -> bool {
803        self.inner
804            .upgrade()
805            .map(|inner| inner.has_updates())
806            .unwrap_or(false)
807    }
808
809    pub(crate) fn register_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
810        if let Some(inner) = self.inner.upgrade() {
811            inner.register_invalid_scope(id, scope);
812        }
813    }
814
815    pub(crate) fn mark_scope_recomposed(&self, id: ScopeId) {
816        if let Some(inner) = self.inner.upgrade() {
817            inner.mark_scope_recomposed(id);
818        }
819    }
820
821    pub(crate) fn take_invalidated_scopes(&self) -> Vec<(ScopeId, Weak<RecomposeScopeInner>)> {
822        // FUTURE(no_std): expose draining iterator without Vec allocation.
823        self.inner
824            .upgrade()
825            .map(|inner| inner.take_invalidated_scopes())
826            .unwrap_or_default()
827    }
828
829    pub fn has_invalid_scopes(&self) -> bool {
830        self.inner
831            .upgrade()
832            .map(|inner| inner.has_invalid_scopes())
833            .unwrap_or(false)
834    }
835
836    pub fn has_frame_callbacks(&self) -> bool {
837        self.inner
838            .upgrade()
839            .map(|inner| inner.has_frame_callbacks())
840            .unwrap_or(false)
841    }
842
843    pub fn assert_ui_thread(&self) {
844        debug_assert_eq!(
845            std::thread::current().id(),
846            self.ui_thread_id,
847            "state mutated off the runtime's UI thread"
848        );
849    }
850
851    pub fn dispatcher(&self) -> UiDispatcher {
852        self.dispatcher.clone()
853    }
854}
855
856impl TaskHandle {
857    pub fn cancel(self) {
858        self.runtime.cancel_task(self.id);
859    }
860}
861
862pub(crate) struct FrameCallbackEntry {
863    id: FrameCallbackId,
864    callback: Option<Box<dyn FnOnce(u64) + 'static>>,
865}
866
867struct RuntimeTaskWaker {
868    scheduler: Arc<dyn RuntimeScheduler>,
869}
870
871impl RuntimeTaskWaker {
872    fn new(inner: Weak<RuntimeInner>) -> Self {
873        // Extract the Arc<RuntimeScheduler> which IS Send+Sync
874        // This way we can wake the runtime without storing the Rc::Weak
875        let scheduler = inner
876            .upgrade()
877            .map(|rc| rc.scheduler.clone())
878            .expect("RuntimeInner dropped before waker created");
879        Self { scheduler }
880    }
881
882    fn into_waker(self) -> Waker {
883        futures_task::waker(Arc::new(self))
884    }
885}
886
887impl futures_task::ArcWake for RuntimeTaskWaker {
888    fn wake_by_ref(arc_self: &Arc<Self>) {
889        arc_self.scheduler.schedule_frame();
890    }
891}
892
893thread_local! {
894    static ACTIVE_RUNTIMES: RefCell<Vec<RuntimeHandle>> = const { RefCell::new(Vec::new()) }; // FUTURE(no_std): move to bounded stack storage.
895    static LAST_RUNTIME: RefCell<Option<RuntimeHandle>> = const { RefCell::new(None) };
896}
897
898/// Gets the current runtime handle from thread-local storage.
899///
900/// Returns the most recently pushed active runtime, or the last known runtime.
901/// Used by fling animation and other components that need access to the runtime.
902pub fn current_runtime_handle() -> Option<RuntimeHandle> {
903    if let Some(handle) = ACTIVE_RUNTIMES.with(|stack| stack.borrow().last().cloned()) {
904        return Some(handle);
905    }
906    LAST_RUNTIME.with(|slot| slot.borrow().clone())
907}
908
909pub(crate) fn push_active_runtime(handle: &RuntimeHandle) {
910    ACTIVE_RUNTIMES.with(|stack| stack.borrow_mut().push(handle.clone()));
911    LAST_RUNTIME.with(|slot| *slot.borrow_mut() = Some(handle.clone()));
912}
913
914pub(crate) fn pop_active_runtime() {
915    ACTIVE_RUNTIMES.with(|stack| {
916        stack.borrow_mut().pop();
917    });
918}
919
920/// Schedule a new frame render using the most recently active runtime handle.
921pub fn schedule_frame() {
922    if let Some(handle) = current_runtime_handle() {
923        handle.schedule();
924        return;
925    }
926    panic!("no runtime available to schedule frame");
927}
928
929/// Schedule an in-place node update using the most recently active runtime.
930pub fn schedule_node_update(
931    update: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
932) {
933    let handle = current_runtime_handle().expect("no runtime available to schedule node update");
934    let mut update_opt = Some(update);
935    handle.enqueue_node_update(Box::new(move |applier: &mut dyn Applier| {
936        if let Some(update) = update_opt.take() {
937            return update(applier);
938        }
939        Ok(())
940    }));
941}