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: 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#[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 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 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 let path = path.to_owned();
70 cache.insert(uri.to_owned(), Poll::Pending);
71 drop(cache);
72
73 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}