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