cranpose_services/
background.rs1use std::sync::{
16 Arc, Mutex, MutexGuard, OnceLock, PoisonError,
17 atomic::{AtomicUsize, Ordering},
18};
19
20#[derive(Clone, Debug, PartialEq)]
22pub struct BackgroundWork {
23 pub title: String,
25 pub progress: Option<f32>,
27}
28
29pub trait BackgroundActivity: Send + Sync {
32 fn show(&self, work: Option<&BackgroundWork>);
36}
37
38pub type BackgroundActivityRef = Arc<dyn BackgroundActivity>;
39
40fn slot() -> &'static Mutex<Option<BackgroundActivityRef>> {
41 static SLOT: OnceLock<Mutex<Option<BackgroundActivityRef>>> = OnceLock::new();
42 SLOT.get_or_init(|| Mutex::new(None))
43}
44
45pub fn set_platform_background_activity(activity: BackgroundActivityRef) {
47 if let Some(work) = leases().shown() {
48 activity.show(Some(&work));
49 }
50 if let Ok(mut s) = slot().lock() {
51 *s = Some(activity);
52 }
53}
54
55pub fn clear_platform_background_activity() {
57 if let Ok(mut s) = slot().lock() {
58 *s = None;
59 }
60}
61
62pub fn background_activity() -> Option<BackgroundActivityRef> {
64 slot().lock().ok().and_then(|s| s.clone())
65}
66
67#[derive(Default)]
68struct Leases {
69 next: u64,
70 active: Vec<(u64, BackgroundWork)>,
72}
73
74impl Leases {
75 fn shown(&self) -> Option<BackgroundWork> {
76 self.active.last().map(|(_, work)| work.clone())
77 }
78}
79
80static ACTIVE_LEASES: AtomicUsize = AtomicUsize::new(0);
81
82fn leases() -> MutexGuard<'static, Leases> {
83 static LEASES: OnceLock<Mutex<Leases>> = OnceLock::new();
84 LEASES
85 .get_or_init(Mutex::default)
86 .lock()
87 .unwrap_or_else(PoisonError::into_inner)
88}
89
90fn update(change: impl FnOnce(&mut Leases)) {
93 let (before, after) = {
94 let mut leases = leases();
95 let before = leases.shown().map(|work| Shown::of(&work));
96 change(&mut leases);
97 ACTIVE_LEASES.store(leases.active.len(), Ordering::Release);
98 (before, leases.shown())
99 };
100 if before != after.as_ref().map(Shown::of)
101 && let Some(activity) = background_activity()
102 {
103 activity.show(after.as_ref());
104 }
105}
106
107#[derive(PartialEq)]
110struct Shown {
111 title: String,
112 percent: Option<u8>,
113}
114
115impl Shown {
116 fn of(work: &BackgroundWork) -> Self {
117 Self {
118 title: work.title.clone(),
119 percent: work
120 .progress
121 .map(|fraction| (fraction.clamp(0.0, 1.0) * 100.0) as u8),
122 }
123 }
124}
125
126pub struct BackgroundWorkLease {
129 id: u64,
130}
131
132impl BackgroundWorkLease {
133 pub fn set_progress(&self, progress: Option<f32>) {
136 self.change(|work| work.progress = progress);
137 }
138
139 pub fn set_title(&self, title: impl Into<String>) {
141 let title = title.into();
142 self.change(|work| work.title = title);
143 }
144
145 fn change(&self, change: impl FnOnce(&mut BackgroundWork)) {
146 update(|leases| {
147 if let Some((_, work)) = leases.active.iter_mut().find(|(id, _)| *id == self.id) {
148 change(work);
149 }
150 });
151 }
152}
153
154impl Drop for BackgroundWorkLease {
155 fn drop(&mut self) {
156 update(|leases| leases.active.retain(|(id, _)| *id != self.id));
157 }
158}
159
160pub fn acquire_background_work(title: impl Into<String>) -> BackgroundWorkLease {
163 let title = title.into();
164 let mut id = 0;
165 update(|leases| {
166 id = leases.next;
167 leases.next += 1;
168 leases.active.push((
169 id,
170 BackgroundWork {
171 title,
172 progress: None,
173 },
174 ));
175 });
176 BackgroundWorkLease { id }
177}
178
179pub fn background_active() -> bool {
188 ACTIVE_LEASES.load(Ordering::Acquire) != 0
189}
190
191#[cfg(test)]
192#[path = "tests/background_tests.rs"]
193mod tests;