1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech
//! Generic, runtime-agnostic delivery of a one-shot main-thread callback that
//! runs with a *fresh* [`EventContext`] bound to a window's tree.
//!
//! This is the plumbing the optional `teksilo-async` crate uses to implement
//! `spawn_local_with`: a future runs on the main-thread executor, and when it
//! completes its result is handed to a callback that needs ambient context
//! operations (`open_window`, `send_intent`, …). Those operations require an
//! [`EventContext`], which only exists *during* event dispatch — so the
//! callback is *registered* here (keyed by an id and the originating window)
//! and *delivered* later by `teksilo-app`, which routes an
//! [`AsyncCompletionPayload`] to the window's tree and calls
//! [`AsyncCompletionHandle::deliver`] inside a freshly-minted context.
//!
//! It mirrors the file-dialog result-delivery pattern, but uses only
//! teksilo-core types so a crate layered *above* `teksilo-app` (like
//! `teksilo-async`) can register callbacks without forcing `teksilo-app` to
//! depend on it (which would be a dependency cycle). There is no async,
//! future, or runtime type here — just a callback registry and a `Send`
//! payload.
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::widget::EventContext;
use crate::window::TeksiloWindowId;
type CompletionCallback = Box<dyn FnOnce(&mut EventContext)>;
struct Pending {
window_id: TeksiloWindowId,
callback: CompletionCallback,
}
struct CompletionState {
next_id: u64,
pending: HashMap<u64, Pending>,
}
/// Main-thread registry of pending async completions. `Clone` shares the same
/// inner state (`Rc`), so the executor and the app event loop both hold a
/// handle to one registry. `!Send` by construction — completions only ever run
/// on the UI thread.
#[derive(Clone)]
pub struct AsyncCompletionHandle {
inner: Rc<RefCell<CompletionState>>,
}
impl Default for AsyncCompletionHandle {
fn default() -> Self {
Self::new()
}
}
impl AsyncCompletionHandle {
pub fn new() -> Self {
Self {
inner: Rc::new(RefCell::new(CompletionState {
next_id: 0,
pending: HashMap::new(),
})),
}
}
/// Register a callback to run later with a fresh [`EventContext`] on the
/// tree of `window_id`. Returns the id to place in an
/// [`AsyncCompletionPayload`].
pub fn register(&self, window_id: TeksiloWindowId, callback: CompletionCallback) -> u64 {
let mut state = self.inner.borrow_mut();
let id = state.next_id;
state.next_id = state.next_id.wrapping_add(1);
state.pending.insert(
id,
Pending {
window_id,
callback,
},
);
id
}
/// Invoke and remove the completion registered under `id`, if its target
/// window still matches `window_id`. Called by `teksilo-app` from inside
/// [`WidgetTree::run_with_event_context`](crate::WidgetTree::run_with_event_context).
/// A no-op if the entry was already purged (e.g. the window closed) — the
/// use-after-free guard.
pub fn deliver(&self, id: u64, window_id: TeksiloWindowId, ctx: &mut EventContext) {
let entry = self.inner.borrow_mut().pending.remove(&id);
if let Some(pending) = entry
&& pending.window_id == window_id
{
(pending.callback)(ctx);
}
}
/// Drop every pending completion targeting `window_id`. Called when a
/// window closes so a late-arriving completion never touches a torn-down
/// tree.
pub fn purge_window(&self, window_id: TeksiloWindowId) {
self.inner
.borrow_mut()
.pending
.retain(|_, p| p.window_id != window_id);
}
/// Number of pending completions (diagnostics / tests).
pub fn pending_len(&self) -> usize {
self.inner.borrow().pending.len()
}
}
/// `Send` payload posted through [`AppEventPoster`](crate::AppEventPoster) when
/// an async task completes. `teksilo-app` downcasts it, routes to the target
/// window's tree, and calls [`AsyncCompletionHandle::deliver`] with a fresh
/// context. Carries only ids — the (`!Send`) callback stays in the registry.
#[derive(Debug, Clone, Copy)]
pub struct AsyncCompletionPayload {
pub id: u64,
pub window_id: TeksiloWindowId,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget_tree::WidgetTree;
use crate::window::NoopWindowOps;
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn deliver_runs_callback_with_fresh_context() {
let handle = AsyncCompletionHandle::new();
let win = TeksiloWindowId::new(1);
let ran = Rc::new(Cell::new(false));
let flag = ran.clone();
let id = handle.register(win, Box::new(move |_ctx| flag.set(true)));
assert_eq!(handle.pending_len(), 1);
let mut tree = WidgetTree::new();
tree.run_with_event_context(&mut NoopWindowOps, |ctx| handle.deliver(id, win, ctx));
assert!(ran.get(), "callback must run with the fresh context");
assert_eq!(
handle.pending_len(),
0,
"delivered completion must be removed"
);
}
#[test]
fn purge_window_drops_only_that_windows_completions() {
let handle = AsyncCompletionHandle::new();
let win = TeksiloWindowId::new(7);
handle.register(win, Box::new(|_ctx| {}));
handle.register(win, Box::new(|_ctx| {}));
handle.register(TeksiloWindowId::new(8), Box::new(|_ctx| {}));
assert_eq!(handle.pending_len(), 3);
handle.purge_window(win);
assert_eq!(
handle.pending_len(),
1,
"only the other window's completion survives"
);
}
#[test]
fn deliver_to_mismatched_window_does_not_run() {
let handle = AsyncCompletionHandle::new();
let win = TeksiloWindowId::new(3);
let ran = Rc::new(Cell::new(false));
let flag = ran.clone();
let id = handle.register(win, Box::new(move |_ctx| flag.set(true)));
let mut tree = WidgetTree::new();
tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
handle.deliver(id, TeksiloWindowId::new(999), ctx)
});
assert!(
!ran.get(),
"a window-mismatched delivery must not run the callback"
);
}
}