iced_fonts_macros/
lib.rs

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    /// e.g. `"fonts/bootstrap-icons-new.ttf"`
16    font_path: LitStr,
17    /// e.g. `bootstrap`
18    module_name: Ident,
19    /// e.g. `"BOOTSTRAP_FONT"`
20    font_name: Ident,
21    /// e.g. `https://icons.getbootstrap.com/icons`
22    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        // It is good-mannered to accept an optional trailing comma
34        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/// Generates a module with functions that create text widgets.
48#[proc_macro]
49pub fn generate_icon_functions(input: TokenStream) -> TokenStream {
50    body(input, "basic")
51}
52
53/// Generates a module with functions that create text widgets with advanced shaping.
54#[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            // We need to rename some common characters.
104            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            // Material font edge case
118            if processed_name.as_str() == "_" {
119                processed_name = String::from("underscore");
120            }
121
122            // In case we have illegals. There are cases where most fonts have a .null icon that
123            // doesn't do anything. So we can safely filter it out with the rest
124            for c in processed_name.chars() {
125                match c {
126                    '+' | '-' | '*' | '/' | '@' | '!' | '#' | '$' | '%' | '^' | '&' | '(' | ')'
127                    | '=' | '~' | '`' | ';' | ':' | '"' | '\'' | ',' | '<' | '>' | '?' | '.'
128                    | ' ' | '[' | ']' | '{' | '}' | '|' | '\\' => continue 'outer,
129                    _ => {}
130                }
131            }
132
133            // Check for duplicates
134            match duplicates.get(&processed_name) {
135                Some(amount) => {
136                    duplicates.insert(processed_name.clone(), *amount + 1);
137                    // We don't care about repeats. Even though we should :(
138                    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          /// Every icon with helpers to use these icons in widgets.
222          ///
223          /// Usage
224          /// ```
225          /// let (content, font, shaping) = advanced_text::my_icon();
226          ///
227          /// advanced::Text {
228          ///     content,
229          ///     font,
230          ///     shaping,
231          ///     ...
232          /// }
233          /// ```
234          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            /// The amount of icons in the font.
261            pub const COUNT: usize = #count_lit;
262
263            #functions
264
265            #advanced_text_tokens
266
267        }
268    })
269}