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