egui_extras/loaders/
file_loader.rs

1use ahash::HashMap;
2use egui::{
3    load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError},
4    mutex::Mutex,
5};
6use std::{sync::Arc, task::Poll, thread};
7
8#[derive(Clone)]
9struct File {
10    bytes: Arc<[u8]>,
11    mime: Option<String>,
12}
13
14type Entry = Poll<Result<File, String>>;
15
16#[derive(Default)]
17pub struct FileLoader {
18    /// Cache for loaded files
19    cache: Arc<Mutex<HashMap<String, Entry>>>,
20}
21
22impl FileLoader {
23    pub const ID: &'static str = egui::generate_loader_id!(FileLoader);
24}
25
26const PROTOCOL: &str = "file://";
27
28/// Remove the leading slash from the path if the target OS is Windows.
29///
30/// This is because Windows paths are not supposed to start with a slash.
31/// For example, `file:///C:/path/to/file` is a valid URI, but `/C:/path/to/file` is not a valid path.
32#[inline]
33fn trim_extra_slash(s: &str) -> &str {
34    if cfg!(target_os = "windows") {
35        s.trim_start_matches('/')
36    } else {
37        s
38    }
39}
40
41impl BytesLoader for FileLoader {
42    fn id(&self) -> &str {
43        Self::ID
44    }
45
46    fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
47        // File loader only supports the `file` protocol.
48        let Some(path) = uri.strip_prefix(PROTOCOL).map(trim_extra_slash) else {
49            return Err(LoadError::NotSupported);
50        };
51
52        let mut cache = self.cache.lock();
53        if let Some(entry) = cache.get(uri).cloned() {
54            // `path` has either begun loading, is loaded, or has failed to load.
55            match entry {
56                Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
57                    size: None,
58                    bytes: Bytes::Shared(file.bytes),
59                    mime: file.mime,
60                }),
61                Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
62                Poll::Pending => Ok(BytesPoll::Pending { size: None }),
63            }
64        } else {
65            log::trace!("started loading {uri:?}");
66            // We need to load the file at `path`.
67
68            // Set the file to `pending` until we finish loading it.
69            let path = path.to_owned();
70            cache.insert(uri.to_owned(), Poll::Pending);
71            drop(cache);
72
73            // Spawn a thread to read the file, so that we don't block the render for too long.
74            thread::Builder::new()
75                .name(format!("egui_extras::FileLoader::load({uri:?})"))
76                .spawn({
77                    let ctx = ctx.clone();
78                    let cache = self.cache.clone();
79                    let uri = uri.to_owned();
80                    move || {
81                        let result = match std::fs::read(&path) {
82                            Ok(bytes) => {
83                                #[cfg(feature = "file")]
84                                let mime = mime_guess2::from_path(&path)
85                                    .first_raw()
86                                    .map(|v| v.to_owned());
87
88                                #[cfg(not(feature = "file"))]
89                                let mime = None;
90
91                                Ok(File {
92                                    bytes: bytes.into(),
93                                    mime,
94                                })
95                            }
96                            Err(err) => Err(err.to_string()),
97                        };
98                        let prev = cache.lock().insert(uri.clone(), Poll::Ready(result));
99                        assert!(matches!(prev, Some(Poll::Pending)));
100                        ctx.request_repaint();
101                        log::trace!("finished loading {uri:?}");
102                    }
103                })
104                .expect("failed to spawn thread");
105
106            Ok(BytesPoll::Pending { size: None })
107        }
108    }
109
110    fn forget(&self, uri: &str) {
111        let _ = self.cache.lock().remove(uri);
112    }
113
114    fn forget_all(&self) {
115        self.cache.lock().clear();
116    }
117
118    fn byte_size(&self) -> usize {
119        self.cache
120            .lock()
121            .values()
122            .map(|entry| match entry {
123                Poll::Ready(Ok(file)) => {
124                    file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
125                }
126                Poll::Ready(Err(err)) => err.len(),
127                _ => 0,
128            })
129            .sum()
130    }
131}