Skip to main content

libsurfer/
wave_source.rs

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            // We want to support opening cxxrtl urls using open url and friends,
360            // so we'll special case
361            #[cfg(not(target_arch = "wasm32"))]
362            Some(WaveSource::Cxxrtl(kind)) => {
363                self.connect_to_cxxrtl(kind, load_options != LoadOptions::Clear);
364            }
365            // However, if we don't get a cxxrtl url, we want to continue loading this as
366            // a url even if it isn't auto detected as a url.
367            _ => {
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                    // check to see if the response came from a Surfer running in server mode
387                    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                                // Request status
394                                get_server_status(sender.clone(), url.clone(), 0);
395                                // Request hierarchy
396                                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                                // Request a reload (will also get status and request hierarchy if needed)
407                                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                                    // We started Surfer with a Surver URL as argument, so request status
426                                    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                    // otherwise we load the body to get at the file
438                    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    /// uses the server status in order to display a loading bar
513    pub fn server_status_to_progress(&mut self, server: &str, file_info: &SurverFileInfo) {
514        // once the body is loaded, we are no longer interested in the status
515        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            // the progress tracker will be cleared once the hierarchy is returned from the server
522            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 another status update
530            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        // try to create a new rayon thread pool so that we do not block drawing functionality
621        // which might be blocked by the waveform reader using up all the threads in the global pool
622        match rayon::ThreadPoolBuilder::new().build() {
623            Ok(pool) => Some(pool),
624            Err(e) => {
625                // on wasm this will always fail
626                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}