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