diff --git a/Cargo.toml b/Cargo.toml index b4e05fe..1e360b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,9 @@ members = ["update_icons"] [dependencies] gtk = { version = "0.8", package = "gtk4" } - -[build-dependencies] -gvdb = { version = "0.5.3", features = ["gresource"] } -serde = { version = "1.0.197", features = ["derive"] } -toml = "0.8.11" +gvdb = { version = "0.5.3", features = ["gresource"], optional = true } +serde = { version = "1.0.197", features = ["derive"], optional = true } +toml = { version = "0.8.11", optional = true } [package.metadata.docs.rs] all-features = true @@ -38,3 +36,8 @@ all = [] # Enable entire icon kits icon-development-kit = [] fluent-system-icons = [] +build-utils = [ + "gvdb", + "serde", + "toml", +] \ No newline at end of file diff --git a/README.md b/README.md index 4536f66..eea7ed4 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,39 @@ icon_folder = "my_svg_icons" ```toml relm4-icons = "0.8.0" + +[build-dependencies] +relm4-icons = { version = "0.8.0", features = ["build-utils"] } +``` + +### 4. Bundle the icons 📦 + +Create a module named `icon_names` in your crate like this: + +```rust +mod icon_names { + include!(concat!(env!("OUT_DIR"), "/icon_names.rs")); +} +``` + +And in your `build.rs` file, use `relm4-icons` to bundle the icons and include them in the compiled binary: + +```rust + +fn main() { + let manifest_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config = Config::load( + &manifest_path, + Some(relm4_icons::constants::SHIPPED_ICONS_PATH.to_string()), + ) + .expect("couldn't load manifest"); + + relm4_icons::build_utils::bundle_icons(config, &manifest_path); +} ``` -### 4. Load the icons 🛫 +### 5. Load the icons 🛫 Add this to your initialization code: @@ -78,7 +108,7 @@ button.set_icon_name("plus"); You can also use the `icon_names` module for extra compile-time generated icon names. ```rust -use relm4_icons::icon_names; +use crate::icon_names; let button = gtk::Button::default(); button.set_icon_name(icon_names::PLUS); diff --git a/build.rs b/build.rs index bbaaf7d..00a164e 100644 --- a/build.rs +++ b/build.rs @@ -1,176 +1,21 @@ -use std::{ - collections::HashMap, - env, - ffi::OsStr, - io, - path::{Path, PathBuf}, -}; +//! Save constant names for icons and manifest path -use gvdb::gresource::{GResourceBuilder, GResourceFileData, PreprocessOptions}; - -const CONFIG_FILE: &str = "icons.toml"; -const GENERAL_PREFIX: &str = "/org/gtkrs/icons/scalable/actions/"; -const SHIPPED_ICONS_PATH: &str = "icons"; - -const TARGET_FILE: &str = "resources.gresource"; -const CONSTANTS_FILE: &str = "icon_names.rs"; - -#[derive(Default, serde::Deserialize)] -struct Config { - app_id: Option, - base_resource_path: Option, - icon_folder: Option, - icons: Option>, -} - -impl Config { - fn load(dir: &str) -> Result { - let config_path: PathBuf = [dir, CONFIG_FILE].iter().collect(); - let config_file = std::fs::read_to_string(config_path)?; - Ok(toml::from_str(&config_file).expect("Couldn't parse icon config file")) - } -} - -fn path_to_icon_name(string: &OsStr) -> String { - match string.to_str() { - Some(string) => { - if string.ends_with(".svg") { - string - .trim_end_matches("-symbolic.svg") - .trim_end_matches(".svg") - .to_owned() - } else { - panic!("Found non-icon file `{string}`"); - } - } - None => panic!("Failed to convert file name `{string:?}` to string"), - } -} +const CONSTANTS_FILE: &str = "constants.rs"; fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let mut manifest_dir = Path::new(&out_dir).canonicalize().unwrap(); - eprintln!("Canonical manifest dir: {manifest_dir:?}"); - - let (config, config_dir) = if cfg!(docsrs) { - if let Ok(source_dir) = env::var("SOURCE_DIR") { - (Config::load(&source_dir).unwrap_or_default(), source_dir) - } else { - (Config::default(), "".into()) - } - } else { - // Try finding the target directory which is just below the manifest directory - // of the user. - // Unfortunately, the CARGO_MANIFEST_DIR env var passed by cargo always points - // to this crate, so we wouldn't find the users config file this way. - while !manifest_dir.join("Cargo.toml").exists() { - if !manifest_dir.pop() { - panic!("Couldn't find your manifest directory"); - } - } - let config_dir = manifest_dir - .to_str() - .expect("Couldn't convert manifest directory to string") - .to_owned(); - ( - Config::load(&config_dir).expect("Couldn't find `icons.toml` next to `Cargo.toml`"), - config_dir, - ) - }; + let out_dir = std::env::var("OUT_DIR").unwrap(); - eprintln!("Canonical config dir: {config_dir:?}"); - println!("cargo:rerun-if-changed={config_dir}/icons.toml"); - - let mut icons: HashMap = HashMap::new(); - - if let Some(folder) = &config.icon_folder { - println!("cargo:rerun-if-changed={folder}"); - let custom_icons_path: PathBuf = [&config_dir, folder].iter().collect(); - let read_dir = std::fs::read_dir(custom_icons_path) - .expect("Couldn't open icon path specified in config (relative to the manifest)"); - for entry in read_dir { - let entry = entry.unwrap(); - let icon_name = path_to_icon_name(&entry.file_name()); - if icons.insert(icon_name.clone(), entry.path()).is_some() { - panic!("Icon with name `{icon_name}` exists twice") - } - } - } - - if let Some(icon_names) = config.icons { - let dirs = - std::fs::read_dir(SHIPPED_ICONS_PATH).expect("Couldn't open folder of shipped icons"); - let dirs: Vec<_> = dirs - .map(|entry| { - let entry = entry.expect("Couldn't open directories in shipped icon folder"); - entry.path() - }) - .collect(); - - 'outer: for icon_name in icon_names { - for dir in &dirs { - let icon_file_name = format!("{icon_name}-symbolic.svg"); - let icon_path = dir.join(icon_file_name); - if icon_path.exists() { - if icons.insert(icon_name.clone(), icon_path).is_some() { - panic!("Icon with name `{icon_name}` exists twice") - } - continue 'outer; - } - } - panic!("Icon {icon_name} not found in shipped icons"); - } - } - - let prefix = if let Some(base_resource_path) = &config.base_resource_path { - format!("{}icons/scalable/actions/", base_resource_path) - } else if let Some(app_id) = &config.app_id { - format!("/{}/icons/scalable/actions/", app_id.replace('.', "/")) - } else { - GENERAL_PREFIX.into() - }; - - // Generate resource bundle - let resources = icons - .iter() - .map(|(icon, path)| { - GResourceFileData::from_file( - [&prefix, icon, "-symbolic.svg"].into_iter().collect(), - path, - true, - &PreprocessOptions::xml_stripblanks(), - ) - .unwrap() - }) - .collect(); - - let data = GResourceBuilder::from_file_data(resources) - .build() - .expect("Failed to build resource bundle"); - - std::fs::write(Path::new(&out_dir).join(TARGET_FILE), data).unwrap(); + let manifest_path = std::env::var("CARGO_MANIFEST_DIR").unwrap(); // Create file that contains the icon names as constants - let constants: String = icons - .iter() - .map(|(icon_name, icon_path)| { - let const_name = icon_name.to_uppercase().replace('-', "_"); - format!( - " - /// Icon name of the icon `{icon_name}`, found at `{icon_path:?}`. - pub const {const_name}: &str = \"{icon_name}\"; - " - ) - }) - .chain([format!( - "pub(crate) const APP_ID: &str = \"{}\";", - config.app_id.unwrap_or_default() - )]) - .chain([format!( - "pub(crate) const BASE_RESOURCE_PATH: &str = \"{}\";", - config.base_resource_path.unwrap_or_default() - )]) - .collect(); - - std::fs::write(Path::new(&out_dir).join(CONSTANTS_FILE), constants).unwrap(); + let constants = format!( + "pub const SHIPPED_ICONS_PATH: &str = \"{}/icons\";", + manifest_path + ); + + std::fs::write( + std::path::Path::new(&out_dir).join(CONSTANTS_FILE), + constants, + ) + .unwrap(); } diff --git a/icons/icon-development-kit/bookmark-filled-symbolic.svg b/icons/icon-development-kit/bookmark-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/bookmark-filled-symbolic.svg rename to icons/icon-development-kit/bookmark-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/circle-filled-symbolic.svg b/icons/icon-development-kit/circle-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/circle-filled-symbolic.svg rename to icons/icon-development-kit/circle-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/cloud-filled-symbolic.svg b/icons/icon-development-kit/cloud-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/cloud-filled-symbolic.svg rename to icons/icon-development-kit/cloud-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/diamond-filled-symbolic.svg b/icons/icon-development-kit/diamond-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/diamond-filled-symbolic.svg rename to icons/icon-development-kit/diamond-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/flag-filled-symbolic.svg b/icons/icon-development-kit/flag-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/flag-filled-symbolic.svg rename to icons/icon-development-kit/flag-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/grid-filled-symbolic.svg b/icons/icon-development-kit/grid-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/grid-filled-symbolic.svg rename to icons/icon-development-kit/grid-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/heart-filled-symbolic.svg b/icons/icon-development-kit/heart-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/heart-filled-symbolic.svg rename to icons/icon-development-kit/heart-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/keyboard-shift-filled-symbolic.svg b/icons/icon-development-kit/keyboard-shift-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/keyboard-shift-filled-symbolic.svg rename to icons/icon-development-kit/keyboard-shift-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/square-filled-symbolic.svg b/icons/icon-development-kit/square-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/square-filled-symbolic.svg rename to icons/icon-development-kit/square-filled-symbolic-1.svg diff --git a/icons/icon-development-kit/star-filled-symbolic.svg b/icons/icon-development-kit/star-filled-symbolic-1.svg similarity index 100% rename from icons/icon-development-kit/star-filled-symbolic.svg rename to icons/icon-development-kit/star-filled-symbolic-1.svg diff --git a/src/build_utils.rs b/src/build_utils.rs new file mode 100644 index 0000000..c4da286 --- /dev/null +++ b/src/build_utils.rs @@ -0,0 +1,161 @@ +//! Utilities for build scripts. + +use std::collections::HashMap; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::{env, io}; + +use gvdb::gresource::{GResourceBuilder, GResourceFileData, PreprocessOptions}; + +const GENERAL_PREFIX: &str = "/org/gtkrs/icons/scalable/actions/"; +const TARGET_FILE: &str = "resources.gresource"; +const CONSTANTS_FILE: &str = "icon_names.rs"; +const CONFIG_FILE: &str = "icons.toml"; + +/// Configuration for the icons. +#[derive(Debug, Default, serde::Deserialize)] +pub struct Config { + app_id: Option, + base_resource_path: Option, + icons_folder: Option, + shipped_icons_folder: Option, + icons: Option>, +} + +impl Config { + /// Load the configuration from the specified directory. + pub fn load(dir: &str, shipped_icons_folder: Option) -> Result { + let config_path: PathBuf = [dir, CONFIG_FILE].iter().collect(); + let config_file = std::fs::read_to_string(config_path)?; + let mut config: Config = + toml::from_str(&config_file).expect("Couldn't parse icon config file"); + + config.shipped_icons_folder = shipped_icons_folder; + + Ok(config) + } +} + +/// Convert file name to icon name +pub fn path_to_icon_name(string: &OsStr) -> String { + match string.to_str() { + Some(string) => { + if string.ends_with(".svg") { + string + .trim_end_matches("-symbolic.svg") + .trim_end_matches(".svg") + .to_owned() + } else { + panic!("Found non-icon file `{string}`"); + } + } + None => panic!("Failed to convert file name `{string:?}` to string"), + } +} + +/// Given config and config directory, bundle icons and generate constants for icon names. +pub fn bundle_icons(config: Config, config_dir: &str) { + let out_dir = env::var("OUT_DIR").unwrap(); + + eprintln!("Canonical config dir: {config_dir:?}"); + println!("cargo:rerun-if-changed={config_dir}/icons.toml"); + + println!("Building icons from config: {}", config_dir); + + let mut icons: HashMap = HashMap::new(); + + if let Some(folder) = &config.icons_folder { + println!("cargo:rerun-if-changed={folder}"); + let custom_icons_path: PathBuf = [config_dir, folder].iter().collect(); + let read_dir = std::fs::read_dir(custom_icons_path) + .expect("Couldn't open icon path specified in config (relative to the manifest)"); + for entry in read_dir { + let entry = entry.unwrap(); + let icon = path_to_icon_name(&entry.file_name()); + if icons.insert(icon.clone(), entry.path()).is_some() { + panic!("Icon with name `{icon}` exists twice") + } + } + } + + let shipped_icons_folder = config + .shipped_icons_folder + .expect("Could not find icons folder specified in config"); + + if let Some(icon_names) = config.icons { + let dirs = + std::fs::read_dir(shipped_icons_folder).expect("Couldn't open folder of shipped icons"); + let dirs: Vec<_> = dirs + .map(|entry| { + let entry = entry.expect("Couldn't open directories in shipped icon folder"); + entry.path() + }) + .collect(); + + 'outer: for icon in icon_names { + for dir in &dirs { + let icon_file_name = format!("{icon}-symbolic.svg"); + let icon_path = dir.join(icon_file_name); + if icon_path.exists() { + if icons.insert(icon.clone(), icon_path).is_some() { + panic!("Icon with name `{icon}` exists twice") + } + continue 'outer; + } + } + panic!("Icon {icon} not found in shipped icons"); + } + } + + let prefix = if let Some(base_resource_path) = &config.base_resource_path { + format!("{}icons/scalable/actions/", base_resource_path) + } else if let Some(app_id) = &config.app_id { + format!("/{}/icons/scalable/actions/", app_id.replace('.', "/")) + } else { + GENERAL_PREFIX.into() + }; + + // Generate resource bundle + let resources = icons + .iter() + .map(|(icon, path)| { + GResourceFileData::from_file( + [&prefix, icon, "-symbolic.svg"].into_iter().collect(), + path, + true, + &PreprocessOptions::xml_stripblanks(), + ) + .unwrap() + }) + .collect(); + + let data = GResourceBuilder::from_file_data(resources) + .build() + .expect("Failed to build resource bundle"); + + std::fs::write(Path::new(&out_dir).join(TARGET_FILE), data).unwrap(); + + // Create file that contains the icon names as constants + let constants: String = icons + .iter() + .map(|(icon, icon_path)| { + let const_name = icon.to_uppercase().replace('-', "_"); + format!( + " + /// Icon name of the icon `{icon}`, found at `{icon_path:?}`. + pub const {const_name}: &str = \"{icon}\"; + " + ) + }) + .chain([format!( + "pub(crate) const APP_ID: &str = \"{}\";", + config.app_id.unwrap_or_default() + )]) + .chain([format!( + "pub(crate) const BASE_RESOURCE_PATH: &str = \"{}\";", + config.base_resource_path.unwrap_or_default() + )]) + .collect(); + + std::fs::write(Path::new(&out_dir).join(CONSTANTS_FILE), constants).unwrap(); +} diff --git a/src/icon_names.rs b/src/icon_names.rs deleted file mode 100644 index 6f0b54a..0000000 --- a/src/icon_names.rs +++ /dev/null @@ -1 +0,0 @@ -include!(concat!(env!("OUT_DIR"), "/icon_names.rs")); diff --git a/src/lib.rs b/src/lib.rs index ca500f0..9910b52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,22 +19,29 @@ #![allow(clippy::negative_feature_names, clippy::multiple_crate_versions)] #![cfg_attr(docsrs, feature(doc_cfg))] -/// Module containing constants for icons names. -pub mod icon_names; - -use gtk::{gdk, gio}; +/// Utilities for building scripts. +#[cfg(feature = "build-utils")] +pub mod build_utils; +/// Constants file with paths to icons. +pub mod constants { + include!(concat!(env!("OUT_DIR"), "/constants.rs")); +} /// Initialized the icons and registers them globally for your application. -pub fn initialize_icons() { - gio::resources_register_include!("resources.gresource").unwrap(); +#[macro_export] +macro_rules! initialize_icons { + ($base_resource_path:path, $app_id:path) => { + use gtk::{gdk, gio}; + + gio::resources_register_include!("resources.gresource").unwrap(); - #[allow(clippy::const_is_empty)] - if icon_names::APP_ID.is_empty() && icon_names::BASE_RESOURCE_PATH.is_empty() { - gtk::init().unwrap(); + if $base_resource_path.is_empty() && $app_id.is_empty() { + gtk::init().unwrap(); - let display = gdk::Display::default().unwrap(); - let theme = gtk::IconTheme::for_display(&display); - theme.add_resource_path("/org/gtkrs/icons/"); - theme.add_resource_path("/org/gtkrs/icons/scalable/actions/"); - } + let display = gdk::Display::default().unwrap(); + let theme = gtk::IconTheme::for_display(&display); + theme.add_resource_path("/org/gtkrs/icons/"); + theme.add_resource_path("/org/gtkrs/icons/scalable/actions/"); + } + }; }