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