Skip to main content

cosmic/widget/icon/
named.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use super::{Handle, Icon};
5use std::borrow::Cow;
6use std::ffi::OsStr;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Default, Hash)]
11/// Fallback icon to use if the icon was not found.
12pub enum IconFallback {
13    #[default]
14    /// Default fallback using the icon name.
15    Default,
16    /// Fallback to specific icon names.
17    Names(Vec<Cow<'static, str>>),
18}
19
20#[must_use]
21#[derive(derive_setters::Setters, Clone, Debug, Hash)]
22pub struct Named {
23    /// Name of icon to locate in an XDG icon path.
24    pub(super) name: Arc<str>,
25
26    /// Checks for a fallback if the icon was not found.
27    pub fallback: Option<IconFallback>,
28
29    /// Restrict the lookup to a given scale.
30    #[setters(strip_option)]
31    pub scale: Option<u16>,
32
33    /// Restrict the lookup to a given size.
34    #[setters(strip_option)]
35    pub size: Option<u16>,
36
37    /// Whether the icon is symbolic or not.
38    pub symbolic: bool,
39
40    /// Prioritizes SVG over PNG
41    pub prefer_svg: bool,
42
43    /// Extra directories to search as flat paths before the icon theme chain.
44    #[setters(skip)]
45    pub extra_paths: Vec<PathBuf>,
46}
47
48impl Named {
49    pub fn new(name: impl Into<Arc<str>>) -> Self {
50        let name = name.into();
51        let symbolic = name.ends_with("-symbolic");
52        Self {
53            symbolic,
54            name,
55            fallback: Some(IconFallback::Default),
56            size: None,
57            scale: None,
58            prefer_svg: symbolic,
59            extra_paths: Vec::new(),
60        }
61    }
62
63    pub fn with_extra_paths(mut self, paths: Vec<PathBuf>) -> Self {
64        self.extra_paths = paths;
65        self
66    }
67
68    #[cfg(all(unix, not(target_os = "macos")))]
69    #[must_use]
70    pub fn path(self) -> Option<PathBuf> {
71        let name = &*self.name;
72        let fallback = &self.fallback;
73        let extra_paths = &self.extra_paths;
74        let locate = |theme: &str, name| {
75            let mut lookup = freedesktop_icons::lookup(name)
76                .with_theme(theme.as_ref())
77                .with_cache();
78
79            if !extra_paths.is_empty() {
80                lookup = lookup.with_extra_paths(extra_paths);
81            }
82
83            if let Some(scale) = self.scale {
84                lookup = lookup.with_scale(scale);
85            }
86
87            if let Some(size) = self.size {
88                lookup = lookup.with_size(size);
89            }
90
91            if self.prefer_svg {
92                lookup = lookup.force_svg();
93            }
94            lookup.find()
95        };
96
97        let theme = crate::icon_theme::DEFAULT.lock().unwrap();
98        let themes = if theme.as_ref() == crate::icon_theme::COSMIC {
99            vec![theme.as_ref()]
100        } else {
101            vec![theme.as_ref(), crate::icon_theme::COSMIC]
102        };
103
104        let mut result = themes.iter().find_map(|t| locate(t, name));
105
106        // On failure, attempt to locate fallback icon.
107        if result.is_none() {
108            if matches!(fallback, Some(IconFallback::Default)) {
109                for new_name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) {
110                    result = themes.iter().find_map(|t| locate(t, new_name));
111                    if result.is_some() {
112                        break;
113                    }
114                }
115            } else if let Some(IconFallback::Names(fallbacks)) = fallback {
116                for fallback in fallbacks {
117                    result = themes.iter().find_map(|t| locate(t, fallback));
118                    if result.is_some() {
119                        break;
120                    }
121                }
122            }
123        }
124
125        result
126    }
127
128    #[cfg(any(not(unix), target_os = "macos"))]
129    #[must_use]
130    pub fn path(self) -> Option<PathBuf> {
131        //TODO: implement icon lookup for Windows
132        None
133    }
134
135    #[inline]
136    pub fn handle(self) -> Handle {
137        let name = self.name.clone();
138        Handle {
139            symbolic: self.symbolic,
140            data: if let Some(path) = self.path() {
141                if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
142                    super::Data::Svg(iced_core::svg::Handle::from_path(path))
143                } else {
144                    super::Data::Image(iced_core::image::Handle::from_path(path))
145                }
146            } else {
147                super::bundle::get(&name).unwrap_or_else(|| {
148                    let bytes: &'static [u8] = &[];
149                    super::Data::Svg(iced_core::svg::Handle::from_memory(bytes))
150                })
151            },
152        }
153    }
154
155    #[inline]
156    pub fn icon(self) -> Icon {
157        let size = self.size;
158
159        let icon = super::icon(self.handle());
160
161        match size {
162            Some(size) => icon.size(size),
163            None => icon,
164        }
165    }
166}
167
168impl From<Named> for Handle {
169    #[inline]
170    fn from(builder: Named) -> Self {
171        builder.handle()
172    }
173}
174
175impl From<Named> for Icon {
176    #[inline]
177    fn from(builder: Named) -> Self {
178        builder.icon()
179    }
180}
181
182impl<Message: 'static> From<Named> for crate::Element<'_, Message> {
183    #[inline]
184    fn from(builder: Named) -> Self {
185        builder.icon().into()
186    }
187}