use std::collections::HashMap;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{
LitInt, LitStr,
parse::{Parse, ParseStream},
parse_macro_input,
token::Comma,
};
use ttf_parser::Face;
struct Input {
font_path: LitStr,
module_name: Ident,
font_name: Ident,
doc_link: Option<LitStr>,
}
impl Parse for Input {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let font_path = input.parse()?;
let _: Comma = input.parse()?;
let module_name = input.parse()?;
let _: Comma = input.parse()?;
let font_name = input.parse()?;
let _: Option<Comma> = input.parse()?;
let doc_link = input.parse()?;
let _: Option<Comma> = input.parse()?;
Ok(Self {
font_path,
module_name,
font_name,
doc_link,
})
}
}
#[proc_macro]
pub fn generate_icon_functions(input: TokenStream) -> TokenStream {
body(input, "basic")
}
#[proc_macro]
pub fn generate_icon_advanced_functions(input: TokenStream) -> TokenStream {
body(input, "advanced")
}
fn body(input: TokenStream, shaping: &str) -> TokenStream {
let Input {
font_path,
module_name,
font_name,
doc_link,
} = parse_macro_input!(input as Input);
let font_path_str = font_path.value();
let font_data = std::fs::read(&font_path_str).expect("Failed to read font file");
let face = Face::parse(&font_data, 0).expect("Failed to parse font");
let mut all_codepoints: Vec<char> = Vec::new();
if let Some(unicode_subtable) = face
.tables()
.cmap
.unwrap()
.subtables
.into_iter()
.find(|s| s.is_unicode())
{
unicode_subtable.codepoints(|c| {
use std::convert::TryFrom;
if let Ok(u) = char::try_from(c) {
all_codepoints.push(u);
}
});
}
let mut functions = proc_macro2::TokenStream::new();
let mut advanced_functions = proc_macro2::TokenStream::new();
let mut duplicates: HashMap<String, u32> = HashMap::new();
let mut count = 0;
#[cfg(feature = "_generate_demo")]
let mut demo_counter = 0;
#[cfg(feature = "_generate_demo")]
let mut demo_rows = 0;
#[cfg(feature = "_generate_demo")]
println!("row![");
'outer: for c in all_codepoints {
if let Some(glyph_id) = face.glyph_index(c) {
let raw_name = face.glyph_name(glyph_id).unwrap_or("unnamed");
let mut processed_name = raw_name
.replace("-", "_")
.replace('0', "zero")
.replace('1', "one")
.replace('2', "two")
.replace('3', "three")
.replace('4', "four")
.replace('5', "five")
.replace('6', "six")
.replace('7', "seven")
.replace('8', "eight")
.replace('9', "nine");
if processed_name.as_str() == "_" {
processed_name = String::from("underscore");
}
for c in processed_name.chars() {
match c {
'+' | '-' | '*' | '/' | '@' | '!' | '#' | '$' | '%' | '^' | '&' | '(' | ')'
| '=' | '~' | '`' | ';' | ':' | '"' | '\'' | ',' | '<' | '>' | '?' | '.'
| ' ' | '[' | ']' | '{' | '}' | '|' | '\\' => continue 'outer,
_ => {}
}
}
match duplicates.get(&processed_name) {
Some(amount) => {
duplicates.insert(processed_name.clone(), *amount + 1);
continue 'outer;
}
None => {
duplicates.insert(processed_name.clone(), 1);
}
}
#[cfg(feature = "_generate_demo")]
if demo_rows < 18 {
if demo_counter == 27 {
demo_counter = 0;
demo_rows += 1;
println!("{}(),", processed_name);
println!("]");
println!(".padding(12)");
println!(".spacing(20)");
println!(".width(Length::Fill)");
println!(".align_y(Center),");
println!("row![");
} else {
demo_counter += 1;
println!("{}(),", processed_name);
}
}
let fn_name = Ident::new_raw(&processed_name, Span::call_site());
let doc = match doc_link {
Some(ref location) => format!(
" Returns an [`iced_widget::Text`] widget of the [{} {}]({}/{}) icon.",
c,
processed_name,
location.value(),
raw_name,
),
None => format!(
" Returns an [`iced_widget::Text`] widget of the {} {} icon.",
c, processed_name
),
};
let shaping = match shaping {
"basic" => {
quote! { text::Shaping::Basic }
}
"advanced" => {
quote! { text::Shaping::Advanced }
}
_ => panic!(
"Shaping either needs to be basic or advanced, if you are unsure use advanced."
),
};
functions.extend(quote! {
#[doc = #doc]
#[must_use]
pub fn #fn_name<'a, Theme: Catalog + 'a, Renderer: text::Renderer<Font = Font>>() -> Text<'a, Theme, Renderer> {
use iced_widget::text;
text(#c).font(#font_name).shaping(#shaping)
}
});
let doc = format!(
" Returns the [`String`] of {} character for lower level API's",
processed_name
);
advanced_functions.extend(quote! {
#[doc = #doc]
#[must_use]
pub fn #fn_name() -> (String, Font, Shaping) {
(#c.to_string(), #font_name, #shaping)
}
});
count += 1;
}
}
#[cfg(feature = "_generate_demo")]
println!("We have {} icons", count);
let advanced_text_tokens = if cfg!(feature = "advanced_text") {
quote! {
pub mod advanced_text {
use iced_widget::core::Font;
use iced_widget::text::{self, Shaping};
use crate::#font_name;
#advanced_functions
}
}
} else {
quote! {}
};
let count_lit = LitInt::new(&count.to_string(), Span::call_site());
let doc = format!(
"A module with a function for every icon in {}'s font.",
module_name.to_string()
);
TokenStream::from(quote! {
#[doc = #doc]
pub mod #module_name {
use iced_widget::core::text;
use iced_widget::core::Font;
use iced_widget::text::Text;
use iced_widget::text::Catalog;
use crate::#font_name;
pub const COUNT: usize = #count_lit;
#functions
#advanced_text_tokens
}
})
}