icon_loader/
loader.rs

1use std::{borrow::Cow, ops::Deref, path::PathBuf, sync::Arc};
2
3use crate::{
4    error::{Error, Result},
5    icon::{Icon, IconThemes},
6    search_paths::SearchPaths,
7    theme_name_provider::ThemeNameProvider,
8};
9
10use dashmap::DashMap;
11
12/// The central icon loader struct.
13///
14/// It lets you load and cache named theme icons from system themes as well as custom themes.
15#[derive(Debug)]
16pub struct IconLoader {
17    theme_name: String,
18    fallback_theme_name: String,
19    theme_cache: DashMap<String, IconThemes>,
20    icon_cache: DashMap<String, Option<Arc<Icon>>>,
21    search_paths: SearchPaths,
22    theme_name_provider: ThemeNameProvider,
23}
24
25impl IconLoader {
26    /// Creates a new `IconLoader` with default settings.
27    pub fn new() -> Self {
28        IconLoader {
29            theme_name: String::from("hicolor"),
30            fallback_theme_name: String::from("hicolor"),
31            theme_cache: DashMap::new(),
32            icon_cache: DashMap::new(),
33            search_paths: Default::default(),
34            theme_name_provider: Default::default(),
35        }
36    }
37
38    /// Creates a new KDE `IconLoader`.
39    /// This is a convenience function.
40    #[cfg(feature = "kde")]
41    pub fn new_kde() -> Result<Self> {
42        let mut loader = Self::new();
43        loader.set_theme_name_provider(ThemeNameProvider::KDE);
44        loader.update_theme_name()?;
45
46        Ok(loader)
47    }
48
49    /// Creates a new GTK `IconLoader`.
50    /// This is a convenience function.
51    #[cfg(feature = "gtk")]
52    pub fn new_gtk() -> Result<Self> {
53        let mut loader = Self::new();
54        loader.set_theme_name_provider(ThemeNameProvider::GTK);
55        loader.update_theme_name()?;
56
57        Ok(loader)
58    }
59
60    /// Loads the icon with the name `icon_name` from the current icon theme.
61    /// If the icon cannot be found, it will be looked for in the fallback icon theme.
62    /// If it cannot be found in the fallback theme, `None` is returned.
63    pub fn load_icon(&self, icon_name: impl AsRef<str>) -> Option<Arc<Icon>> {
64        let icon_name = icon_name.as_ref();
65
66        if let Some(icon) = self.icon_cache.get(icon_name) {
67            return icon.clone();
68        }
69
70        let mut searched_themes = Vec::new();
71
72        let icon = self
73            .find_icon(self.theme_name(), icon_name, &mut searched_themes)
74            .or_else(|| self.find_icon(self.fallback_theme_name(), icon_name, &mut searched_themes))
75            .map(Arc::new);
76
77        self.icon_cache.insert(icon_name.into(), icon.clone());
78
79        icon
80    }
81
82    /// Returns the currently used theme name.
83    ///
84    /// See also [`IconLoader::update_theme_name()`].
85    pub fn theme_name(&self) -> &str {
86        &self.theme_name
87    }
88
89    /// Returns the currently used fallback theme name.
90    ///
91    /// See also [`IconLoader::set_fallback_theme_name()`].
92    pub fn fallback_theme_name(&self) -> &str {
93        &self.fallback_theme_name
94    }
95
96    /// Returns the paths that are searched for icon themes.
97    pub fn search_paths(&self) -> Cow<[PathBuf]> {
98        self.search_paths.paths()
99    }
100
101    /// Sets the paths where to search for icon themes.
102    ///
103    /// # Arguments
104    ///
105    /// * `search_paths` - The paths where to look for icon themes.
106    /// Anything that implements `IntoIterator<Item = Into<PathBuf>>` can be used.
107    ///
108    /// # Examples
109    ///
110    /// Custom search paths:
111    /// ```
112    /// use icon_loader::IconLoader;
113    ///
114    /// let mut loader = IconLoader::new();
115    /// loader.set_search_paths(&["/path/to/icon/themes", "/other/path/to/icon/themes"]);
116    /// ```
117    ///
118    /// System search paths:
119    /// ```
120    /// use icon_loader::{IconLoader, SearchPaths};
121    ///
122    /// let mut loader = IconLoader::new();
123    /// loader.set_search_paths(SearchPaths::System);
124    /// ```
125    ///
126    /// By default these are the system icon paths.
127    pub fn set_search_paths(&mut self, search_paths: impl Into<SearchPaths>) {
128        let search_paths = search_paths.into();
129
130        if self.search_paths == search_paths {
131            return;
132        }
133
134        self.search_paths = search_paths;
135        self.theme_cache.clear();
136        self.icon_cache.clear();
137    }
138
139    /// Sets the way in which the used theme name is determined.
140    ///
141    /// # Arguments
142    ///
143    /// * `theme_name_provider` - The provider of the default icon theme name.
144    /// Anything that implements `Into<ThemeNameProvider>` can be used.
145    ///
146    /// # Examples
147    ///
148    /// User defined theme name:
149    /// ```
150    /// use icon_loader::IconLoader;
151    ///
152    /// let mut loader = IconLoader::new();
153    /// loader.set_theme_name_provider("theme_name");
154    /// ```
155    ///
156    /// KDE system theme:
157    /// ```
158    /// use icon_loader::{IconLoader, ThemeNameProvider};
159    ///
160    /// let mut loader = IconLoader::new();
161    /// loader.set_theme_name_provider(ThemeNameProvider::KDE);
162    /// ```
163    ///
164    /// [`IconLoader::update_theme_name()`] needs to be called after setting a new theme name provider.
165    pub fn set_theme_name_provider(&mut self, theme_name_provider: impl Into<ThemeNameProvider>) {
166        let theme_name_provider = theme_name_provider.into();
167
168        if self.theme_name_provider == theme_name_provider {
169            return;
170        }
171
172        self.theme_name_provider = theme_name_provider;
173    }
174
175    /// Queries the set [`ThemeNameProvider`](ThemeNameProvider) for the theme name to be used.
176    /// Returns an error, if the set [`ThemeNameProvider`](ThemeNameProvider) returns an error or the theme with the returned name cannot be found.
177    ///
178    /// Set a theme name provider with [`IconLoader::set_theme_name_provider()`].
179    pub fn update_theme_name(&mut self) -> Result<()> {
180        let theme_name = self.theme_name_provider.theme_name()?;
181
182        if self.theme_name == theme_name {
183            return Ok(());
184        }
185
186        if self.theme_exists(&theme_name) {
187            self.theme_name = theme_name;
188            self.icon_cache.clear();
189
190            Ok(())
191        } else {
192            Err(Error::theme_not_found(theme_name))
193        }
194    }
195
196    /// Sets a new fallback theme name. If an icon cannot be found in the set theme,
197    /// it will be looked for in the fallback theme.
198    /// The default fallback theme name is 'hicolor'.
199    pub fn set_fallback_theme_name(&mut self, fallback_theme_name: impl Into<String>) {
200        let fallback_theme_name = fallback_theme_name.into();
201
202        if self.fallback_theme_name == fallback_theme_name {
203            return;
204        }
205
206        self.fallback_theme_name = fallback_theme_name;
207        self.icon_cache.clear();
208    }
209
210    /// Returns whether a theme with the name `theme_name` exists in the current search paths.
211    pub fn theme_exists(&self, theme_name: impl AsRef<str>) -> bool {
212        let theme_name = theme_name.as_ref();
213
214        if theme_name.is_empty() {
215            return false;
216        }
217
218        !self.load_themes(theme_name).is_empty()
219    }
220
221    fn load_themes<'a>(
222        &'a self,
223        theme_name: impl AsRef<str> + Into<String>,
224    ) -> impl Deref<Target = IconThemes> + 'a {
225        let theme_name_ref = theme_name.as_ref();
226
227        if let Some(themes) = self.theme_cache.get(theme_name_ref) {
228            return themes;
229        }
230
231        let new_themes = IconThemes::find(theme_name_ref, &self.search_paths());
232
233        self.theme_cache
234            .entry(theme_name.into())
235            .insert(new_themes)
236            .downgrade()
237    }
238
239    fn find_icon(
240        &self,
241        theme_name: impl AsRef<str> + Into<String>,
242        icon_name: &str,
243        searched_themes: &mut Vec<String>,
244    ) -> Option<Icon> {
245        let theme_name_ref = theme_name.as_ref();
246
247        if theme_name_ref.is_empty() || icon_name.is_empty() {
248            return None;
249        }
250
251        let themes = self.load_themes(theme_name_ref);
252
253        if let Some(icon) = themes.find_icon(icon_name) {
254            return Some(icon);
255        }
256
257        // Prevent deadlock
258        let parents = themes.parents().to_vec();
259        drop(themes);
260
261        searched_themes.push(theme_name.into());
262
263        for parent in parents {
264            if !searched_themes.contains(&parent) {
265                if let Some(icon) = self.find_icon(parent, icon_name, searched_themes) {
266                    return Some(icon);
267                }
268            }
269        }
270
271        None
272    }
273}
274
275impl Default for IconLoader {
276    fn default() -> Self {
277        IconLoader::new()
278    }
279}