1#[cfg(not(target_arch = "wasm32"))]
2use std::cell::{Cell, RefCell};
3#[cfg(not(target_arch = "wasm32"))]
4use std::rc::Rc;
5use std::{
6 future::Future,
7 pin::Pin,
8 sync::{
9 Arc,
10 atomic::{AtomicBool, Ordering},
11 },
12};
13
14use crate::{Key, RuntimeHandle, TaskHandle, effect_key::EffectKey, with_current_composer};
15
16#[derive(Default)]
17struct LaunchedEffectState {
18 key: Option<EffectKey>,
19 cancel: Option<LaunchedEffectCancellation>,
20}
21
22struct LaunchedEffectCancellation {
23 #[cfg(not(target_arch = "wasm32"))]
24 runtime: RuntimeHandle,
25 active: Arc<AtomicBool>,
26 #[cfg(not(target_arch = "wasm32"))]
27 continuations: Rc<RefCell<Vec<u64>>>,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct TaskSite {
38 pub file: &'static str,
39 pub line: u32,
40}
41
42impl TaskSite {
43 pub const fn new(file: &'static str, line: u32) -> TaskSite {
45 TaskSite { file, line }
46 }
47}
48
49impl Default for TaskSite {
50 fn default() -> TaskSite {
51 TaskSite::new("unknown", 0)
52 }
53}
54
55impl std::fmt::Display for TaskSite {
56 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(formatter, "{}:{}", self.file, self.line)
58 }
59}
60
61impl From<&'static std::panic::Location<'static>> for TaskSite {
62 fn from(location: &'static std::panic::Location<'static>) -> TaskSite {
64 TaskSite::new(location.file(), location.line())
65 }
66}
67
68#[derive(Default)]
69struct LaunchedEffectAsyncState {
70 key: Option<EffectKey>,
71 cancel: Option<LaunchedEffectCancellation>,
72 task: Option<TaskHandle>,
73 site: TaskSite,
74}
75
76impl LaunchedEffectState {
77 fn should_run(&self, key: &EffectKey) -> bool {
78 match &self.key {
79 Some(current) => key.differs_from(current),
80 None => true,
81 }
82 }
83
84 fn set_key(&mut self, key: EffectKey) {
85 self.key = Some(key);
86 }
87
88 fn launch(
89 &mut self,
90 runtime: RuntimeHandle,
91 effect: impl FnOnce(LaunchedEffectScope) + 'static,
92 ) {
93 self.cancel_current();
94 let active = Arc::new(AtomicBool::new(true));
95 #[cfg(not(target_arch = "wasm32"))]
96 let continuations = Rc::new(RefCell::new(Vec::new()));
97 self.cancel = Some(LaunchedEffectCancellation {
98 #[cfg(not(target_arch = "wasm32"))]
99 runtime: runtime.clone(),
100 active: Arc::clone(&active),
101 #[cfg(not(target_arch = "wasm32"))]
102 continuations: Rc::clone(&continuations),
103 });
104 let scope = LaunchedEffectScope {
105 active: Arc::clone(&active),
106 runtime: runtime.clone(),
107 #[cfg(not(target_arch = "wasm32"))]
108 continuations,
109 };
110 runtime.enqueue_ui_task(Box::new(move || effect(scope)));
111 }
112
113 fn cancel_current(&mut self) {
114 if let Some(cancel) = self.cancel.take() {
115 cancel.cancel();
116 }
117 }
118}
119
120impl LaunchedEffectCancellation {
121 fn cancel(&self) {
122 self.active.store(false, Ordering::SeqCst);
123 #[cfg(not(target_arch = "wasm32"))]
124 {
125 let mut pending = self.continuations.borrow_mut();
126 for id in pending.drain(..) {
127 self.runtime.cancel_ui_cont(id);
128 }
129 }
130 }
131}
132
133impl LaunchedEffectAsyncState {
134 fn should_run(&self, key: &EffectKey) -> bool {
135 match &self.key {
136 Some(current) => key.differs_from(current),
137 None => true,
138 }
139 }
140
141 fn set_key(&mut self, key: EffectKey) {
142 self.key = Some(key);
143 }
144
145 fn set_site(&mut self, site: TaskSite) {
146 self.site = site;
147 }
148
149 fn launch(
150 &mut self,
151 runtime: RuntimeHandle,
152 mk_future: impl FnOnce(LaunchedEffectScope) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
153 ) {
154 self.cancel_current();
155 let active = Arc::new(AtomicBool::new(true));
156 #[cfg(not(target_arch = "wasm32"))]
157 let continuations = Rc::new(RefCell::new(Vec::new()));
158 self.cancel = Some(LaunchedEffectCancellation {
159 #[cfg(not(target_arch = "wasm32"))]
160 runtime: runtime.clone(),
161 active: Arc::clone(&active),
162 #[cfg(not(target_arch = "wasm32"))]
163 continuations: Rc::clone(&continuations),
164 });
165 let scope = LaunchedEffectScope {
166 active: Arc::clone(&active),
167 runtime: runtime.clone(),
168 #[cfg(not(target_arch = "wasm32"))]
169 continuations,
170 };
171 let future = mk_future(scope.clone());
172 let active_flag = Arc::clone(&scope.active);
173 crate::label_next_ui_task(self.site.to_string());
174 match runtime.spawn_ui(async move {
175 future.await;
176 active_flag.store(false, Ordering::SeqCst);
177 }) {
178 Some(handle) => {
179 self.task = Some(handle);
180 }
181 None => {
182 active.store(false, Ordering::SeqCst);
183 self.cancel = None;
184 }
185 }
186 }
187
188 fn cancel_current(&mut self) {
189 if let Some(handle) = self.task.take() {
190 handle.cancel();
191 }
192 if let Some(cancel) = self.cancel.take() {
193 cancel.cancel();
194 }
195 }
196}
197
198impl Drop for LaunchedEffectState {
199 fn drop(&mut self) {
200 self.cancel_current();
201 }
202}
203
204impl Drop for LaunchedEffectAsyncState {
205 fn drop(&mut self) {
206 self.cancel_current();
207 }
208}
209
210#[derive(Clone)]
211pub struct LaunchedEffectScope {
212 active: Arc<AtomicBool>,
213 runtime: RuntimeHandle,
214 #[cfg(not(target_arch = "wasm32"))]
215 continuations: Rc<RefCell<Vec<u64>>>,
216}
217
218impl LaunchedEffectScope {
219 #[cfg(not(target_arch = "wasm32"))]
220 fn track_continuation(&self, id: u64) {
221 self.continuations.borrow_mut().push(id);
222 }
223
224 #[cfg(not(target_arch = "wasm32"))]
225 fn release_continuation(&self, id: u64) {
226 let mut continuations = self.continuations.borrow_mut();
227 if let Some(index) = continuations.iter().position(|entry| *entry == id) {
228 continuations.remove(index);
229 }
230 }
231
232 pub fn is_active(&self) -> bool {
233 self.active.load(Ordering::SeqCst)
234 }
235
236 pub fn runtime(&self) -> RuntimeHandle {
237 self.runtime.clone()
238 }
239
240 pub fn launch(&self, task: impl FnOnce(LaunchedEffectScope) + 'static) {
246 if !self.is_active() {
247 return;
248 }
249 let scope = self.clone();
250 self.runtime.enqueue_ui_task(Box::new(move || {
251 if scope.is_active() {
252 task(scope);
253 }
254 }));
255 }
256
257 pub fn post_ui(&self, task: impl FnOnce() + 'static) {
262 if !self.is_active() {
263 return;
264 }
265 let active = Arc::clone(&self.active);
266 self.runtime.enqueue_ui_task(Box::new(move || {
267 if active.load(Ordering::SeqCst) {
268 task();
269 }
270 }));
271 }
272
273 #[cfg(not(target_arch = "wasm32"))]
280 pub fn launch_background<T, Work, Ui, Fut>(&self, work: Work, on_ui: Ui)
281 where
282 T: Send + 'static,
283 Work: FnOnce(CancelToken) -> Fut + Send + 'static,
284 Fut: Future<Output = T> + Send + 'static,
285 Ui: FnOnce(T) + 'static,
286 {
287 if !self.is_active() {
288 return;
289 }
290 let dispatcher = self.runtime.dispatcher();
291 let active_for_thread = Arc::clone(&self.active);
292 let continuation_scope = self.clone();
293 let continuation_active = Arc::clone(&self.active);
294 let id_cell = Rc::new(Cell::new(0));
295 let id_for_closure = Rc::clone(&id_cell);
296 let continuation = move |value: T| {
297 let id = id_for_closure.get();
298 continuation_scope.release_continuation(id);
299 if continuation_active.load(Ordering::SeqCst) {
300 on_ui(value);
301 }
302 };
303
304 let Some(cont_id) = self.runtime.register_ui_cont(continuation) else {
305 return;
306 };
307 id_cell.set(cont_id);
308 self.track_continuation(cont_id);
309
310 std::thread::spawn(move || {
311 let token = CancelToken::new(Arc::clone(&active_for_thread));
312 let value = pollster::block_on(work(token.clone()));
313 if token.is_cancelled() {
314 return;
315 }
316 dispatcher.post_invoke(cont_id, value);
317 });
318 }
319
320 #[cfg(target_arch = "wasm32")]
327 pub fn launch_background<T, Work, Ui, Fut>(&self, work: Work, on_ui: Ui)
328 where
329 T: 'static,
330 Work: FnOnce(CancelToken) -> Fut + 'static,
331 Fut: Future<Output = T> + 'static,
332 Ui: FnOnce(T) + 'static,
333 {
334 if !self.is_active() {
335 return;
336 }
337 let active_for_task = Arc::clone(&self.active);
338 let scope = self.clone();
339 wasm_bindgen_futures::spawn_local(async move {
340 let token = CancelToken::new(Arc::clone(&active_for_task));
341 let value = work(token.clone()).await;
342 if token.is_cancelled() {
343 return;
344 }
345 scope.post_ui(move || {
346 if token.is_active() {
347 on_ui(value);
348 }
349 });
350 });
351 }
352}
353
354#[derive(Clone)]
355pub struct CancelToken {
361 active: Arc<AtomicBool>,
362}
363
364impl CancelToken {
365 fn new(active: Arc<AtomicBool>) -> Self {
366 Self { active }
367 }
368
369 pub fn is_cancelled(&self) -> bool {
371 !self.active.load(Ordering::SeqCst)
372 }
373
374 pub fn is_active(&self) -> bool {
376 self.active.load(Ordering::SeqCst)
377 }
378}
379
380pub fn __launched_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
381where
382 K: PartialEq + 'static,
383 F: FnOnce(LaunchedEffectScope) + 'static,
384{
385 with_current_composer(|composer| {
388 composer.with_group(group_key, |composer| {
389 let key = EffectKey::new(keys);
390 let state = composer.remember_effect::<LaunchedEffectState>();
391 if state.with(|state| state.should_run(&key)) {
392 state.update(|state| state.set_key(key));
393 let runtime = composer.runtime_handle();
394 let state_for_effect = state.clone();
395 let mut effect_opt = Some(effect);
396 composer.register_side_effect(move || {
397 if let Some(effect) = effect_opt.take() {
398 state_for_effect.update(|state| state.launch(runtime.clone(), effect));
399 }
400 });
401 }
402 });
403 });
404}
405
406#[macro_export]
407macro_rules! LaunchedEffect {
408 ($keys:expr, $effect:expr) => {
409 $crate::__launched_effect_impl(
410 $crate::location_key(file!(), line!(), column!()),
411 $keys,
412 $effect,
413 )
414 };
415}
416
417pub fn __launched_effect_async_impl<K, F>(group_key: Key, site: TaskSite, keys: K, mk_future: F)
420where
421 K: PartialEq + 'static,
422 F: FnOnce(LaunchedEffectScope) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
423{
424 with_current_composer(|composer| {
425 composer.with_group(group_key, |composer| {
426 let key = EffectKey::new(keys);
427 let state = composer.remember_effect::<LaunchedEffectAsyncState>();
428 if state.with(|state| state.should_run(&key)) {
429 state.update(|state| {
430 state.set_key(key);
431 state.set_site(site);
432 });
433 let runtime = composer.runtime_handle();
434 let state_for_effect = state.clone();
435 let mut mk_future_opt = Some(mk_future);
436 composer.register_side_effect(move || {
437 if let Some(mk_future) = mk_future_opt.take() {
438 state_for_effect.update(|state| {
439 state.launch(runtime.clone(), mk_future);
440 });
441 }
442 });
443 }
444 });
445 });
446}
447
448#[macro_export]
449macro_rules! LaunchedEffectAsync {
450 ($keys:expr, $future:expr) => {
451 $crate::__launched_effect_async_impl(
452 $crate::location_key(file!(), line!(), column!()),
453 $crate::TaskSite::new(file!(), line!()),
454 $keys,
455 $future,
456 )
457 };
458}