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_name_provider: Default::default(),
32            theme_cache: Default::default(),
33            icon_cache: Default::default(),
34            search_paths: 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 !self.icon_cache.contains_key(icon_name) {
67            let mut searched_themes = Vec::new();
68
69            let icon = self
70                .find_icon(self.theme_name(), icon_name, &mut searched_themes)
71                .or_else(|| {
72                    self.find_icon(self.fallback_theme_name(), icon_name, &mut searched_themes)
73                })
74                .map(Arc::new);
75
76            self.icon_cache.insert(icon_name.into(), icon);
77        }
78
79        // Unwrapping is ok, since we just added a value
80        self.icon_cache.get(icon_name).unwrap().value().clone()
81    }
82
83    /// Returns the currently used theme name.
84    ///
85    /// See also [`IconLoader::update_theme_name()`].
86    pub fn theme_name(&self) -> &str {
87        &self.theme_name
88    }
89
90    /// Returns the currently used fallback theme name.
91    ///
92    /// See also [`IconLoader::set_fallback_theme_name()`].
93    pub fn fallback_theme_name(&self) -> &str {
94        &self.fallback_theme_name
95    }
96
97    /// Returns the paths that are searched for icon themes.
98    pub fn search_paths(&self) -> Cow<[PathBuf]> {
99        self.search_paths.paths()
100    }
101
102    /// Sets the paths where to search for icon themes.
103    ///
104    /// # Arguments
105    ///
106    /// * `search_paths` - The paths where to look for icon themes.
107    /// Anything that implements `IntoIterator<Item = Into<PathBuf>>` can be used.
108    ///
109    /// # Examples
110    ///
111    /// Custom search paths:
112    /// ```
113    /// use icon_loader::IconLoader;
114    ///
115    /// let mut loader = IconLoader::new();
116    /// loader.set_search_paths(&["/path/to/icon/themes", "/other/path/to/icon/themes"]);
117    /// ```
118    ///
119    /// System search paths:
120    /// ```
121    /// use icon_loader::{IconLoader, SearchPaths};
122    ///
123    /// let mut loader = IconLoader::new();
124    /// loader.set_search_paths(SearchPaths::System);
125    /// ```
126    ///
127    /// By default these are the system icon paths.
128    pub fn set_search_paths(&mut self, search_paths: impl Into<SearchPaths>) {
129        let search_paths = search_paths.into();
130
131        if self.search_paths == search_paths {
132            return;
133        }
134
135        self.search_paths = search_paths;
136        self.theme_cache.clear();
137        self.icon_cache.clear();
138    }
139
140    /// Sets the way in which the used theme name is determined.
141    ///
142    /// # Arguments
143    ///
144    /// * `theme_name_provider` - The provider of the default icon theme name.
145    /// Anything that implements `Into<ThemeNameProvider>` can be used.
146    ///
147    /// # Examples
148    ///
149    /// User defined theme name:
150    /// ```
151    /// use icon_loader::IconLoader;
152    ///
153    /// let mut loader = IconLoader::new();
154    /// loader.set_theme_name_provider("theme_name");
155    /// ```
156    ///
157    /// KDE system theme:
158    /// ```
159    /// use icon_loader::{IconLoader, ThemeNameProvider};
160    ///
161    /// let mut loader = IconLoader::new();
162    /// loader.set_theme_name_provider(ThemeNameProvider::KDE);
163    /// ```
164    ///
165    /// [`IconLoader::update_theme_name()`] needs to be called after setting a new theme name provider.
166    pub fn set_theme_name_provider(&mut self, theme_name_provider: impl Into<ThemeNameProvider>) {
167        let theme_name_provider = theme_name_provider.into();
168
169        if self.theme_name_provider == theme_name_provider {
170            return;
171        }
172
173        self.theme_name_provider = theme_name_provider;
174    }
175
176    /// Queries the set [`ThemeNameProvider`](ThemeNameProvider) for the theme name to be used.
177    /// Returns an error, if the set [`ThemeNameProvider`](ThemeNameProvider) returns an error or the theme with the returned name cannot be found.
178    ///
179    /// Set a theme name provider with [`IconLoader::set_theme_name_provider()`].
180    pub fn update_theme_name(&mut self) -> Result<()> {
181        let theme_name = self.theme_name_provider.theme_name()?;
182
183        if self.theme_name == theme_name {
184            return Ok(());
185        }
186
187        if self.theme_exists(&theme_name) {
188            self.theme_name = theme_name;
189            self.icon_cache.clear();
190
191            Ok(())
192        } else {
193            Err(Error::theme_not_found(theme_name))
194        }
195    }
196
197    /// Sets a new fallback theme name. If an icon cannot be found in the set theme,
198    /// it will be looked for in the fallback theme.
199    /// The default fallback theme name is 'hicolor'.
200    pub fn set_fallback_theme_name(&mut self, fallback_theme_name: impl Into<String>) {
201        let fallback_theme_name = fallback_theme_name.into();
202
203        if self.fallback_theme_name == fallback_theme_name {
204            return;
205        }
206
207        self.fallback_theme_name = fallback_theme_name;
208        self.icon_cache.clear();
209    }
210
211    /// Returns whether a theme with the name `theme_name` exists in the current search paths.
212    pub fn theme_exists(&self, theme_name: impl AsRef<str>) -> bool {
213        let theme_name = theme_name.as_ref();
214
215        if theme_name.is_empty() {
216            return false;
217        }
218
219        !self.load_themes(theme_name).is_empty()
220    }
221
222    fn load_themes<'a>(&'a self, theme_name: &'a str) -> impl Deref<Target = IconThemes> + 'a {
223        if !self.theme_cache.contains_key(theme_name) {
224            let new_themes = IconThemes::find(theme_name, &self.search_paths());
225
226            self.theme_cache.insert(theme_name.into(), new_themes);
227        }
228
229        // Unwrapping is ok, since we just added a value
230        self.theme_cache.get(theme_name).unwrap()
231    }
232
233    fn find_icon(
234        &self,
235        theme_name: &str,
236        icon_name: &str,
237        searched_themes: &mut Vec<String>,
238    ) -> Option<Icon> {
239        if theme_name.is_empty() || icon_name.is_empty() {
240            return None;
241        }
242
243        searched_themes.push(theme_name.into());
244
245        let themes = self.load_themes(theme_name);
246
247        if let Some(icon) = themes.find_icon(icon_name) {
248            return Some(icon);
249        }
250
251        for parent in themes.parents() {
252            if !searched_themes.contains(parent) {
253                if let Some(icon) = self.find_icon(parent, icon_name, searched_themes) {
254                    return Some(icon);
255                }
256            }
257        }
258
259        None
260    }
261}
262
263impl Default for IconLoader {
264    fn default() -> Self {
265        IconLoader::new()
266    }
267}