1use std::fmt::{Display, Formatter};
2use std::fs;
3use std::io::Cursor;
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::sync::atomic::AtomicU64;
7
8use crate::async_util::{perform_async_work, perform_work};
9use crate::channels::checked_send;
10use crate::cxxrtl_container::CxxrtlContainer;
11use crate::file_dialog::OpenMode;
12use crate::remote::{get_hierarchy_from_server, get_server_status, server_reload};
13use crate::transactions::TRANSACTIONS_FILE_EXTENSION;
14use crate::util::get_multi_extension;
15use camino::{Utf8Path, Utf8PathBuf};
16use eyre::Report;
17use eyre::Result;
18use eyre::{WrapErr as _, anyhow};
19use ftr_parser::parse;
20use futures_util::FutureExt;
21use serde::{Deserialize, Serialize};
22use tracing::{error, info, warn};
23use web_time::Instant;
24
25use crate::transaction_container::TransactionContainer;
26use crate::wave_container::WaveContainer;
27use crate::wellen::{
28 BodyResult, HeaderResult, LoadSignalPayload, LoadSignalsCmd, LoadSignalsResult,
29};
30use crate::{SystemState, message::Message};
31use surver::{
32 HTTP_SERVER_KEY, HTTP_SERVER_VALUE_SURFER, SurverFileInfo, WELLEN_SURFER_DEFAULT_OPTIONS,
33};
34
35#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
36pub enum CxxrtlKind {
37 Tcp { url: String },
38 Mailbox,
39}
40impl std::fmt::Display for CxxrtlKind {
41 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42 match self {
43 CxxrtlKind::Tcp { url } => write!(f, "cxxrtl+tcp://{url}"),
44 CxxrtlKind::Mailbox => write!(f, "cxxrtl mailbox"),
45 }
46 }
47}
48
49#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
50pub enum WaveSource {
51 File(Utf8PathBuf),
52 Data,
53 DragAndDrop(Option<Utf8PathBuf>),
54 Url(String),
55 Cxxrtl(CxxrtlKind),
56}
57
58pub const STATE_FILE_EXTENSION: &str = "surf.ron";
59
60impl WaveSource {
61 #[must_use]
62 pub fn as_file(&self) -> Option<&Utf8Path> {
63 match self {
64 WaveSource::File(path) => Some(path.as_path()),
65 _ => None,
66 }
67 }
68
69 #[must_use]
70 pub fn path(&self) -> Option<&Utf8PathBuf> {
71 match self {
72 WaveSource::File(path) => Some(path),
73 WaveSource::DragAndDrop(Some(path)) => Some(path),
74 _ => None,
75 }
76 }
77
78 #[must_use]
79 pub fn sibling_state_file(&self) -> Option<Utf8PathBuf> {
80 let path = self.path()?;
81 let directory = path.parent()?;
82 let paths = fs::read_dir(directory).ok()?;
83
84 for entry in paths {
85 let Ok(entry) = entry else { continue };
86 if let Ok(path) = Utf8PathBuf::from_path_buf(entry.path()) {
87 let Some(ext) = get_multi_extension(&path) else {
88 continue;
89 };
90 if ext.as_str() == STATE_FILE_EXTENSION {
91 return Some(path);
92 }
93 }
94 }
95
96 None
97 }
98
99 #[must_use]
100 pub fn into_translation_type(&self) -> surfer_translation_types::WaveSource {
101 use surfer_translation_types::WaveSource as Ws;
102 match self {
103 WaveSource::File(file) => Ws::File(file.to_string()),
104 WaveSource::Data => Ws::Data,
105 WaveSource::DragAndDrop(file) => {
106 Ws::DragAndDrop(file.as_ref().map(ToString::to_string))
107 }
108 WaveSource::Url(u) => Ws::Url(u.clone()),
109 WaveSource::Cxxrtl(_) => Ws::Cxxrtl,
110 }
111 }
112}
113
114pub fn url_to_wavesource(url: &str) -> Option<WaveSource> {
115 if url.starts_with("https://") || url.starts_with("http://") {
116 info!("Wave source is url");
117 Some(WaveSource::Url(url.to_string()))
118 } else if url.starts_with("cxxrtl+tcp://") {
119 #[cfg(not(target_arch = "wasm32"))]
120 {
121 info!("Wave source is cxxrtl tcp");
122 Some(WaveSource::Cxxrtl(CxxrtlKind::Tcp {
123 url: url.replace("cxxrtl+tcp://", ""),
124 }))
125 }
126 #[cfg(target_arch = "wasm32")]
127 {
128 warn!("Loading waves from cxxrtl via tcp is unsupported in WASM builds.");
129 None
130 }
131 } else {
132 None
133 }
134}
135
136pub fn string_to_wavesource(path: &str) -> WaveSource {
137 if let Some(source) = url_to_wavesource(path) {
138 source
139 } else {
140 info!("Wave source is file");
141 WaveSource::File(path.into())
142 }
143}
144
145impl Display for WaveSource {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 WaveSource::File(file) => write!(f, "{file}"),
149 WaveSource::Data => write!(f, "File data"),
150 WaveSource::DragAndDrop(None) => write!(f, "Dropped file"),
151 WaveSource::DragAndDrop(Some(filename)) => write!(f, "Dropped file ({filename})"),
152 WaveSource::Url(url) => write!(f, "{url}"),
153 WaveSource::Cxxrtl(CxxrtlKind::Tcp { url }) => write!(f, "cxxrtl+tcp://{url}"),
154 WaveSource::Cxxrtl(CxxrtlKind::Mailbox) => write!(f, "cxxrtl mailbox"),
155 }
156 }
157}
158
159#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
160pub enum WaveFormat {
161 Vcd,
162 Fst,
163 Ghw,
164 CxxRtl,
165 Ftr,
166}
167
168impl Display for WaveFormat {
169 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170 match self {
171 WaveFormat::Vcd => write!(f, "VCD"),
172 WaveFormat::Fst => write!(f, "FST"),
173 WaveFormat::Ghw => write!(f, "GHW"),
174 WaveFormat::CxxRtl => write!(f, "CXXRTL"),
175 WaveFormat::Ftr => write!(f, "FTR"),
176 }
177 }
178}
179
180#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
181pub enum LoadOptions {
182 Clear,
183 KeepAvailable,
184 KeepAll,
185}
186
187impl From<(OpenMode, bool)> for LoadOptions {
188 fn from(val: (OpenMode, bool)) -> Self {
189 match val {
190 (OpenMode::Open, _) => LoadOptions::Clear,
191 (OpenMode::Switch, false) => LoadOptions::KeepAvailable,
192 (OpenMode::Switch, true) => LoadOptions::KeepAll,
193 }
194 }
195}
196
197pub struct LoadProgress {
198 pub started: Instant,
199 pub progress: LoadProgressStatus,
200}
201
202impl LoadProgress {
203 #[must_use]
204 pub fn new(progress: LoadProgressStatus) -> Self {
205 LoadProgress {
206 started: Instant::now(),
207 progress,
208 }
209 }
210}
211
212pub enum LoadProgressStatus {
213 Downloading(String),
214 Connecting(String),
215 ReadingHeader(WaveSource),
216 ReadingBody(WaveSource, u64, Arc<AtomicU64>),
217 LoadingVariables(u64),
218}
219
220impl SystemState {
221 pub fn load_from_file(
222 &mut self,
223 filename: Utf8PathBuf,
224 load_options: LoadOptions,
225 ) -> Result<()> {
226 match get_multi_extension(&filename) {
227 Some(ext) => match ext.as_str() {
228 STATE_FILE_EXTENSION => {
229 self.load_state_file(Some(filename.into_std_path_buf()));
230 Ok(())
231 }
232 TRANSACTIONS_FILE_EXTENSION => {
233 self.load_transactions_from_file(filename, load_options)
234 }
235 _ => self.load_wave_from_file(filename, load_options),
236 },
237 _ => self.load_wave_from_file(filename, load_options),
238 }
239 }
240
241 pub fn load_from_bytes(
242 &mut self,
243 source: WaveSource,
244 bytes: Vec<u8>,
245 load_options: LoadOptions,
246 ) {
247 if parse::is_ftr(&mut Cursor::new(&bytes)).is_ok_and(|is_ftr| is_ftr) {
248 self.load_transactions_from_bytes(source, bytes, load_options);
249 } else {
250 self.load_wave_from_bytes(source, bytes, load_options);
251 }
252 }
253
254 pub fn load_wave_from_file(
255 &mut self,
256 filename: Utf8PathBuf,
257 load_options: LoadOptions,
258 ) -> Result<()> {
259 info!("Loading a waveform file: {filename}");
260 let start = web_time::Instant::now();
261 let source = WaveSource::File(filename.clone());
262 let source_copy = source.clone();
263 let sender = self.channels.msg_sender.clone();
264 if !filename.exists() {
265 let msg = Message::Error(anyhow!("Waveform file is missing: {filename}"));
266 checked_send(&sender, msg);
267 return Ok(());
268 }
269 perform_work(move || {
270 let header_result = wellen::viewers::read_header_from_file(
271 filename.as_str(),
272 &WELLEN_SURFER_DEFAULT_OPTIONS,
273 )
274 .map_err(|e| anyhow!("{e:?}"))
275 .with_context(|| format!("Failed to parse wave file: {source}"));
276
277 let msg = match header_result {
278 Ok(header) => Message::WaveHeaderLoaded(
279 start,
280 source,
281 load_options,
282 HeaderResult::LocalFile(Box::new(header)),
283 ),
284 Err(e) => Message::Error(e),
285 };
286 checked_send(&sender, msg);
287 });
288
289 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
290 source_copy,
291 )));
292 Ok(())
293 }
294
295 pub fn load_from_data(&mut self, data: Vec<u8>, load_options: LoadOptions) -> Result<()> {
296 self.load_from_bytes(WaveSource::Data, data, load_options);
297 Ok(())
298 }
299
300 pub fn load_from_dropped(&mut self, file: egui::DroppedFile) -> Result<()> {
301 info!("Got a dropped file");
302
303 let path = file.path.and_then(|x| Utf8PathBuf::try_from(x).ok());
304
305 if let Some(bytes) = file.bytes {
306 if bytes.is_empty() {
307 Err(anyhow!("Dropped an empty file"))
308 } else {
309 if let Some(path) = path.clone() {
310 if get_multi_extension(&path) == Some(STATE_FILE_EXTENSION.to_string()) {
311 let sender = self.channels.msg_sender.clone();
312 perform_async_work(async move {
313 let new_state = match ron::de::from_bytes(&bytes)
314 .context(format!("Failed loading {path}"))
315 {
316 Ok(s) => s,
317 Err(e) => {
318 error!("Failed to load state: {e:#?}");
319 return;
320 }
321 };
322
323 checked_send(
324 &sender,
325 Message::LoadState(new_state, Some(path.into_std_path_buf())),
326 );
327 });
328 } else {
329 self.load_from_bytes(
330 WaveSource::DragAndDrop(Some(path)),
331 bytes.to_vec(),
332 LoadOptions::Clear,
333 );
334 }
335 } else {
336 self.load_from_bytes(
337 WaveSource::DragAndDrop(path),
338 bytes.to_vec(),
339 LoadOptions::Clear,
340 );
341 }
342 Ok(())
343 }
344 } else if let Some(path) = path {
345 self.load_from_file(path, LoadOptions::Clear)
346 } else {
347 Err(anyhow!(
348 "Unknown how to load dropped file w/o path or bytes"
349 ))
350 }
351 }
352
353 pub fn load_wave_from_url(
354 &mut self,
355 url: String,
356 load_options: LoadOptions,
357 force_switch: bool,
358 file_index: Option<usize>,
359 ) {
360 if file_index.is_some() {
361 self.user.selected_server_file_index = file_index;
362 *self.surver_selected_file.borrow_mut() = file_index;
363 }
364
365 match url_to_wavesource(&url) {
366 #[cfg(not(target_arch = "wasm32"))]
369 Some(WaveSource::Cxxrtl(kind)) => {
370 self.connect_to_cxxrtl(kind, load_options != LoadOptions::Clear);
371 }
372 _ => {
375 let sender = self.channels.msg_sender.clone();
376 let url_ = url.clone();
377 info!("Loading wave from url: {url}");
378 perform_async_work(async move {
379 let maybe_response = reqwest::get(&url)
380 .map(|e| e.with_context(|| format!("Failed fetch download {url}")))
381 .await;
382 let response: reqwest::Response = match maybe_response {
383 Ok(r) => r,
384 Err(e) => {
385 checked_send(&sender, Message::Error(e));
386 return;
387 }
388 };
389
390 if let Some(value) = response.headers().get(HTTP_SERVER_KEY)
392 && matches!(value.to_str(), Ok(HTTP_SERVER_VALUE_SURFER))
393 {
394 match load_options {
395 LoadOptions::Clear => {
396 info!("Connecting to a surfer server at: {url}");
397 get_server_status(sender.clone(), url.clone(), 0);
399 if let Some(file_index) = file_index {
401 get_hierarchy_from_server(
402 sender.clone(),
403 url,
404 load_options,
405 file_index,
406 );
407 }
408 }
409 LoadOptions::KeepAvailable | LoadOptions::KeepAll => {
410 if let Some(file_index) = file_index {
412 if force_switch {
413 get_hierarchy_from_server(
414 sender.clone(),
415 url,
416 load_options,
417 file_index,
418 );
419 } else {
420 info!("Reloading from surver instance at: {url}");
421 server_reload(
422 sender.clone(),
423 url,
424 load_options,
425 file_index,
426 );
427 }
428 } else if force_switch {
429 get_server_status(sender.clone(), url.clone(), 0);
431 } else {
432 warn!(
433 "Cannot reload from surver instance without a selected file index"
434 );
435 }
436 }
437 }
438 return;
439 }
440
441 let bytes = response
443 .bytes()
444 .map(|e| e.with_context(|| format!("Failed to download {url}")))
445 .await;
446
447 let msg = match bytes {
448 Ok(b) => Message::FileDownloaded(url, b, load_options),
449 Err(e) => Message::Error(e),
450 };
451 checked_send(&sender, msg);
452 });
453
454 self.progress_tracker =
455 Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
456 }
457 }
458 }
459
460 pub fn load_transactions_from_file(
461 &mut self,
462 filename: camino::Utf8PathBuf,
463 load_options: LoadOptions,
464 ) -> Result<()> {
465 info!("Loading a transaction file: {filename}");
466 let sender = self.channels.msg_sender.clone();
467 let source = WaveSource::File(filename.clone());
468 let format = WaveFormat::Ftr;
469
470 let result = ftr_parser::parse::parse_ftr(filename.into_std_path_buf());
471
472 info!("Done with loading ftr file");
473
474 let msg = match result {
475 Ok(ftr) => Message::TransactionStreamsLoaded(
476 source,
477 format,
478 TransactionContainer { inner: ftr },
479 load_options,
480 ),
481 Err(e) => Message::Error(Report::msg(e)),
482 };
483 checked_send(&sender, msg);
484 Ok(())
485 }
486 pub fn load_transactions_from_bytes(
487 &mut self,
488 source: WaveSource,
489 bytes: Vec<u8>,
490 load_options: LoadOptions,
491 ) {
492 let sender = self.channels.msg_sender.clone();
493
494 let result = parse::parse_ftr_from_bytes(bytes);
495
496 info!("Done with loading ftr file");
497
498 let msg = match result {
499 Ok(ftr) => Message::TransactionStreamsLoaded(
500 source,
501 WaveFormat::Ftr,
502 TransactionContainer { inner: ftr },
503 load_options,
504 ),
505 Err(e) => Message::Error(Report::msg(e)),
506 };
507 checked_send(&sender, msg);
508 }
509
510 pub fn server_status_to_progress(&mut self, server: &str, file_info: &SurverFileInfo) {
512 let body_loaded = self
514 .user
515 .waves
516 .as_ref()
517 .is_some_and(|w| w.inner.body_loaded());
518 if !body_loaded {
519 let source = WaveSource::Url(server.to_string());
521 let sender = self.channels.msg_sender.clone();
522 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
523 source,
524 file_info.bytes,
525 Arc::new(AtomicU64::new(file_info.bytes_loaded)),
526 )));
527 get_server_status(sender, server.to_string(), 250);
529 }
530 }
531
532 pub fn connect_to_cxxrtl(&mut self, kind: CxxrtlKind, keep_variables: bool) {
533 let sender = self.channels.msg_sender.clone();
534
535 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Connecting(format!(
536 "{kind}"
537 ))));
538
539 let task = async move {
540 let container = match &kind {
541 #[cfg(not(target_arch = "wasm32"))]
542 CxxrtlKind::Tcp { url } => {
543 CxxrtlContainer::new_tcp(url, self.channels.msg_sender.clone()).await
544 }
545 #[cfg(target_arch = "wasm32")]
546 CxxrtlKind::Tcp { .. } => {
547 error!("Cxxrtl tcp is not supported om wasm");
548 return;
549 }
550 #[cfg(not(target_arch = "wasm32"))]
551 CxxrtlKind::Mailbox => {
552 error!("CXXRTL mailboxes are only supported on wasm for now");
553 return;
554 }
555 #[cfg(target_arch = "wasm32")]
556 CxxrtlKind::Mailbox => CxxrtlContainer::new_wasm_mailbox(sender.clone()).await,
557 };
558
559 let load_options = if keep_variables {
560 LoadOptions::KeepAvailable
561 } else {
562 LoadOptions::Clear
563 };
564 let msg = match container {
565 Ok(c) => Message::WavesLoaded(
566 WaveSource::Cxxrtl(kind),
567 WaveFormat::CxxRtl,
568 Box::new(WaveContainer::Cxxrtl(Box::new(Mutex::new(c)))),
569 load_options,
570 ),
571 Err(e) => Message::Error(e),
572 };
573 checked_send(&sender, msg);
574 };
575 #[cfg(not(target_arch = "wasm32"))]
576 futures::executor::block_on(task);
577 #[cfg(target_arch = "wasm32")]
578 wasm_bindgen_futures::spawn_local(task);
579 }
580
581 pub fn load_wave_from_bytes(
582 &mut self,
583 source: WaveSource,
584 bytes: Vec<u8>,
585 load_options: LoadOptions,
586 ) {
587 let start = web_time::Instant::now();
588 let sender = self.channels.msg_sender.clone();
589 let source_copy = source.clone();
590 perform_work(move || {
591 let header_result =
592 wellen::viewers::read_header(Cursor::new(bytes), &WELLEN_SURFER_DEFAULT_OPTIONS)
593 .map_err(|e| anyhow!("{e:?}"))
594 .with_context(|| format!("Failed to parse wave file: {source}"));
595
596 let msg = match header_result {
597 Ok(header) => Message::WaveHeaderLoaded(
598 start,
599 source,
600 load_options,
601 HeaderResult::LocalBytes(Box::new(header)),
602 ),
603 Err(e) => Message::Error(e),
604 };
605 checked_send(&sender, msg);
606 });
607
608 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingHeader(
609 source_copy,
610 )));
611 }
612
613 fn get_thread_pool() -> Option<rayon::ThreadPool> {
614 match rayon::ThreadPoolBuilder::new().build() {
617 Ok(pool) => Some(pool),
618 Err(e) => {
619 warn!("failed to create thread pool: {e:?}");
621 None
622 }
623 }
624 }
625
626 pub fn load_wave_body<R: std::io::BufRead + std::io::Seek + Sync + Send + 'static>(
627 &mut self,
628 source: WaveSource,
629 cont: wellen::viewers::ReadBodyContinuation<R>,
630 body_len: u64,
631 hierarchy: Arc<wellen::Hierarchy>,
632 ) {
633 let start = web_time::Instant::now();
634 let sender = self.channels.msg_sender.clone();
635 let source_copy = source.clone();
636 let progress = Arc::new(AtomicU64::new(0));
637 let progress_copy = progress.clone();
638 let pool = Self::get_thread_pool();
639
640 perform_work(move || {
641 let action = || {
642 let p = Some(progress_copy);
643 let body_result = wellen::viewers::read_body(cont, &hierarchy, p)
644 .map_err(|e| anyhow!("{e:?}"))
645 .with_context(|| format!("Failed to parse body of wave file: {source}"));
646
647 let msg = match body_result {
648 Ok(body) => Message::WaveBodyLoaded(start, source, BodyResult::Local(body)),
649 Err(e) => Message::Error(e),
650 };
651 checked_send(&sender, msg);
652 };
653 if let Some(pool) = pool {
654 pool.install(action);
655 } else {
656 action();
657 }
658 });
659
660 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::ReadingBody(
661 source_copy,
662 body_len,
663 progress,
664 )));
665 }
666
667 pub fn load_variables(&mut self, cmd: LoadSignalsCmd) {
668 let (signals, from_unique_id, payload) = cmd.destruct();
669 if signals.is_empty() {
670 return;
671 }
672 let num_signals = signals.len() as u64;
673 let start = web_time::Instant::now();
674 let sender = self.channels.msg_sender.clone();
675 let max_url_length = self.user.config.max_url_length;
676 match payload {
677 LoadSignalPayload::Local(mut source, hierarchy) => {
678 let pool = Self::get_thread_pool();
679
680 perform_work(move || {
681 let action = || {
682 let loaded = source.load_signals(&signals, &hierarchy, true);
683 let res = LoadSignalsResult::local(source, loaded, from_unique_id);
684 checked_send(&sender, Message::SignalsLoaded(start, res));
685 };
686 if let Some(pool) = pool {
687 pool.install(action);
688 } else {
689 action();
690 }
691 });
692 }
693 LoadSignalPayload::Remote(server, file_index) => {
694 perform_async_work(async move {
695 let res =
696 crate::remote::get_signals(
697 server.clone(),
698 &signals,
699 max_url_length,
700 file_index,
701 )
702 .await
703 .map_err(|e| anyhow!("{e:?}"))
704 .with_context(|| {
705 format!(
706 "Failed to retrieve signals from remote server {server} file index {file_index}"
707 )
708 });
709
710 let msg = match res {
711 Ok(loaded) => {
712 let res = LoadSignalsResult::remote(server, loaded, from_unique_id);
713 Message::SignalsLoaded(start, res)
714 }
715 Err(e) => Message::Error(e),
716 };
717 checked_send(&sender, msg);
718 });
719 }
720 }
721
722 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::LoadingVariables(
723 num_signals,
724 )));
725 }
726}
727
728pub fn draw_progress_information(ui: &mut egui::Ui, progress_data: &LoadProgress) {
729 match &progress_data.progress {
730 LoadProgressStatus::Connecting(url) => {
731 ui.horizontal(|ui| {
732 ui.spinner();
733 ui.monospace(format!("Connecting {url}"));
734 });
735 }
736 LoadProgressStatus::Downloading(url) => {
737 ui.horizontal(|ui| {
738 ui.spinner();
739 ui.monospace(format!("Downloading {url}"));
740 });
741 }
742 LoadProgressStatus::ReadingHeader(source) => {
743 ui.spinner();
744 ui.monospace(format!("Loading variable names from {source}"));
745 }
746 LoadProgressStatus::ReadingBody(source, 0, _) => {
747 ui.spinner();
748 ui.monospace(format!("Loading variable change data from {source}"));
749 }
750 LoadProgressStatus::LoadingVariables(num) => {
751 ui.spinner();
752 ui.monospace(format!("Loading {num} variables"));
753 }
754 LoadProgressStatus::ReadingBody(source, total, bytes_done) => {
755 let num_bytes = bytes_done.load(std::sync::atomic::Ordering::SeqCst);
756 let progress = num_bytes as f32 / *total as f32;
757 ui.monospace(format!(
758 "Loading variable change data from {source}. {} / {}",
759 bytesize::ByteSize::b(num_bytes),
760 bytesize::ByteSize::b(*total),
761 ));
762 let progress_bar = egui::ProgressBar::new(progress)
763 .show_percentage()
764 .desired_width(300.);
765 ui.add(progress_bar);
766 }
767 }
768}