Skip to main content

tessera_ui/
dispatcher.rs

1//! Result delivery to an owned UI session.
2//!
3//! ## Usage
4//!
5//! Return background results to a live UI owner before its next build.
6
7use std::{collections::VecDeque, sync::Arc};
8
9use parking_lot::Mutex;
10
11type UiWork = Box<dyn FnOnce() + Send + 'static>;
12type Wake = Arc<dyn Fn() + Send + Sync>;
13
14struct QueueState {
15    open: bool,
16    pending: VecDeque<UiWork>,
17    wake: Option<Wake>,
18}
19
20struct DispatchQueue(Mutex<QueueState>);
21
22impl Default for DispatchQueue {
23    fn default() -> Self {
24        Self(Mutex::new(QueueState {
25            open: crate::RuntimeSession::current().is_some_and(|session| session.accepts_work()),
26            pending: VecDeque::new(),
27            wake: None,
28        }))
29    }
30}
31
32/// A captured delivery service for one UI session.
33#[derive(Clone)]
34pub struct UiDispatcher {
35    owner: crate::RuntimeSessionId,
36    queue: Arc<DispatchQueue>,
37    liveness: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
38}
39
40impl UiDispatcher {
41    /// Queues work for the next UI frame, returning false after shutdown.
42    ///
43    /// An owner-scoped dispatcher runs work only while that owner remains alive.
44    pub fn dispatch(&self, work: impl FnOnce() + Send + 'static) -> bool {
45        if !self.is_alive() {
46            return false;
47        }
48        let liveness = self.liveness.clone();
49        let work = move || {
50            if liveness.as_ref().is_none_or(|alive| alive()) {
51                work();
52            }
53        };
54        let wake = {
55            let mut state = self.queue.0.lock();
56            if !state.open {
57                return false;
58            }
59            state.pending.push_back(Box::new(work));
60            state.wake.clone()
61        };
62        if let Some(wake) = wake {
63            wake();
64        }
65        true
66    }
67
68    /// Returns whether the destination session still accepts results.
69    pub fn is_alive(&self) -> bool {
70        let open = self.queue.0.lock().open;
71        open && self.liveness.as_ref().is_none_or(|alive| alive())
72    }
73
74    pub(crate) fn scoped(mut self, alive: impl Fn() -> bool + Send + Sync + 'static) -> Self {
75        self.liveness = Some(Arc::new(alive));
76        self
77    }
78
79    /// Returns the destination session's identity.
80    pub fn session_id(&self) -> crate::RuntimeSessionId {
81        self.owner
82    }
83}
84
85pub(crate) fn current() -> UiDispatcher {
86    let session =
87        crate::RuntimeSession::current().expect("a UI dispatcher requires an active session");
88    UiDispatcher {
89        owner: session.identity(),
90        queue: crate::runtime_resource::<DispatchQueue>(),
91        liveness: None,
92    }
93}
94
95pub(crate) fn set_waker(wake: Option<Wake>) {
96    let queue = crate::runtime_resource::<DispatchQueue>();
97    let (previous, pending) = {
98        let mut state = queue.0.lock();
99        let previous = std::mem::replace(&mut state.wake, wake.clone());
100        (previous, !state.pending.is_empty())
101    };
102    drop(previous);
103    if pending && let Some(wake) = wake {
104        wake();
105    }
106}
107
108pub(crate) fn has_pending() -> bool {
109    crate::RuntimeSession::current().is_some()
110        && !crate::runtime_resource::<DispatchQueue>()
111            .0
112            .lock()
113            .pending
114            .is_empty()
115}
116
117pub(crate) fn flush() {
118    let queue = crate::runtime_resource::<DispatchQueue>();
119    let work = std::mem::take(&mut queue.0.lock().pending);
120    for work in work {
121        work();
122    }
123}
124
125pub(crate) fn close() {
126    let queue = crate::runtime_resource::<DispatchQueue>();
127    let removed = {
128        let mut state = queue.0.lock();
129        state.open = false;
130        (std::mem::take(&mut state.pending), state.wake.take())
131    };
132    drop(removed);
133}