libsurfer/
wave_source.rs

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            // We want to support opening cxxrtl urls using open url and friends,
340            // so we'll special case
341            #[cfg(not(target_arch = "wasm32"))]
342            Some(WaveSource::Cxxrtl(kind)) => {
343                self.connect_to_cxxrtl(kind, load_options.keep_variables);
344            }
345            // However, if we don't get a cxxrtl url, we want to continue loading this as
346            // a url even if it isn't auto detected as a url.
347            _ => {
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                    // check to see if the response came from a Surfer running in server mode
363                    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                            // request status and hierarchy
367                            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                    // otherwise we load the body to get at the file
378                    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    /// uses the server status in order to display a loading bar
521    pub fn server_status_to_progress(&mut self, server: String, status: Status) {
522        // once the body is loaded, we are no longer interested in the status
523        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            // the progress tracker will be cleared once the hierarchy is returned from the server
530            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            // get another status update
538            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        // try to create a new rayon thread pool so that we do not block drawing functionality
625        // which might be blocked by the waveform reader using up all the threads in the global pool
626        match rayon::ThreadPoolBuilder::new().build() {
627            Ok(pool) => Some(pool),
628            Err(e) => {
629                // on wasm this will always fail
630                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}