luminol_filesystem/
egui_bytes_loader.rs

1// Copyright (C) 2024 Melody Madeline Lyons
2//
3// This file is part of Luminol.
4//
5// Luminol is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Luminol is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Luminol.  If not, see <http://www.gnu.org/licenses/>.
17//
18//     Additional permission under GNU GPL version 3 section 7
19//
20// If you modify this Program, or any covered work, by linking or combining
21// it with Steamworks API by Valve Corporation, containing parts covered by
22// terms of the Steamworks API by Valve Corporation, the licensors of this
23// Program grant you additional permission to convey the resulting work.
24
25use std::sync::Arc;
26
27use dashmap::{DashMap, DashSet};
28use egui::load::{Bytes, BytesPoll, LoadError};
29
30#[derive(Default)]
31pub struct Loader {
32    loaded_files: DashMap<camino::Utf8PathBuf, Arc<[u8]>>,
33    errored_files: DashMap<camino::Utf8PathBuf, color_eyre::Report>,
34    unloaded_files: DashSet<camino::Utf8PathBuf>,
35}
36
37pub const BYTES_LOADER_ID: &str = egui::load::generate_loader_id!(BytesLoader);
38
39pub const PROTOCOL: &str = "project://";
40
41fn supported_uri_to_path(uri: &str) -> Option<&camino::Utf8Path> {
42    uri.strip_prefix(PROTOCOL).map(camino::Utf8Path::new)
43}
44
45impl Loader {
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    pub fn load_unloaded_files(&self, ctx: &egui::Context, filesystem: &impl crate::FileSystem) {
51        // dashmap has no drain iterator unfortunately so this is the best we can do
52        for path in self.unloaded_files.iter() {
53            let path = path.to_path_buf();
54            match filesystem.read(&path) {
55                Ok(bytes) => {
56                    self.loaded_files.insert(path, bytes.into());
57                }
58                Err(err) => {
59                    self.errored_files.insert(path, err);
60                }
61            };
62        }
63        if !self.unloaded_files.is_empty() {
64            ctx.request_repaint();
65        }
66        self.unloaded_files.clear();
67    }
68}
69
70impl egui::load::BytesLoader for Loader {
71    fn id(&self) -> &str {
72        BYTES_LOADER_ID
73    }
74
75    fn load(&self, _: &egui::Context, uri: &str) -> egui::load::BytesLoadResult {
76        let Some(path) = supported_uri_to_path(uri) else {
77            return Err(LoadError::NotSupported);
78        };
79
80        if let Some(bytes) = self.loaded_files.get(path) {
81            return Ok(BytesPoll::Ready {
82                size: None,
83                bytes: Bytes::Shared(bytes.clone()),
84                mime: None,
85            });
86        }
87
88        if let Some(error) = self.errored_files.get(path) {
89            return Err(LoadError::Loading(error.to_string()));
90        }
91
92        self.unloaded_files.insert(path.to_path_buf());
93        Ok(BytesPoll::Pending { size: None })
94    }
95
96    fn forget(&self, uri: &str) {
97        let Some(path) = supported_uri_to_path(uri) else {
98            return;
99        };
100
101        self.loaded_files.remove(path);
102        self.errored_files.remove(path);
103        self.unloaded_files.remove(path);
104    }
105
106    fn forget_all(&self) {
107        self.loaded_files.clear();
108        self.errored_files.clear();
109        self.unloaded_files.clear();
110    }
111
112    fn byte_size(&self) -> usize {
113        self.loaded_files.iter().map(|e| e.len()).sum()
114    }
115}