// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 //! Lazily-generated SVG icon widget for Iced. use crate::{Element, Renderer}; use derive_setters::Setters; use iced::{ widget::{svg, Image}, ContentFit, Length, }; use std::{hash::Hash, path::PathBuf}; use std::{ borrow::Cow, collections::hash_map::DefaultHasher, ffi::OsStr, hash::Hasher, path::Path, }; #[derive(Debug, Hash)] pub enum IconSource<'a> { Path(Cow<'a, Path>), Name(Cow<'a, str>), Embedded(Image), } impl<'a> From> for IconSource<'a> { fn from(value: Cow<'a, Path>) -> Self { Self::Path(value) } } impl From for IconSource<'static> { fn from(value: PathBuf) -> Self { Self::Path(Cow::Owned(value)) } } impl<'a> From<&'a Path> for IconSource<'a> { fn from(value: &'a Path) -> Self { Self::Path(Cow::Borrowed(value)) } } impl<'a> From> for IconSource<'a> { fn from(value: Cow<'a, str>) -> Self { Self::Name(value) } } impl From for IconSource<'static> { fn from(value: String) -> Self { Self::Name(value.into()) } } impl<'a> From<&'a str> for IconSource<'a> { fn from(value: &'a str) -> Self { Self::Name(value.into()) } } impl<'a> From for IconSource<'a> { fn from(value: Image) -> Self { Self::Embedded(value) } } /// A lazily-generated icon. #[derive(Hash, Setters)] pub struct Icon<'a> { #[setters(skip)] name: IconSource<'a>, #[setters(into)] theme: Cow<'a, str>, style: crate::theme::Svg, size: u16, #[setters(strip_option)] content_fit: Option, #[setters(strip_option)] width: Option, #[setters(strip_option)] height: Option, force_svg: bool, } /// A lazily-generated icon. #[must_use] pub fn icon<'a>(name: impl Into>, size: u16) -> Icon<'a> { Icon { content_fit: None, height: None, name: name.into(), size, style: crate::theme::Svg::default(), theme: Cow::Borrowed("Pop"), width: None, force_svg: false, } } impl<'a> Icon<'a> { #[must_use] fn into_element(self) -> Element<'a, Message> { if let IconSource::Embedded(mut image) = self.name { image = image .width(self.width.unwrap_or(Length::Units(self.size))) .height(self.height.unwrap_or(Length::Units(self.size))); if let Some(content_fit) = self.content_fit { image = image.content_fit(content_fit); } return image.into(); } let mut hasher = DefaultHasher::new(); self.hash(&mut hasher); iced_lazy::lazy(hasher.finish(), move || -> Element { let name_path_buffer: Option; let icon: Option<&Path> = match &self.name { IconSource::Path(path) => Some(path), IconSource::Name(name) => { let icon = freedesktop_icons::lookup(name) .with_size(self.size) .with_theme(&self.theme) .with_cache() .find(); name_path_buffer = if icon.is_none() { freedesktop_icons::lookup(name) .with_size(self.size) .with_cache() .find() } else { icon }; name_path_buffer.as_deref() } IconSource::Embedded(_) => unimplemented!(), }; let is_svg = self.force_svg || icon .as_ref() .map_or(true, |path| path.extension() == Some(OsStr::new("svg"))); if is_svg { let handle = if let Some(path) = icon { svg::Handle::from_path(path) } else { eprintln!("icon '{:?}' size {} not found", &self.name, self.size); svg::Handle::from_memory(Vec::new()) }; let mut widget = svg::Svg::::new(handle) .style(self.style) .width(self.width.unwrap_or(Length::Units(self.size))) .height(self.height.unwrap_or(Length::Units(self.size))); if let Some(content_fit) = self.content_fit { widget = widget.content_fit(content_fit); } widget.into() } else { let icon_path = icon.unwrap(); let mut image = Image::new(icon_path) .width(self.width.unwrap_or(Length::Units(self.size))) .height(self.height.unwrap_or(Length::Units(self.size))); if let Some(content_fit) = self.content_fit { image = image.content_fit(content_fit); } image.into() } }) .into() } } impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(icon: Icon<'a>) -> Self { icon.into_element::() } }