1#![allow(clippy::unwrap_used, reason = "lock poisoning is unrecoverable")]
3
4use std::sync::{
5 Arc, Mutex,
6 atomic::{AtomicU64, Ordering},
7};
8
9use crate::error::CoreError;
10
11#[derive(Debug)]
17struct IntervalInfo {
18 width: AtomicU64,
21 progress: AtomicU64,
23}
24
25#[derive(Debug)]
27struct ProgressTree {
28 intervals: Mutex<Vec<Arc<IntervalInfo>>>,
29}
30
31#[derive(Clone, Debug)]
51pub struct ProgressToken {
52 tree: Arc<ProgressTree>,
53 interval: Arc<IntervalInfo>,
54}
55
56impl ProgressToken {
57 pub fn new() -> Self {
59 let interval = Arc::new(IntervalInfo {
60 width: AtomicU64::new(1.0_f64.to_bits()),
61 progress: AtomicU64::new(0.0_f64.to_bits()),
62 });
63 let tree = Arc::new(ProgressTree {
64 intervals: Mutex::new(vec![Arc::clone(&interval)]),
65 });
66 Self { tree, interval }
67 }
68
69 pub fn set(&self, fraction: f64) {
73 let f = fraction.clamp(0.0, 1.0);
74 self.interval.progress.store(f.to_bits(), Ordering::Relaxed);
75 }
76
77 pub fn fraction(&self) -> f64 {
79 f64::from_bits(self.interval.progress.load(Ordering::Relaxed))
80 }
81
82 pub fn width(&self) -> f64 {
84 f64::from_bits(self.interval.width.load(Ordering::Relaxed))
85 }
86
87 pub fn is_complete(&self) -> bool {
89 self.fraction() >= 1.0
90 }
91
92 pub fn root_fraction(&self) -> f64 {
95 let intervals = self.tree.intervals.lock().unwrap(); let sum: f64 = intervals
97 .iter()
98 .map(|iv| {
99 let w = f64::from_bits(iv.width.load(Ordering::Relaxed));
100 let p = f64::from_bits(iv.progress.load(Ordering::Relaxed));
101 w * p
102 })
103 .sum();
104 sum.clamp(0.0, 1.0)
105 }
106
107 pub fn split(&self, weights: &[u32]) -> Result<Vec<Self>, CoreError> {
114 if weights.is_empty() {
115 return Err(CoreError::InvalidProgressSplit {
116 reason: "weights must not be empty".into(),
117 });
118 }
119 if let Some(i) = weights.iter().position(|&w| w == 0) {
120 return Err(CoreError::InvalidProgressSplit {
121 reason: format!("weight at index {i} must be positive"),
122 });
123 }
124 let total_w: f64 = weights.iter().map(|&w| w as f64).sum();
125
126 let my_width = self.width();
127
128 self.interval
130 .width
131 .store(0.0_f64.to_bits(), Ordering::Relaxed);
132 self.interval
133 .progress
134 .store(0.0_f64.to_bits(), Ordering::Relaxed);
135
136 let mut intervals = self.tree.intervals.lock().unwrap(); Ok(weights
139 .iter()
140 .map(|&w| {
141 let child_width = (w as f64 / total_w) * my_width;
142 let iv = Arc::new(IntervalInfo {
143 width: AtomicU64::new(child_width.to_bits()),
144 progress: AtomicU64::new(0.0_f64.to_bits()),
145 });
146 intervals.push(Arc::clone(&iv));
147 Self {
148 tree: Arc::clone(&self.tree),
149 interval: iv,
150 }
151 })
152 .collect())
153 }
154
155 pub fn subtoken(&self, frac_width: f64) -> Self {
160 let frac = frac_width.clamp(0.0, 1.0);
161 let my_width = self.width();
162 let child_width = frac * my_width;
163 let remaining = (my_width - child_width).max(0.0);
164
165 self.interval
167 .width
168 .store(remaining.to_bits(), Ordering::Relaxed);
169
170 let iv = Arc::new(IntervalInfo {
171 width: AtomicU64::new(child_width.to_bits()),
172 progress: AtomicU64::new(0.0_f64.to_bits()),
173 });
174
175 let mut intervals = self.tree.intervals.lock().unwrap(); intervals.push(Arc::clone(&iv));
177
178 Self {
179 tree: Arc::clone(&self.tree),
180 interval: iv,
181 }
182 }
183}
184
185impl Default for ProgressToken {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_root_set_and_fraction() {
197 let token = ProgressToken::new();
198 assert_eq!(token.fraction(), 0.0);
199 assert_eq!(token.root_fraction(), 0.0);
200
201 token.set(0.5);
202 assert!((token.fraction() - 0.5).abs() < f64::EPSILON);
203 assert!((token.root_fraction() - 0.5).abs() < f64::EPSILON);
204
205 token.set(1.0);
206 assert!(token.is_complete());
207 assert!((token.root_fraction() - 1.0).abs() < f64::EPSILON);
208 }
209
210 #[test]
211 fn test_set_clamps_to_unit_range() {
212 let token = ProgressToken::new();
213 token.set(2.0);
214 assert!((token.fraction() - 1.0).abs() < f64::EPSILON);
215 token.set(-0.5);
216 assert!(token.fraction().abs() < f64::EPSILON);
217 }
218
219 #[test]
220 fn test_split_creates_subtokens_with_correct_widths() {
221 let root = ProgressToken::new();
222 let subs = root.split(&[1, 2, 1]).unwrap();
223 assert_eq!(subs.len(), 3);
224 assert!((subs[0].width() - 0.25).abs() < f64::EPSILON);
225 assert!((subs[1].width() - 0.5).abs() < f64::EPSILON);
226 assert!((subs[2].width() - 0.25).abs() < f64::EPSILON);
227 assert!(root.width().abs() < f64::EPSILON);
228 }
229
230 #[test]
231 fn test_subtokens_sum_on_root() {
232 let root = ProgressToken::new();
233 let subs = root.split(&[1, 1]).unwrap();
234 subs[0].set(1.0);
235 subs[1].set(1.0);
236 assert!((root.root_fraction() - 1.0).abs() < f64::EPSILON);
237 }
238
239 #[test]
240 fn test_partial_subtoken_progress() {
241 let root = ProgressToken::new();
242 let subs = root.split(&[1, 1]).unwrap();
243 subs[0].set(0.5); subs[1].set(0.0);
245 assert!((root.root_fraction() - 0.25).abs() < f64::EPSILON);
246 }
247
248 #[test]
249 fn test_nested_split() {
250 let root = ProgressToken::new();
251 let subs = root.split(&[1, 1]).unwrap(); let nested = subs[0].split(&[1, 1]).unwrap(); assert!((nested[0].width() - 0.25).abs() < f64::EPSILON);
254 assert!((nested[1].width() - 0.25).abs() < f64::EPSILON);
255
256 nested[0].set(1.0); nested[1].set(1.0); subs[1].set(1.0); assert!((root.root_fraction() - 1.0).abs() < f64::EPSILON);
260 }
261
262 #[test]
263 fn test_split_after_set_retracts_parent() {
264 let root = ProgressToken::new();
265 root.set(0.5);
266 assert!((root.root_fraction() - 0.5).abs() < f64::EPSILON);
267
268 let subs = root.split(&[1, 1]).unwrap();
269 assert!(root.root_fraction() < f64::EPSILON);
271
272 subs[0].set(1.0);
273 subs[1].set(1.0);
274 assert!((root.root_fraction() - 1.0).abs() < f64::EPSILON);
275 }
276
277 #[test]
278 fn test_subtoken_shrinks_parent() {
279 let root = ProgressToken::new();
280 assert!((root.width() - 1.0).abs() < f64::EPSILON);
281
282 let child = root.subtoken(0.3);
283 assert!((child.width() - 0.3).abs() < f64::EPSILON);
284 assert!((root.width() - 0.7).abs() < f64::EPSILON);
285
286 child.set(1.0); root.set(1.0); assert!((root.root_fraction() - 1.0).abs() < f64::EPSILON);
289 }
290
291 #[test]
292 fn test_split_rejects_zero_weight() {
293 let root = ProgressToken::new();
294 let err = root.split(&[1, 0, 1]).unwrap_err();
295 assert!(err.to_string().contains("index 1"));
296 assert!((root.width() - 1.0).abs() < f64::EPSILON);
298 }
299
300 #[test]
301 fn test_split_rejects_empty_weights() {
302 let root = ProgressToken::new();
303 let err = root.split(&[]).unwrap_err();
304 assert!(err.to_string().contains("empty"));
305 assert!((root.width() - 1.0).abs() < f64::EPSILON);
306 }
307
308 #[test]
309 fn test_clone_shares_interval() {
310 let root = ProgressToken::new();
311 let clone = root.clone();
312 root.set(0.7);
313 assert!((clone.fraction() - 0.7).abs() < f64::EPSILON);
314 }
315}