1use std::collections::HashMap;
2
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span};
5use quote::quote;
6use syn::{
7 LitInt, LitStr,
8 parse::{Parse, ParseStream},
9 parse_macro_input,
10 token::Comma,
11};
12use ttf_parser::Face;
13
14struct Input {
15 font_path: LitStr,
17 module_name: Ident,
19 font_name: Ident,
21 doc_link: Option<LitStr>,
23}
24
25impl Parse for Input {
26 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
27 let font_path = input.parse()?;
28 let _: Comma = input.parse()?;
29 let module_name = input.parse()?;
30 let _: Comma = input.parse()?;
31 let font_name = input.parse()?;
32
33 let _: Option<Comma> = input.parse()?;
35 let doc_link = input.parse()?;
36 let _: Option<Comma> = input.parse()?;
37
38 Ok(Self {
39 font_path,
40 module_name,
41 font_name,
42 doc_link,
43 })
44 }
45}
46
47#[proc_macro]
49pub fn generate_icon_functions(input: TokenStream) -> TokenStream {
50 body(input, "basic")
51}
52
53#[proc_macro]
55pub fn generate_icon_advanced_functions(input: TokenStream) -> TokenStream {
56 body(input, "advanced")
57}
58
59fn body(input: TokenStream, shaping: &str) -> TokenStream {
60 let Input {
61 font_path,
62 module_name,
63 font_name,
64 doc_link,
65 } = parse_macro_input!(input as Input);
66
67 let font_path_str = font_path.value();
68 let font_data = std::fs::read(&font_path_str).expect("Failed to read font file");
69 let face = Face::parse(&font_data, 0).expect("Failed to parse font");
70
71 let mut all_codepoints: Vec<char> = Vec::new();
72 if let Some(unicode_subtable) = face
73 .tables()
74 .cmap
75 .unwrap()
76 .subtables
77 .into_iter()
78 .find(|s| s.is_unicode())
79 {
80 unicode_subtable.codepoints(|c| {
81 use std::convert::TryFrom;
82 if let Ok(u) = char::try_from(c) {
83 all_codepoints.push(u);
84 }
85 });
86 }
87
88 let mut functions = proc_macro2::TokenStream::new();
89 let mut advanced_functions = proc_macro2::TokenStream::new();
90 let mut duplicates: HashMap<String, u32> = HashMap::new();
91 let mut count = 0;
92
93 #[cfg(feature = "_generate_demo")]
94 let mut demo_counter = 0;
95 #[cfg(feature = "_generate_demo")]
96 let mut demo_rows = 0;
97 #[cfg(feature = "_generate_demo")]
98 println!("row![");
99 'outer: for c in all_codepoints {
100 if let Some(glyph_id) = face.glyph_index(c) {
101 let raw_name = face.glyph_name(glyph_id).unwrap_or("unnamed");
102
103 let mut processed_name = raw_name
105 .replace("-", "_")
106 .replace('0', "zero")
107 .replace('1', "one")
108 .replace('2', "two")
109 .replace('3', "three")
110 .replace('4', "four")
111 .replace('5', "five")
112 .replace('6', "six")
113 .replace('7', "seven")
114 .replace('8', "eight")
115 .replace('9', "nine");
116
117 if processed_name.as_str() == "_" {
119 processed_name = String::from("underscore");
120 }
121
122 for c in processed_name.chars() {
125 match c {
126 '+' | '-' | '*' | '/' | '@' | '!' | '#' | '$' | '%' | '^' | '&' | '(' | ')'
127 | '=' | '~' | '`' | ';' | ':' | '"' | '\'' | ',' | '<' | '>' | '?' | '.'
128 | ' ' | '[' | ']' | '{' | '}' | '|' | '\\' => continue 'outer,
129 _ => {}
130 }
131 }
132
133 match duplicates.get(&processed_name) {
135 Some(amount) => {
136 duplicates.insert(processed_name.clone(), *amount + 1);
137 continue 'outer;
139 }
140 None => {
141 duplicates.insert(processed_name.clone(), 1);
142 }
143 }
144
145 #[cfg(feature = "_generate_demo")]
146 if demo_rows < 18 {
147 if demo_counter == 27 {
148 demo_counter = 0;
149 demo_rows += 1;
150
151 println!("{}(),", processed_name);
152 println!("]");
153 println!(".padding(12)");
154 println!(".spacing(20)");
155 println!(".width(Length::Fill)");
156 println!(".align_y(Center),");
157 println!("row![");
158 } else {
159 demo_counter += 1;
160 println!("{}(),", processed_name);
161 }
162 }
163 let fn_name = Ident::new_raw(&processed_name, Span::call_site());
164
165 let doc = match doc_link {
166 Some(ref location) => format!(
167 " Returns an [`iced_widget::Text`] widget of the [{} {}]({}/{}) icon.",
168 c,
169 processed_name,
170 location.value(),
171 raw_name,
172 ),
173 None => format!(
174 " Returns an [`iced_widget::Text`] widget of the {} {} icon.",
175 c, processed_name
176 ),
177 };
178
179 let shaping = match shaping {
180 "basic" => {
181 quote! { text::Shaping::Basic }
182 }
183 "advanced" => {
184 quote! { text::Shaping::Advanced }
185 }
186 _ => panic!(
187 "Shaping either needs to be basic or advanced, if you are unsure use advanced."
188 ),
189 };
190
191 functions.extend(quote! {
192 #[doc = #doc]
193 #[must_use]
194 pub fn #fn_name<'a, Theme: Catalog + 'a, Renderer: text::Renderer<Font = Font>>() -> Text<'a, Theme, Renderer> {
195 use iced_widget::text;
196 text(#c).font(#font_name).shaping(#shaping)
197 }
198 });
199
200 let doc = format!(
201 " Returns the [`String`] of {} character for lower level API's",
202 processed_name
203 );
204 advanced_functions.extend(quote! {
205 #[doc = #doc]
206 #[must_use]
207 pub fn #fn_name() -> (String, Font, Shaping) {
208 (#c.to_string(), #font_name, #shaping)
209 }
210 });
211
212 count += 1;
213 }
214 }
215
216 #[cfg(feature = "_generate_demo")]
217 println!("We have {} icons", count);
218
219 let advanced_text_tokens = if cfg!(feature = "advanced_text") {
220 quote! {
221 pub mod advanced_text {
235 use iced_widget::core::Font;
236 use iced_widget::text::{self, Shaping};
237 use crate::#font_name;
238
239 #advanced_functions
240 }
241 }
242 } else {
243 quote! {}
244 };
245
246 let count_lit = LitInt::new(&count.to_string(), Span::call_site());
247 let doc = format!(
248 "A module with a function for every icon in {}'s font.",
249 module_name.to_string()
250 );
251 TokenStream::from(quote! {
252 #[doc = #doc]
253 pub mod #module_name {
254 use iced_widget::core::text;
255 use iced_widget::core::Font;
256 use iced_widget::text::Text;
257 use iced_widget::text::Catalog;
258 use crate::#font_name;
259
260 pub const COUNT: usize = #count_lit;
262
263 #functions
264
265 #advanced_text_tokens
266
267 }
268 })
269}