flash_font_injector/
lib.rs1use std::collections::HashSet;
2
3use camino::{Utf8Path, Utf8PathBuf};
4use rayon::prelude::*;
5
6use error::{FontError, FontResult};
7use sys::NativeFontRegistry;
8
9pub mod error;
10mod sys;
11
12pub(crate) trait FontRegistry {
13 fn add_font(path: &Utf8Path) -> FontResult<()>;
14 fn remove_font(path: &Utf8Path) -> FontResult<()>;
15}
16
17#[derive(Debug, Default)]
37pub struct FontManager {
38 loaded_fonts: HashSet<Utf8PathBuf>,
39 config: FontManagerConfig,
40}
41
42#[derive(Debug, Clone)]
44pub struct FontManagerConfig {
45 pub keep_loaded_fonts: bool,
48}
49
50impl Default for FontManagerConfig {
51 fn default() -> Self {
52 Self {
53 keep_loaded_fonts: true,
54 }
55 }
56}
57
58impl FontManager {
59 pub fn new(config: FontManagerConfig) -> Self {
61 Self {
62 loaded_fonts: HashSet::new(),
63 config,
64 }
65 }
66
67 pub fn load(&mut self, path: &Utf8Path) -> FontResult<()> {
74 if !self.loaded_fonts.contains(path) {
75 NativeFontRegistry::add_font(path)?;
76 self.loaded_fonts.insert(path.to_path_buf());
77 }
78
79 Ok(())
80 }
81
82 pub fn load_all(&mut self, paths: Vec<Utf8PathBuf>) -> FontResult<()> {
87 let to_load: Vec<_> = paths
88 .into_iter()
89 .filter(|path| !self.loaded_fonts.contains(path))
90 .collect();
91
92 let results: Vec<_> = to_load
93 .into_par_iter()
94 .map(|path| {
95 if NativeFontRegistry::add_font(&path).is_ok() {
96 Ok(path)
97 } else {
98 Err(path)
99 }
100 })
101 .collect();
102
103 let mut first_err = None;
104 for res in results {
105 match res {
106 Ok(path) => {
107 self.loaded_fonts.insert(path);
108 }
109 Err(path) => {
110 if first_err.is_none() {
111 first_err = Some(FontError::LoadFailed(path));
112 }
113 }
114 }
115 }
116
117 if let Some(err) = first_err {
118 return Err(err);
119 }
120
121 Ok(())
122 }
123
124 pub fn unload(&mut self, path: &Utf8Path) -> FontResult<()> {
129 if self.loaded_fonts.remove(path) {
130 NativeFontRegistry::remove_font(path)?;
131 }
132
133 Ok(())
134 }
135
136 pub fn unload_all(&mut self) -> FontResult<()> {
141 let to_unload: Vec<_> = self.loaded_fonts.drain().collect();
142
143 let errs: Vec<_> = to_unload
144 .into_par_iter()
145 .filter(|path| NativeFontRegistry::remove_font(path).is_err())
146 .collect();
147
148 if !errs.is_empty() {
149 return Err(FontError::UnloadFailed(errs[0].clone()));
150 }
151
152 Ok(())
153 }
154
155 pub fn loaded_fonts(&self) -> impl Iterator<Item = &Utf8Path> {
157 self.loaded_fonts.iter().map(|p| p.as_path())
158 }
159
160 pub fn len(&self) -> usize {
162 self.loaded_fonts.len()
163 }
164
165 pub fn is_empty(&self) -> bool {
167 self.loaded_fonts.is_empty()
168 }
169}
170
171impl Drop for FontManager {
172 fn drop(&mut self) {
173 if !self.config.keep_loaded_fonts {
174 let _ = self.unload_all();
175 }
176 }
177}
178
179#[cfg(all(test, not(ci)))]
180mod tests {
181
182 use super::*;
183
184 #[test]
185 fn test_font_manager() {
186 let font_path = Utf8Path::new("../fonts/方正少儿_GBK.ttf");
187
188 let mut manager = FontManager::new(FontManagerConfig {
189 keep_loaded_fonts: false,
190 });
191
192 manager.load(font_path).unwrap();
193 assert!(manager.loaded_fonts.contains(font_path));
194 assert_eq!(manager.len(), 1);
195
196 manager.load(font_path).unwrap();
198 assert_eq!(manager.len(), 1);
199
200 manager.unload(font_path).unwrap();
201 assert!(!manager.loaded_fonts.contains(font_path));
202 assert!(manager.is_empty());
203 }
204}