1use std::fmt::{Debug, Formatter};
2use std::io;
3use std::sync::{Arc, RwLock};
4use std::thread::panicking;
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Instant;
7
8use crate::draw_target::{DrawState, DrawStateWrapper, LineAdjust, ProgressDrawTarget};
9use crate::progress_bar::ProgressBar;
10#[cfg(target_arch = "wasm32")]
11use instant::Instant;
12
13/// Manages multiple progress bars from different threads
14#[derive(Debug, Clone)]
15pub struct MultiProgress {
16 pub(crate) state: Arc<RwLock<MultiState>>,
17}
18
19impl Default for MultiProgress {
20 fn default() -> Self {
21 Self::with_draw_target(ProgressDrawTarget::stderr())
22 }
23}
24
25impl MultiProgress {
26 /// Creates a new multi progress object.
27 ///
28 /// Progress bars added to this object by default draw directly to stderr, and refresh
29 /// a maximum of 15 times a second. To change the refresh rate set the draw target to
30 /// one with a different refresh rate.
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 /// Creates a new multi progress object with the given draw target.
36 pub fn with_draw_target(draw_target: ProgressDrawTarget) -> Self {
37 Self {
38 state: Arc::new(RwLock::new(MultiState::new(draw_target))),
39 }
40 }
41
42 /// Sets a different draw target for the multiprogress bar.
43 pub fn set_draw_target(&self, target: ProgressDrawTarget) {
44 let mut state = self.state.write().unwrap();
45 state.draw_target.disconnect(Instant::now());
46 state.draw_target = target;
47 }
48
49 /// Set whether we should try to move the cursor when possible instead of clearing lines.
50 ///
51 /// This can reduce flickering, but do not enable it if you intend to change the number of
52 /// progress bars.
53 pub fn set_move_cursor(&self, move_cursor: bool) {
54 self.state.write().unwrap().move_cursor = move_cursor;
55 }
56
57 /// Set alignment flag
58 pub fn set_alignment(&self, alignment: MultiProgressAlignment) {
59 self.state.write().unwrap().alignment = alignment;
60 }
61
62 /// Adds a progress bar.
63 ///
64 /// The progress bar added will have the draw target changed to a
65 /// remote draw target that is intercepted by the multi progress
66 /// object overriding custom `ProgressDrawTarget` settings.
67 ///
68 /// Adding a progress bar that is already a member of the `MultiProgress`
69 /// will have no effect.
70 pub fn add(&self, pb: ProgressBar) -> ProgressBar {
71 self.internalize(InsertLocation::End, pb)
72 }
73
74 /// Inserts a progress bar.
75 ///
76 /// The progress bar inserted at position `index` will have the draw
77 /// target changed to a remote draw target that is intercepted by the
78 /// multi progress object overriding custom `ProgressDrawTarget` settings.
79 ///
80 /// If `index >= MultiProgressState::objects.len()`, the progress bar
81 /// is added to the end of the list.
82 ///
83 /// Inserting a progress bar that is already a member of the `MultiProgress`
84 /// will have no effect.
85 pub fn insert(&self, index: usize, pb: ProgressBar) -> ProgressBar {
86 self.internalize(InsertLocation::Index(index), pb)
87 }
88
89 /// Inserts a progress bar from the back.
90 ///
91 /// The progress bar inserted at position `MultiProgressState::objects.len() - index`
92 /// will have the draw target changed to a remote draw target that is
93 /// intercepted by the multi progress object overriding custom
94 /// `ProgressDrawTarget` settings.
95 ///
96 /// If `index >= MultiProgressState::objects.len()`, the progress bar
97 /// is added to the start of the list.
98 ///
99 /// Inserting a progress bar that is already a member of the `MultiProgress`
100 /// will have no effect.
101 pub fn insert_from_back(&self, index: usize, pb: ProgressBar) -> ProgressBar {
102 self.internalize(InsertLocation::IndexFromBack(index), pb)
103 }
104
105 /// Inserts a progress bar before an existing one.
106 ///
107 /// The progress bar added will have the draw target changed to a
108 /// remote draw target that is intercepted by the multi progress
109 /// object overriding custom `ProgressDrawTarget` settings.
110 ///
111 /// Inserting a progress bar that is already a member of the `MultiProgress`
112 /// will have no effect.
113 pub fn insert_before(&self, before: &ProgressBar, pb: ProgressBar) -> ProgressBar {
114 self.internalize(InsertLocation::Before(before.index().unwrap()), pb)
115 }
116
117 /// Inserts a progress bar after an existing one.
118 ///
119 /// The progress bar added will have the draw target changed to a
120 /// remote draw target that is intercepted by the multi progress
121 /// object overriding custom `ProgressDrawTarget` settings.
122 ///
123 /// Inserting a progress bar that is already a member of the `MultiProgress`
124 /// will have no effect.
125 pub fn insert_after(&self, after: &ProgressBar, pb: ProgressBar) -> ProgressBar {
126 self.internalize(InsertLocation::After(after.index().unwrap()), pb)
127 }
128
129 /// Removes a progress bar.
130 ///
131 /// The progress bar is removed only if it was previously inserted or added
132 /// by the methods `MultiProgress::insert` or `MultiProgress::add`.
133 /// If the passed progress bar does not satisfy the condition above,
134 /// the `remove` method does nothing.
135 pub fn remove(&self, pb: &ProgressBar) {
136 let mut state = pb.state();
137 let idx = match &state.draw_target.remote() {
138 Some((state, idx)) => {
139 // Check that this progress bar is owned by the current MultiProgress.
140 assert!(Arc::ptr_eq(&self.state, state));
141 *idx
142 }
143 _ => return,
144 };
145
146 state.draw_target = ProgressDrawTarget::hidden();
147 self.state.write().unwrap().remove_idx(idx);
148 }
149
150 fn internalize(&self, location: InsertLocation, pb: ProgressBar) -> ProgressBar {
151 let mut state = self.state.write().unwrap();
152 let idx = state.insert(location);
153 drop(state);
154
155 pb.set_draw_target(ProgressDrawTarget::new_remote(self.state.clone(), idx));
156 pb
157 }
158
159 /// Print a log line above all progress bars in the [`MultiProgress`]
160 ///
161 /// If the draw target is hidden (e.g. when standard output is not a terminal), `println()`
162 /// will not do anything.
163 pub fn println<I: AsRef<str>>(&self, msg: I) -> io::Result<()> {
164 let mut state = self.state.write().unwrap();
165 state.println(msg, Instant::now())
166 }
167
168 /// Hide all progress bars temporarily, execute `f`, then redraw the [`MultiProgress`]
169 ///
170 /// Executes 'f' even if the draw target is hidden.
171 ///
172 /// Useful for external code that writes to the standard output.
173 ///
174 /// **Note:** The internal lock is held while `f` is executed. Other threads trying to print
175 /// anything on the progress bar will be blocked until `f` finishes.
176 /// Therefore, it is recommended to avoid long-running operations in `f`.
177 pub fn suspend<F: FnOnce() -> R, R>(&self, f: F) -> R {
178 let mut state = self.state.write().unwrap();
179 state.suspend(f, Instant::now())
180 }
181
182 pub fn clear(&self) -> io::Result<()> {
183 self.state.write().unwrap().clear(Instant::now())
184 }
185
186 pub fn is_hidden(&self) -> bool {
187 self.state.read().unwrap().is_hidden()
188 }
189}
190
191#[derive(Debug)]
192pub(crate) struct MultiState {
193 /// The collection of states corresponding to progress bars
194 members: Vec<MultiStateMember>,
195 /// Set of removed bars, should have corresponding members in the `members` vector with a
196 /// `draw_state` of `None`.
197 free_set: Vec<usize>,
198 /// Indices to the `draw_states` to maintain correct visual order
199 ordering: Vec<usize>,
200 /// Target for draw operation for MultiProgress
201 draw_target: ProgressDrawTarget,
202 /// Whether or not to just move cursor instead of clearing lines
203 move_cursor: bool,
204 /// Controls how the multi progress is aligned if some of its progress bars get removed, default is `Top`
205 alignment: MultiProgressAlignment,
206 /// Lines to be drawn above everything else in the MultiProgress. These specifically come from
207 /// calling `ProgressBar::println` on a pb that is connected to a `MultiProgress`.
208 orphan_lines: Vec<String>,
209 /// The count of currently visible zombie lines.
210 zombie_lines_count: usize,
211}
212
213impl MultiState {
214 fn new(draw_target: ProgressDrawTarget) -> Self {
215 Self {
216 members: vec![],
217 free_set: vec![],
218 ordering: vec![],
219 draw_target,
220 move_cursor: false,
221 alignment: MultiProgressAlignment::default(),
222 orphan_lines: Vec::new(),
223 zombie_lines_count: 0,
224 }
225 }
226
227 pub(crate) fn mark_zombie(&mut self, index: usize) {
228 let member = &mut self.members[index];
229
230 // If the zombie is the first visual bar then we can reap it right now instead of
231 // deferring it to the next draw.
232 if index != self.ordering.first().copied().unwrap() {
233 member.is_zombie = true;
234 return;
235 }
236
237 let line_count = member
238 .draw_state
239 .as_ref()
240 .map(|d| d.lines.len())
241 .unwrap_or_default();
242
243 // Track the total number of zombie lines on the screen
244 self.zombie_lines_count += line_count;
245
246 // Make `DrawTarget` forget about the zombie lines so that they aren't cleared on next draw.
247 self.draw_target
248 .adjust_last_line_count(LineAdjust::Keep(line_count));
249
250 self.remove_idx(index);
251 }
252
253 pub(crate) fn draw(
254 &mut self,
255 mut force_draw: bool,
256 extra_lines: Option<Vec<String>>,
257 now: Instant,
258 ) -> io::Result<()> {
259 if panicking() {
260 return Ok(());
261 }
262 let width = self.width() as f64;
263 // Calculate real length based on terminal width
264 // This take in account linewrap from terminal
265 fn real_len(lines: &[String], width: f64) -> usize {
266 lines.iter().fold(0, |sum, val| {
267 sum + (console::measure_text_width(val) as f64 / width).ceil() as usize
268 })
269 }
270
271 // Assumption: if extra_lines is not None, then it has at least one line
272 debug_assert_eq!(
273 extra_lines.is_some(),
274 extra_lines.as_ref().map(Vec::len).unwrap_or_default() > 0
275 );
276
277 let mut reap_indices = vec![];
278
279 // Reap all consecutive 'zombie' progress bars from head of the list.
280 let mut adjust = 0;
281 for &index in &self.ordering {
282 let member = &self.members[index];
283 if !member.is_zombie {
284 break;
285 }
286
287 let line_count = member
288 .draw_state
289 .as_ref()
290 .map(|d| real_len(&d.lines, width))
291 .unwrap_or_default();
292 // Track the total number of zombie lines on the screen.
293 self.zombie_lines_count += line_count;
294
295 // Track the number of zombie lines that will be drawn by this call to draw.
296 adjust += line_count;
297
298 reap_indices.push(index);
299 }
300
301 // If this draw is due to a `println`, then we need to erase all the zombie lines.
302 // This is because `println` is supposed to appear above all other elements in the
303 // `MultiProgress`.
304 if extra_lines.is_some() {
305 self.draw_target
306 .adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count));
307 self.zombie_lines_count = 0;
308 }
309
310 let orphan_lines_count = real_len(&self.orphan_lines, width);
311 force_draw |= orphan_lines_count > 0;
312 let mut drawable = match self.draw_target.drawable(force_draw, now) {
313 Some(drawable) => drawable,
314 None => return Ok(()),
315 };
316
317 let mut draw_state = drawable.state();
318 draw_state.orphan_lines_count = orphan_lines_count;
319 draw_state.alignment = self.alignment;
320
321 if let Some(extra_lines) = &extra_lines {
322 draw_state.lines.extend_from_slice(extra_lines.as_slice());
323 draw_state.orphan_lines_count += real_len(extra_lines, width);
324 }
325
326 // Add lines from `ProgressBar::println` call.
327 draw_state.lines.append(&mut self.orphan_lines);
328
329 for index in &self.ordering {
330 let member = &self.members[*index];
331 if let Some(state) = &member.draw_state {
332 draw_state.lines.extend_from_slice(&state.lines[..]);
333 }
334 }
335
336 drop(draw_state);
337 let drawable = drawable.draw();
338
339 for index in reap_indices {
340 self.remove_idx(index);
341 }
342
343 // The zombie lines were drawn for the last time, so make `DrawTarget` forget about them
344 // so they aren't cleared on next draw.
345 if extra_lines.is_none() {
346 self.draw_target
347 .adjust_last_line_count(LineAdjust::Keep(adjust));
348 }
349
350 drawable
351 }
352
353 pub(crate) fn println<I: AsRef<str>>(&mut self, msg: I, now: Instant) -> io::Result<()> {
354 let msg = msg.as_ref();
355
356 // If msg is "", make sure a line is still printed
357 let lines: Vec<String> = match msg.is_empty() {
358 false => msg.lines().map(Into::into).collect(),
359 true => vec![String::new()],
360 };
361
362 self.draw(true, Some(lines), now)
363 }
364
365 pub(crate) fn draw_state(&mut self, idx: usize) -> DrawStateWrapper<'_> {
366 let member = self.members.get_mut(idx).unwrap();
367 // alignment is handled by the `MultiProgress`'s underlying draw target, so there is no
368 // point in propagating it here.
369 let state = member.draw_state.get_or_insert(DrawState {
370 move_cursor: self.move_cursor,
371 ..Default::default()
372 });
373
374 DrawStateWrapper::for_multi(state, &mut self.orphan_lines)
375 }
376
377 pub(crate) fn is_hidden(&self) -> bool {
378 self.draw_target.is_hidden()
379 }
380
381 pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, f: F, now: Instant) -> R {
382 self.clear(now).unwrap();
383 let ret = f();
384 self.draw(true, None, Instant::now()).unwrap();
385 ret
386 }
387
388 pub(crate) fn width(&self) -> u16 {
389 self.draw_target.width()
390 }
391
392 fn insert(&mut self, location: InsertLocation) -> usize {
393 let idx = if let Some(idx) = self.free_set.pop() {
394 self.members[idx] = MultiStateMember::default();
395 idx
396 } else {
397 self.members.push(MultiStateMember::default());
398 self.members.len() - 1
399 };
400
401 match location {
402 InsertLocation::End => self.ordering.push(idx),
403 InsertLocation::Index(pos) => {
404 let pos = Ord::min(pos, self.ordering.len());
405 self.ordering.insert(pos, idx);
406 }
407 InsertLocation::IndexFromBack(pos) => {
408 let pos = self.ordering.len().saturating_sub(pos);
409 self.ordering.insert(pos, idx);
410 }
411 InsertLocation::After(after_idx) => {
412 let pos = self.ordering.iter().position(|i| *i == after_idx).unwrap();
413 self.ordering.insert(pos + 1, idx);
414 }
415 InsertLocation::Before(before_idx) => {
416 let pos = self.ordering.iter().position(|i| *i == before_idx).unwrap();
417 self.ordering.insert(pos, idx);
418 }
419 }
420
421 assert_eq!(
422 self.len(),
423 self.ordering.len(),
424 "Draw state is inconsistent"
425 );
426
427 idx
428 }
429
430 fn clear(&mut self, now: Instant) -> io::Result<()> {
431 match self.draw_target.drawable(true, now) {
432 Some(mut drawable) => {
433 // Make the clear operation also wipe out zombie lines
434 drawable.adjust_last_line_count(LineAdjust::Clear(self.zombie_lines_count));
435 self.zombie_lines_count = 0;
436 drawable.clear()
437 }
438 None => Ok(()),
439 }
440 }
441
442 fn remove_idx(&mut self, idx: usize) {
443 if self.free_set.contains(&idx) {
444 return;
445 }
446
447 self.members[idx] = MultiStateMember::default();
448 self.free_set.push(idx);
449 self.ordering.retain(|&x| x != idx);
450
451 assert_eq!(
452 self.len(),
453 self.ordering.len(),
454 "Draw state is inconsistent"
455 );
456 }
457
458 fn len(&self) -> usize {
459 self.members.len() - self.free_set.len()
460 }
461}
462
463#[derive(Default)]
464struct MultiStateMember {
465 /// Draw state will be `None` for members that haven't been drawn before, or for entries that
466 /// correspond to something in the free set.
467 draw_state: Option<DrawState>,
468 /// Whether the corresponding progress bar (more precisely, `BarState`) has been dropped.
469 is_zombie: bool,
470}
471
472impl Debug for MultiStateMember {
473 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
474 f&mut DebugStruct<'_, '_>.debug_struct("MultiStateElement")
475 .field("draw_state", &self.draw_state)
476 .field(name:"is_zombie", &self.is_zombie)
477 .finish_non_exhaustive()
478 }
479}
480
481/// Vertical alignment of a multi progress.
482///
483/// The alignment controls how the multi progress is aligned if some of its progress bars get removed.
484/// E.g. `Top` alignment (default), when _progress bar 2_ is removed:
485/// ```ignore
486/// [0/100] progress bar 1 [0/100] progress bar 1
487/// [0/100] progress bar 2 => [0/100] progress bar 3
488/// [0/100] progress bar 3
489/// ```
490///
491/// `Bottom` alignment
492/// ```ignore
493/// [0/100] progress bar 1
494/// [0/100] progress bar 2 => [0/100] progress bar 1
495/// [0/100] progress bar 3 [0/100] progress bar 3
496/// ```
497#[derive(Debug, Copy, Clone)]
498pub enum MultiProgressAlignment {
499 Top,
500 Bottom,
501}
502
503impl Default for MultiProgressAlignment {
504 fn default() -> Self {
505 Self::Top
506 }
507}
508
509enum InsertLocation {
510 End,
511 Index(usize),
512 IndexFromBack(usize),
513 After(usize),
514 Before(usize),
515}
516
517#[cfg(test)]
518mod tests {
519 use crate::{MultiProgress, ProgressBar, ProgressDrawTarget};
520
521 #[test]
522 fn late_pb_drop() {
523 let pb = ProgressBar::new(10);
524 let mpb = MultiProgress::new();
525 // This clone call is required to trigger a now fixed bug.
526 // See <https://github.com/console-rs/indicatif/pull/141> for context
527 #[allow(clippy::redundant_clone)]
528 mpb.add(pb.clone());
529 }
530
531 #[test]
532 fn progress_bar_sync_send() {
533 let _: Box<dyn Sync> = Box::new(ProgressBar::new(1));
534 let _: Box<dyn Send> = Box::new(ProgressBar::new(1));
535 let _: Box<dyn Sync> = Box::new(MultiProgress::new());
536 let _: Box<dyn Send> = Box::new(MultiProgress::new());
537 }
538
539 #[test]
540 fn multi_progress_hidden() {
541 let mpb = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
542 let pb = mpb.add(ProgressBar::new(123));
543 pb.finish();
544 }
545
546 #[test]
547 fn multi_progress_modifications() {
548 let mp = MultiProgress::new();
549 let p0 = mp.add(ProgressBar::new(1));
550 let p1 = mp.add(ProgressBar::new(1));
551 let p2 = mp.add(ProgressBar::new(1));
552 let p3 = mp.add(ProgressBar::new(1));
553 mp.remove(&p2);
554 mp.remove(&p1);
555 let p4 = mp.insert(1, ProgressBar::new(1));
556
557 let state = mp.state.read().unwrap();
558 // the removed place for p1 is reused
559 assert_eq!(state.members.len(), 4);
560 assert_eq!(state.len(), 3);
561
562 // free_set may contain 1 or 2
563 match state.free_set.last() {
564 Some(1) => {
565 assert_eq!(state.ordering, vec![0, 2, 3]);
566 assert!(state.members[1].draw_state.is_none());
567 assert_eq!(p4.index().unwrap(), 2);
568 }
569 Some(2) => {
570 assert_eq!(state.ordering, vec![0, 1, 3]);
571 assert!(state.members[2].draw_state.is_none());
572 assert_eq!(p4.index().unwrap(), 1);
573 }
574 _ => unreachable!(),
575 }
576
577 assert_eq!(p0.index().unwrap(), 0);
578 assert_eq!(p1.index(), None);
579 assert_eq!(p2.index(), None);
580 assert_eq!(p3.index().unwrap(), 3);
581 }
582
583 #[test]
584 fn multi_progress_insert_from_back() {
585 let mp = MultiProgress::new();
586 let p0 = mp.add(ProgressBar::new(1));
587 let p1 = mp.add(ProgressBar::new(1));
588 let p2 = mp.add(ProgressBar::new(1));
589 let p3 = mp.insert_from_back(1, ProgressBar::new(1));
590 let p4 = mp.insert_from_back(10, ProgressBar::new(1));
591
592 let state = mp.state.read().unwrap();
593 assert_eq!(state.ordering, vec![4, 0, 1, 3, 2]);
594 assert_eq!(p0.index().unwrap(), 0);
595 assert_eq!(p1.index().unwrap(), 1);
596 assert_eq!(p2.index().unwrap(), 2);
597 assert_eq!(p3.index().unwrap(), 3);
598 assert_eq!(p4.index().unwrap(), 4);
599 }
600
601 #[test]
602 fn multi_progress_insert_after() {
603 let mp = MultiProgress::new();
604 let p0 = mp.add(ProgressBar::new(1));
605 let p1 = mp.add(ProgressBar::new(1));
606 let p2 = mp.add(ProgressBar::new(1));
607 let p3 = mp.insert_after(&p2, ProgressBar::new(1));
608 let p4 = mp.insert_after(&p0, ProgressBar::new(1));
609
610 let state = mp.state.read().unwrap();
611 assert_eq!(state.ordering, vec![0, 4, 1, 2, 3]);
612 assert_eq!(p0.index().unwrap(), 0);
613 assert_eq!(p1.index().unwrap(), 1);
614 assert_eq!(p2.index().unwrap(), 2);
615 assert_eq!(p3.index().unwrap(), 3);
616 assert_eq!(p4.index().unwrap(), 4);
617 }
618
619 #[test]
620 fn multi_progress_insert_before() {
621 let mp = MultiProgress::new();
622 let p0 = mp.add(ProgressBar::new(1));
623 let p1 = mp.add(ProgressBar::new(1));
624 let p2 = mp.add(ProgressBar::new(1));
625 let p3 = mp.insert_before(&p0, ProgressBar::new(1));
626 let p4 = mp.insert_before(&p2, ProgressBar::new(1));
627
628 let state = mp.state.read().unwrap();
629 assert_eq!(state.ordering, vec![3, 0, 1, 4, 2]);
630 assert_eq!(p0.index().unwrap(), 0);
631 assert_eq!(p1.index().unwrap(), 1);
632 assert_eq!(p2.index().unwrap(), 2);
633 assert_eq!(p3.index().unwrap(), 3);
634 assert_eq!(p4.index().unwrap(), 4);
635 }
636
637 #[test]
638 fn multi_progress_insert_before_and_after() {
639 let mp = MultiProgress::new();
640 let p0 = mp.add(ProgressBar::new(1));
641 let p1 = mp.add(ProgressBar::new(1));
642 let p2 = mp.add(ProgressBar::new(1));
643 let p3 = mp.insert_before(&p0, ProgressBar::new(1));
644 let p4 = mp.insert_after(&p3, ProgressBar::new(1));
645 let p5 = mp.insert_after(&p3, ProgressBar::new(1));
646 let p6 = mp.insert_before(&p1, ProgressBar::new(1));
647
648 let state = mp.state.read().unwrap();
649 assert_eq!(state.ordering, vec![3, 5, 4, 0, 6, 1, 2]);
650 assert_eq!(p0.index().unwrap(), 0);
651 assert_eq!(p1.index().unwrap(), 1);
652 assert_eq!(p2.index().unwrap(), 2);
653 assert_eq!(p3.index().unwrap(), 3);
654 assert_eq!(p4.index().unwrap(), 4);
655 assert_eq!(p5.index().unwrap(), 5);
656 assert_eq!(p6.index().unwrap(), 6);
657 }
658
659 #[test]
660 fn multi_progress_multiple_remove() {
661 let mp = MultiProgress::new();
662 let p0 = mp.add(ProgressBar::new(1));
663 let p1 = mp.add(ProgressBar::new(1));
664 // double remove beyond the first one have no effect
665 mp.remove(&p0);
666 mp.remove(&p0);
667 mp.remove(&p0);
668
669 let state = mp.state.read().unwrap();
670 // the removed place for p1 is reused
671 assert_eq!(state.members.len(), 2);
672 assert_eq!(state.free_set.len(), 1);
673 assert_eq!(state.len(), 1);
674 assert!(state.members[0].draw_state.is_none());
675 assert_eq!(state.free_set.last(), Some(&0));
676
677 assert_eq!(state.ordering, vec![1]);
678 assert_eq!(p0.index(), None);
679 assert_eq!(p1.index().unwrap(), 1);
680 }
681
682 #[test]
683 fn mp_no_crash_double_add() {
684 let mp = MultiProgress::new();
685 let pb = mp.add(ProgressBar::new(10));
686 mp.add(pb);
687 }
688}
689