// 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::{image, svg, Image}, ContentFit, Length, }; use std::{ borrow::Cow, collections::hash_map::DefaultHasher, ffi::OsStr, hash::Hash, hash::Hasher, path::Path, path::PathBuf, }; #[derive(Clone, Debug, Hash)] pub enum Handle { Image(image::Handle), Svg(svg::Handle), } #[derive(Clone, Debug, Hash)] pub enum IconSource<'a> { Path(Cow<'a, Path>), Name(Cow<'a, str>), Handle(Handle), } impl<'a> IconSource<'a> { /// Loads the icon as either an image or svg [`Handle`]. #[must_use] pub fn load(&self, size: u16, theme: Option<&str>, svg: bool) -> Handle { let name_path_buffer: Option; let icon: Option<&Path> = match self { IconSource::Handle(handle) => return handle.clone(), IconSource::Path(ref path) => Some(path), #[cfg(unix)] IconSource::Name(ref name) => { let icon = crate::settings::DEFAULT_ICON_THEME.with(|default_theme| { let default_theme: &str = &default_theme.borrow(); freedesktop_icons::lookup(name) .with_size(size) .with_theme(theme.unwrap_or(default_theme)) .with_cache() .find() }); name_path_buffer = if icon.is_none() { freedesktop_icons::lookup(name) .with_size(size) .with_cache() .find() } else { icon }; name_path_buffer.as_deref() } // TODO: Icon loading mechanism for non-Unix systems #[cfg(not(unix))] IconSource::Name(_) => None, }; let is_svg = 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!("svg icon '{self:?}' size {size} not found"); svg::Handle::from_memory(Vec::new()) }; Handle::Svg(handle) } else if let Some(icon) = icon { Handle::Image(icon.into()) } else { eprintln!("icon '{self:?}' size {size} not found"); Handle::Image(image::Handle::from_memory(Vec::new())) } } /// Get a handle to a raster image from a path. pub fn raster_from_path(path: impl Into) -> Self { IconSource::Handle(Handle::Image(image::Handle::from_path(path))) } /// Get a handle to a raster image from memory. pub fn raster_from_memory( bytes: impl Into> + std::convert::AsRef<[u8]> + std::marker::Send + std::marker::Sync + 'static, ) -> Self { IconSource::Handle(Handle::Image(image::Handle::from_memory(bytes))) } /// Get a handle to a raster image from RGBA data, where you must define the width and height. pub fn raster_from_pixels( width: u32, height: u32, pixels: impl Into> + std::convert::AsRef<[u8]> + std::marker::Send + std::marker::Sync + 'static, ) -> Self { IconSource::Handle(Handle::Image(image::Handle::from_pixels( width, height, pixels, ))) } /// Get a handle to a SVG from a path. pub fn svg_from_path(path: impl Into) -> Self { IconSource::Handle(Handle::Svg(svg::Handle::from_path(path))) } /// Get a handle to a SVG from memory. pub fn svg_from_memory(bytes: impl Into>) -> Self { IconSource::Handle(Handle::Svg(svg::Handle::from_memory(bytes))) } } 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 From for IconSource<'static> { fn from(handle: image::Handle) -> Self { Self::Handle(Handle::Image(handle)) } } impl From for IconSource<'static> { fn from(handle: svg::Handle) -> Self { Self::Handle(Handle::Svg(handle)) } } /// A lazily-generated icon. #[derive(Setters)] pub struct Icon<'a> { #[setters(skip)] source: IconSource<'a>, #[setters(strip_option, into)] theme: Option>, style: crate::theme::Svg, size: u16, content_fit: ContentFit, #[setters(strip_option)] width: Option, #[setters(strip_option)] height: Option, force_svg: bool, } // XXX Hopefully this will be enough precision impl Hash for Icon<'_> { #[allow(clippy::cast_possible_truncation)] fn hash(&self, state: &mut H) { self.source.hash(state); self.theme.hash(state); self.style.hash(state); self.size.hash(state); self.content_fit.hash(state); self.force_svg.hash(state); match self.width { Some(Length::Fill) => 0.hash(state), Some(Length::Shrink) => 1.hash(state), Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state), Some(Length::FillPortion(p)) => p.hash(state), None => 2.hash(state), } match self.height { Some(Length::Fill) => 0.hash(state), Some(Length::Shrink) => 1.hash(state), Some(Length::Fixed(v)) => ((v * 1000.0) as i32).hash(state), Some(Length::FillPortion(p)) => p.hash(state), None => 2.hash(state), } } } /// A lazily-generated icon. #[must_use] pub fn icon<'a>(source: impl Into>, size: u16) -> Icon<'a> { Icon { content_fit: ContentFit::Fill, height: None, source: source.into(), size, style: crate::theme::Svg::default(), theme: None, width: None, force_svg: false, } } impl<'a> Icon<'a> { fn raster_element(&self, handle: image::Handle) -> Element<'static, Message> { Image::new(handle) .width(self.width.unwrap_or(Length::Fixed(f32::from(self.size)))) .height(self.height.unwrap_or(Length::Fixed(f32::from(self.size)))) .content_fit(self.content_fit) .into() } fn svg_element(&self, handle: svg::Handle) -> Element<'static, Message> { svg::Svg::::new(handle) .style(self.style.clone()) .width(self.width.unwrap_or(Length::Fixed(f32::from(self.size)))) .height(self.height.unwrap_or(Length::Fixed(f32::from(self.size)))) .content_fit(self.content_fit) .into() } #[must_use] fn into_element(mut self) -> Element<'a, Message> { let mut hasher = DefaultHasher::new(); self.hash(&mut hasher); if self.theme.is_none() { crate::settings::DEFAULT_ICON_THEME.with(|f| f.borrow().hash(&mut hasher)); } let hash = hasher.finish(); let mut source = IconSource::Name(Cow::Borrowed("")); std::mem::swap(&mut source, &mut self.source); iced::widget::lazy(hash, move |_| -> Element { match source.load(self.size, self.theme.as_deref(), self.force_svg) { Handle::Svg(handle) => self.svg_element(handle), Handle::Image(handle) => self.raster_element(handle), } }) .into() } } impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(icon: Icon<'a>) -> Self { icon.into_element::() } }