From e42750863b77cb4c82cd48bc0a25af903b558069 Mon Sep 17 00:00:00 2001 From: Malte Veerman Date: Fri, 6 Dec 2019 16:00:25 +0100 Subject: [PATCH] Implemented theme icons. Theme icons are icon widgets that are created with only their name as arguments. The global static `ICON_LOADER` then searches the currently used icon theme for an icon with that name. If an icon with that name exists with multiple sizes in the theme, the icon with the best size for the widget is chosen. --- examples/todos.rs | 44 +-- native/Cargo.toml | 3 + native/src/icon_loader.rs | 5 + native/src/icon_loader/icon_loader.rs | 413 ++++++++++++++++++++++++++ native/src/lib.rs | 2 + native/src/widget/icon.rs | 71 ++++- wgpu/src/renderer/widget/icon.rs | 32 +- 7 files changed, 530 insertions(+), 40 deletions(-) create mode 100644 native/src/icon_loader.rs create mode 100644 native/src/icon_loader/icon_loader.rs diff --git a/examples/todos.rs b/examples/todos.rs index 5f435fdcfb..38283ec715 100644 --- a/examples/todos.rs +++ b/examples/todos.rs @@ -1,7 +1,7 @@ use iced::{ button, scrollable, text_input, Align, Application, Button, Checkbox, Color, Column, Command, Container, Element, Font, HorizontalAlignment, - Length, Row, Scrollable, Settings, Text, TextInput, + Icon, Length, Row, Scrollable, Settings, Text, TextInput, }; use serde::{Deserialize, Serialize}; @@ -291,12 +291,9 @@ impl Task { .align_items(Align::Center) .push(checkbox) .push( - Button::new( - edit_button, - edit_icon().color([0.5, 0.5, 0.5]), - ) - .on_press(TaskMessage::Edit) - .padding(10), + Button::new(edit_button, edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10), ) .into() } @@ -320,14 +317,11 @@ impl Task { .push( Button::new( delete_button, - Row::new() - .spacing(10) - .push(delete_icon().color(Color::WHITE)) - .push( - Text::new("Delete") - .width(Length::Shrink) - .color(Color::WHITE), - ), + Row::new().spacing(10).push(delete_icon()).push( + Text::new("Delete") + .width(Length::Shrink) + .color(Color::WHITE), + ), ) .on_press(TaskMessage::Delete) .padding(10) @@ -471,12 +465,24 @@ fn icon(unicode: char) -> Text { .size(20) } -fn edit_icon() -> Text { - icon('\u{F303}') +fn edit_icon<'a, Message>() -> Element<'a, Message> { + if Icon::theme_includes_icon("document-properties") { + Icon::from_theme("document-properties") + .size(Length::Units(32)) + .into() + } else { + icon('\u{F303}').color([0.5, 0.5, 0.5]).into() + } } -fn delete_icon() -> Text { - icon('\u{F1F8}') +fn delete_icon<'a, Message>() -> Element<'a, Message> { + if Icon::theme_includes_icon("edit-delete") { + Icon::from_theme("edit-delete") + .size(Length::Units(32)) + .into() + } else { + icon('\u{F1F8}').color(Color::WHITE).into() + } } // Persistence diff --git a/native/Cargo.toml b/native/Cargo.toml index 6ece36e4f8..58015ba904 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -12,3 +12,6 @@ iced_core = { version = "0.1.0", path = "../core", features = ["command"] } twox-hash = "1.5" raw-window-handle = "0.3" unicode-segmentation = "1.6" +xdg = "2.2" +rust-ini = "0.13" +lazy_static = "1.4" diff --git a/native/src/icon_loader.rs b/native/src/icon_loader.rs new file mode 100644 index 0000000000..1adc30e2ce --- /dev/null +++ b/native/src/icon_loader.rs @@ -0,0 +1,5 @@ +//! A collection of structs to handle themed icons. + +mod icon_loader; + +pub use icon_loader::*; diff --git a/native/src/icon_loader/icon_loader.rs b/native/src/icon_loader/icon_loader.rs new file mode 100644 index 0000000000..76247a5a97 --- /dev/null +++ b/native/src/icon_loader/icon_loader.rs @@ -0,0 +1,413 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::RwLock, +}; + +use lazy_static::*; + +lazy_static! { + /// The static [`IconLoader`]. Use this to search for and load themed icons. + /// + /// [`IconLoader`]: struct.IconLoader.html + pub static ref ICON_LOADER: IconLoader = IconLoader::new(); +} + +/// Struct that searches and loads themed icons. +#[derive(Debug)] +pub struct IconLoader { + theme_key: u32, + user_theme: RwLock>, + system_theme: RwLock>, + theme_list: RwLock>, +} + +impl IconLoader { + fn new() -> Self { + Self { + theme_key: 1, + user_theme: RwLock::new(None), + system_theme: RwLock::new(Self::system_theme_name()), + theme_list: RwLock::new(HashMap::new()), + } + } + + /// Returns the currently used theme name. + /// Returns `None`, if no theme name is set. + pub fn theme_name(&self) -> Option { + if let Some(user_theme) = self.user_theme.read().unwrap().clone() { + Some(user_theme) + } else { + self.system_theme.read().unwrap().clone() + } + } + + /// Set a new icon theme name. Newly loaded icons will be from the new + /// theme. + pub fn set_theme_name(&mut self, theme_name: impl Into) { + let theme_name = theme_name.into(); + + if theme_name.is_empty() { + return; + } + + if self + .user_theme + .read() + .unwrap() + .as_ref() + .map_or(true, |ut| ut != &theme_name) + { + *self.user_theme.write().unwrap() = Some(theme_name.into()); + self.invalidate_key(); + } + } + + /// Load the icon from the current icon theme with the name `icon_name`. + pub fn load_icon( + &self, + icon_name: impl AsRef, + ) -> Option { + if let Some(theme_name) = self.theme_name() { + self.find_icon(theme_name, icon_name) + } else { + None + } + } + + /// Returns `true`, if a custom icon theme name was set. + /// Returns `false` otherwise. + pub fn has_user_theme(&self) -> bool { + self.user_theme.read().unwrap().is_some() + } + + /// Returns the name of the system icon theme. + pub fn system_theme_name() -> Option { + //TODO: System theme logic + Some(String::from( + "hicolor", + )) + } + + fn find_icon( + &self, + theme_name: impl Into, + icon_name: impl AsRef, + ) -> Option { + let theme_name = theme_name.into(); + if theme_name.is_empty() { + return None; + } + + let mut read_lock = self.theme_list.read().unwrap(); + let mut theme = read_lock.get(&theme_name); + if theme.is_none() { + if let Some(new_theme) = IconTheme::new(&theme_name) { + std::mem::drop(read_lock); + + if self + .theme_list + .write() + .unwrap() + .insert(theme_name.clone(), new_theme) + .is_some() + { + println!("Oops"); + } + + read_lock = self.theme_list.read().unwrap(); + theme = read_lock.get(&theme_name); + } else { + return None; + } + } + + let theme = theme.unwrap(); + let icon_name = icon_name.as_ref(); + let mut icon_png_name = icon_name.to_string(); + icon_png_name.push_str(".png"); + let mut icon_svg_name = icon_name.to_string(); + icon_svg_name.push_str(".svg"); + + let mut entries = Vec::new(); + + for content_dir_path in theme.content_dirs() { + for icon_dir_info in theme.key_list() { + let png_path = content_dir_path + .join(&icon_dir_info.path) + .join(&icon_png_name); + + if png_path.is_file() { + entries.insert( + 0, + ThemeIconFile::PNG { + dir_info: icon_dir_info.clone(), + file_path: png_path, + }, + ); + } else { + let svg_path = content_dir_path + .join(&icon_dir_info.path) + .join(&icon_svg_name); + + if svg_path.is_file() { + entries.push(ThemeIconFile::SVG { + dir_info: icon_dir_info.clone(), + file_path: svg_path, + }); + } + } + } + } + + ThemeIconInfo::new(icon_name, entries) + } + + fn invalidate_key(&mut self) { + self.theme_key += 1; + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct IconTheme { + content_dirs: Vec, + key_list: Vec, +} + +impl IconTheme { + fn new(name: impl AsRef) -> Option { + let mut icon_theme = Self { + content_dirs: Vec::new(), + key_list: Vec::new(), + }; + + if let Ok(base_dirs) = xdg::BaseDirectories::new() { + for data_dir in base_dirs.get_data_dirs() { + let theme_dir_path = data_dir.join("icons").join(&name); + if theme_dir_path.is_dir() { + icon_theme.content_dirs.push(theme_dir_path.clone()); + } + + let theme_index_path = theme_dir_path.join("index.theme"); + if theme_index_path.is_file() { + //TODO: Better error handling + if let Ok(ini) = ini::Ini::load_from_file(theme_index_path) + { + for (dir_key, properties) in ini.iter() { + if let Some(dir_key) = dir_key { + let mut dir_info = IconDirInfo::new(dir_key); + + for (key, value) in properties { + match key.as_str() { + "Size" => { + if let Ok(size) = value.parse() { + dir_info.size = size; + } + } + "Type" => { + dir_info.dir_type = value.into() + } + "Threshold" => { + if let Ok(threshold) = value.parse() + { + dir_info.threshold = Some(threshold); + } + } + "MinSize" => { + if let Ok(min_size) = value.parse() + { + dir_info.min_size = Some(min_size); + } + } + "MaxSize" => { + if let Ok(max_size) = value.parse() + { + dir_info.max_size = Some(max_size); + } + } + "Scale" => { + if let Ok(scale) = value.parse() { + dir_info.scale = scale; + } + } + _ => {} + } + } + + if dir_info.is_valid() { + icon_theme.key_list.push(dir_info); + } + } else { + continue; + } + } + } + } + } + } + + if icon_theme.key_list.is_empty() { + None + } else { + Some(icon_theme) + } + } + + fn content_dirs(&self) -> &Vec { + &self.content_dirs + } + + fn key_list(&self) -> &Vec { + &self.key_list + } +} + +#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)] +enum IconDirType { + Fixed, + Scalable, + Threshold, +} + +impl> From for IconDirType { + fn from(s: S) -> Self { + match s.as_ref() { + "Fixed" => IconDirType::Fixed, + "Scalable" => IconDirType::Scalable, + _ => IconDirType::Threshold, + } + } +} + +/// Struct that holds information about a directory containing a set of icons +/// with a particular size. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct IconDirInfo { + dir_type: IconDirType, + path: PathBuf, + size: u16, + max_size: Option, + min_size: Option, + threshold: Option, + scale: u16, +} + +impl IconDirInfo { + fn new(path: impl Into) -> Self { + Self { + dir_type: IconDirType::Threshold, + path: path.into(), + size: 0, + max_size: None, + min_size: None, + threshold: None, + scale: 1, + } + } + + /// Returns the size of the icons contained. + pub fn size(&self) -> u16 { + self.size + } + + fn is_valid(&self) -> bool { + self.size != 0 + } +} + +/// Struct containing information about an icon. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct ThemeIconInfo { + files: Vec, + icon_name: String, +} + +impl ThemeIconInfo { + /// Returns the name of the associated icon. + pub fn icon_name(&self) -> &str { + &self.icon_name + } + + ///Returns a list of [`ThemeIconFile`]s for the associated icon. + /// + /// [`ThemeIconFile`]: enum.ThemeIconFile.html + pub fn files(&self) -> &Vec { + &self.files + } + + /// Returns the [`ThemeIconFile`] of the associated icon that fits the given + /// `size` the closest. + pub fn file_for_size(&self, size: u16) -> &ThemeIconFile { + // Try to return an exact fit. + if let Some(icon_file) = self + .files + .iter() + .find(|file| size == file.dir_info().size()) + { + return icon_file; + } + + // Try to return a slightly bigger fit. + if let Some(icon_file) = self + .files + .iter() + .filter(|file| file.dir_info().size() > size) + .min_by_key(|file| file.dir_info().size() - size) + { + return icon_file; + } + + // return the biggest available. + self.files + .iter() + .max_by_key(|file| file.dir_info().size()) + .unwrap() // Empty ThemeIconInfos are never created. + } + + fn new( + icon_name: impl Into, + files: Vec, + ) -> Option { + let icon_name = icon_name.into(); + if icon_name.is_empty() || files.is_empty() { + None + } else { + Some(Self { files, icon_name }) + } + } +} + +/// Enum containing information about a single icon file on disk. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum ThemeIconFile { + /// An icon file with ending `.png` + PNG { + dir_info: IconDirInfo, + file_path: PathBuf, + }, + + /// An icon file with ending `.svg` + SVG { + dir_info: IconDirInfo, + file_path: PathBuf, + }, +} + +impl ThemeIconFile { + /// Returns information about the directory the icon file lives in. + pub fn dir_info(&self) -> &IconDirInfo { + match self { + ThemeIconFile::PNG { dir_info, .. } => dir_info, + ThemeIconFile::SVG { dir_info, .. } => dir_info, + } + } + + /// Returns the file path of this icon file. + pub fn file_path(&self) -> &Path { + match self { + ThemeIconFile::PNG { file_path, .. } => file_path.as_path(), + ThemeIconFile::SVG { file_path, .. } => file_path.as_path(), + } + } +} diff --git a/native/src/lib.rs b/native/src/lib.rs index 45c3c69919..5370e18a89 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -39,6 +39,7 @@ #![deny(unused_results)] #![deny(unsafe_code)] #![deny(rust_2018_idioms)] +pub mod icon_loader; pub mod input; pub mod layout; pub mod renderer; @@ -59,6 +60,7 @@ pub use iced_core::{ pub use element::Element; pub use event::Event; pub use hasher::Hasher; +pub use icon_loader::*; pub use layout::Layout; pub use mouse_cursor::MouseCursor; pub use renderer::Renderer; diff --git a/native/src/widget/icon.rs b/native/src/widget/icon.rs index 47995306ff..867d0080fb 100644 --- a/native/src/widget/icon.rs +++ b/native/src/widget/icon.rs @@ -1,13 +1,16 @@ //! Display an icon. -use crate::{layout, Element, Hasher, Layout, Length, Point, Rectangle, Widget}; +use crate::{ + layout, Element, Hasher, Layout, Length, Point, Rectangle, ThemeIconInfo, + Widget, ICON_LOADER, +}; use std::hash::Hash; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; /// A simple icon_loader widget. #[derive(Debug, Clone)] pub struct Icon { - path: PathBuf, + handle: Handle, size: Length, } @@ -17,7 +20,17 @@ impl Icon { /// [`Icon`]: struct.Icon.html pub fn new(path: impl Into) -> Self { Icon { - path: path.into(), + handle: Handle::from_path(path), + size: Length::Fill, + } + } + + /// Create a new [`Icon`] with the name `icon_name`. + /// + /// [`Icon`]: struct.Icon.html + pub fn from_theme(icon_name: impl AsRef) -> Self { + Icon { + handle: Handle::from_theme(icon_name), size: Length::Fill, } } @@ -29,6 +42,12 @@ impl Icon { self.size = size; self } + + /// Returns `true`, if the current icon theme includes `icon_name` + /// otherwise returns `false`. + pub fn theme_includes_icon(icon_name: impl AsRef) -> bool { + ICON_LOADER.load_icon(icon_name).is_some() + } } impl Widget for Icon @@ -43,15 +62,8 @@ where self.size } - fn layout( - &self, - _: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits - .width(self.size) - .height(self.size) - .max(); + fn layout(&self, _: &Renderer, limits: &layout::Limits) -> layout::Node { + let size = limits.width(self.size).height(self.size).max(); layout::Node::new(size) } @@ -66,7 +78,7 @@ where renderer.draw( bounds, - self.path.as_path(), + &self.handle, ) } @@ -75,6 +87,35 @@ where } } +/// An [`Icon`] handle. +/// +/// [`Icon`]: struct.Icon.html +#[derive(Debug, Clone, Hash, PartialEq)] +pub enum Handle { + /// An icon handle associated with a theme icon + Theme(ThemeIconInfo), + /// An icon handle associated with a single icon file path + Simple(PathBuf), + /// An empty icon handle + None, +} + +impl Handle { + /// Create an [`Icon`] handle from a file path. + /// + /// [`Icon`]: struct.Icon.html + pub fn from_path(path: impl Into) -> Self { + Self::Simple(path.into()) + } + + fn from_theme(icon_name: impl AsRef) -> Self { + match ICON_LOADER.load_icon(icon_name) { + Some(icon) => Self::Theme(icon), + None => Self::None, + } + } +} + /// The renderer of an [`Icon`]. /// /// Your [renderer] will need to implement this trait before being @@ -90,7 +131,7 @@ pub trait Renderer: crate::Renderer { fn draw( &mut self, bounds: Rectangle, - path: &Path, + handle: &Handle, ) -> Self::Output; } diff --git a/wgpu/src/renderer/widget/icon.rs b/wgpu/src/renderer/widget/icon.rs index a271bb4717..54ac136203 100644 --- a/wgpu/src/renderer/widget/icon.rs +++ b/wgpu/src/renderer/widget/icon.rs @@ -1,18 +1,38 @@ -use crate::{svg::Handle, Primitive, Renderer}; +use crate::{svg, Primitive, Renderer}; use iced_native::{ - icon, MouseCursor, Rectangle, + icon, image, MouseCursor, Rectangle, }; -use std::path::Path; +use std::path::PathBuf; impl icon::Renderer for Renderer { fn draw( &mut self, bounds: Rectangle, - path: &Path, + handle: &icon::Handle, ) -> Self::Output { + let path = match handle { + icon::Handle::Simple(path) => path.clone(), + icon::Handle::Theme(icon_info) => { + icon_info.file_for_size(bounds.width as u16).file_path().to_path_buf() + }, + icon::Handle::None => PathBuf::new(), + }; + + if let Some(ext) = path.extension() { + if ext == "svg" || ext == "SVG" || ext == "svgz" || ext == "SVGZ" { + return ( + Primitive::Svg { + handle: svg::Handle::from_path(path), + bounds, + }, + MouseCursor::OutOfBounds, + ); + } + } + ( - Primitive::Svg { - handle: Handle::from_path(path), + Primitive::Image { + handle: image::Handle::from_path(path), bounds, }, MouseCursor::OutOfBounds,