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