slint_macros/
lib.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell:ignore punct
5
6#![doc = include_str!("README.md")]
7#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
8
9extern crate proc_macro;
10
11use i_slint_compiler::diagnostics::BuildDiagnostics;
12use i_slint_compiler::parser::SyntaxKind;
13use i_slint_compiler::*;
14use proc_macro::{Spacing, TokenStream, TokenTree};
15use quote::quote;
16use std::path::PathBuf;
17
18/// Returns true if the two token are touching. For example the two token `foo`and `-` are touching if
19/// it was written like so in the source code: `foo-` but not when written like so `foo -`
20///
21/// Returns None if we couldn't detect whether they are touching  (eg, our heuristics don't work with rust-analyzer)
22fn are_token_touching(token1: proc_macro::Span, token2: proc_macro::Span) -> Option<bool> {
23    let t1 = token1.end();
24    let t2 = token2.start();
25    let t1_column = t1.column();
26    if t1_column == 1 && t1.line() == 1 && t2.end().line() == 1 && t2.end().column() == 1 {
27        // If everything is 1, this means that Span::line and Span::column are not working properly
28        // (eg, rust-analyzer)
29        return None;
30    }
31    Some(t1.line() == t2.line() && t1_column == t2.column())
32}
33
34fn fill_token_vec(stream: impl Iterator<Item = TokenTree>, vec: &mut Vec<parser::Token>) {
35    let mut prev_spacing = Spacing::Alone;
36    let mut prev_span = proc_macro::Span::call_site();
37    for t in stream {
38        let span = t.span();
39        match t {
40            TokenTree::Ident(i) => {
41                if let Some(last) = vec.last_mut() {
42                    if (last.kind == SyntaxKind::ColorLiteral && last.text.len() == 1)
43                        || (last.kind == SyntaxKind::Identifier
44                            && are_token_touching(prev_span, span)
45                                .unwrap_or_else(|| last.text.ends_with('-')))
46                    {
47                        last.text = format!("{}{}", last.text, i).into();
48                        prev_span = span;
49                        continue;
50                    }
51                }
52                vec.push(parser::Token {
53                    kind: SyntaxKind::Identifier,
54                    text: i.to_string().into(),
55                    span: Some(i.span()),
56                    ..Default::default()
57                });
58            }
59            TokenTree::Punct(p) => {
60                let kind = match p.as_char() {
61                    ':' => SyntaxKind::Colon,
62                    '=' => {
63                        if let Some(last) = vec.last_mut() {
64                            let kt = match last.kind {
65                                SyntaxKind::Star => Some((SyntaxKind::StarEqual, "*=")),
66                                SyntaxKind::Colon => Some((SyntaxKind::ColonEqual, ":=")),
67                                SyntaxKind::Plus => Some((SyntaxKind::PlusEqual, "+=")),
68                                SyntaxKind::Minus => Some((SyntaxKind::MinusEqual, "-=")),
69                                SyntaxKind::Div => Some((SyntaxKind::DivEqual, "/=")),
70                                SyntaxKind::LAngle => Some((SyntaxKind::LessEqual, "<=")),
71                                SyntaxKind::RAngle => Some((SyntaxKind::GreaterEqual, ">=")),
72                                SyntaxKind::Equal => Some((SyntaxKind::EqualEqual, "==")),
73                                SyntaxKind::Bang => Some((SyntaxKind::NotEqual, "!=")),
74                                _ => None,
75                            };
76                            if let Some((k, t)) = kt {
77                                if prev_spacing == Spacing::Joint {
78                                    last.kind = k;
79                                    last.text = t.into();
80                                    continue;
81                                }
82                            }
83                        }
84                        SyntaxKind::Equal
85                    }
86                    ';' => SyntaxKind::Semicolon,
87                    '!' => SyntaxKind::Bang,
88                    '.' => {
89                        // `4..log` is lexed as `4 . . log` in rust, but should be `4. . log` in slint
90                        if let Some(last) = vec.last_mut() {
91                            if last.kind == SyntaxKind::NumberLiteral
92                                && are_token_touching(prev_span, p.span()).unwrap_or(false)
93                                && !last.text.contains('.')
94                                && !last.text.ends_with(char::is_alphabetic)
95                            {
96                                last.text = format!("{}.", last.text).into();
97                                prev_span = span;
98                                continue;
99                            }
100                        }
101                        SyntaxKind::Dot
102                    }
103                    '+' => SyntaxKind::Plus,
104                    '-' => {
105                        if let Some(last) = vec.last_mut() {
106                            if last.kind == SyntaxKind::Identifier
107                                && are_token_touching(prev_span, p.span()).unwrap_or(true)
108                            {
109                                last.text = format!("{}-", last.text).into();
110                                prev_span = span;
111                                continue;
112                            }
113                        }
114                        SyntaxKind::Minus
115                    }
116                    '*' => SyntaxKind::Star,
117                    '/' => SyntaxKind::Div,
118                    '<' => SyntaxKind::LAngle,
119                    '>' => {
120                        if let Some(last) = vec.last_mut() {
121                            if last.kind == SyntaxKind::LessEqual && prev_spacing == Spacing::Joint
122                            {
123                                last.kind = SyntaxKind::DoubleArrow;
124                                last.text = "<=>".into();
125                                continue;
126                            } else if last.kind == SyntaxKind::Equal
127                                && prev_spacing == Spacing::Joint
128                            {
129                                last.kind = SyntaxKind::FatArrow;
130                                last.text = "=>".into();
131                                continue;
132                            } else if last.kind == SyntaxKind::Minus
133                                && prev_spacing == Spacing::Joint
134                            {
135                                last.kind = SyntaxKind::Arrow;
136                                last.text = "->".into();
137                                continue;
138                            }
139                        }
140                        SyntaxKind::RAngle
141                    }
142                    '#' => SyntaxKind::ColorLiteral,
143                    '?' => SyntaxKind::Question,
144                    ',' => SyntaxKind::Comma,
145                    '&' => {
146                        // Since the '&' alone does not exist or cannot be part of any other token that &&
147                        // just consider it as '&&' and skip the joint ones.  FIXME. do that properly
148                        if let Some(last) = vec.last_mut() {
149                            if last.kind == SyntaxKind::AndAnd && prev_spacing == Spacing::Joint {
150                                continue;
151                            }
152                        }
153                        SyntaxKind::AndAnd
154                    }
155                    '|' => {
156                        // Since the '|' alone does not exist or cannot be part of any other token that ||
157                        // just consider it as '||' and skip the joint ones.
158                        if let Some(last) = vec.last_mut() {
159                            if last.kind == SyntaxKind::Pipe && prev_spacing == Spacing::Joint {
160                                last.kind = SyntaxKind::OrOr;
161                                continue;
162                            }
163                        }
164                        SyntaxKind::Pipe
165                    }
166                    '%' => {
167                        // handle % as a unit
168                        if let Some(last) = vec.last_mut() {
169                            if last.kind == SyntaxKind::NumberLiteral {
170                                last.text = format!("{}%", last.text).into();
171                                continue;
172                            }
173                        }
174                        SyntaxKind::Percent
175                    }
176                    '$' => SyntaxKind::Dollar,
177                    '@' => SyntaxKind::At,
178                    _ => SyntaxKind::Error,
179                };
180                prev_spacing = p.spacing();
181                vec.push(parser::Token {
182                    kind,
183                    text: p.to_string().into(),
184                    span: Some(p.span()),
185                    ..Default::default()
186                });
187            }
188            TokenTree::Literal(l) => {
189                let s = l.to_string();
190                // Why can't the rust API give me the type of the literal
191                let f = s.chars().next().unwrap();
192                let kind = if f == '"' {
193                    SyntaxKind::StringLiteral
194                } else if f.is_ascii_digit() {
195                    if let Some(last) = vec.last_mut() {
196                        if (last.kind == SyntaxKind::ColorLiteral && last.text.len() == 1)
197                            || (last.kind == SyntaxKind::Identifier
198                                && are_token_touching(prev_span, span)
199                                    .unwrap_or_else(|| last.text.ends_with('-')))
200                        {
201                            last.text = format!("{}{}", last.text, s).into();
202                            prev_span = span;
203                            continue;
204                        }
205                    }
206                    SyntaxKind::NumberLiteral
207                } else {
208                    SyntaxKind::Error
209                };
210                vec.push(parser::Token {
211                    kind,
212                    text: s.into(),
213                    span: Some(l.span()),
214                    ..Default::default()
215                });
216            }
217            TokenTree::Group(g) => {
218                use proc_macro::Delimiter::*;
219                use SyntaxKind::*;
220                let (l, r, sl, sr) = match g.delimiter() {
221                    Parenthesis => (LParent, RParent, "(", ")"),
222                    Brace => (LBrace, RBrace, "{", "}"),
223                    Bracket => (LBracket, RBracket, "[", "]"),
224                    None => todo!(),
225                };
226                vec.push(parser::Token {
227                    kind: l,
228                    text: sl.into(),
229                    span: Some(g.span()), // span_open is not stable
230                    ..Default::default()
231                });
232                fill_token_vec(g.stream().into_iter(), vec);
233                vec.push(parser::Token {
234                    kind: r,
235                    text: sr.into(),
236                    span: Some(g.span()), // span_clone is not stable
237                    ..Default::default()
238                });
239            }
240        }
241        prev_span = span;
242    }
243}
244
245fn extract_path(literal: proc_macro::Literal) -> std::path::PathBuf {
246    let path_with_quotes = literal.to_string();
247    let path_with_quotes_stripped = if let Some(p) = path_with_quotes.strip_prefix('r') {
248        let hash_removed = p.trim_matches('#');
249        hash_removed.strip_prefix('\"').unwrap().strip_suffix('\"').unwrap()
250    } else {
251        // FIXME: unescape
252        path_with_quotes.trim_matches('\"')
253    };
254    path_with_quotes_stripped.into()
255}
256
257fn extract_compiler_config(
258    mut stream: proc_macro::token_stream::IntoIter,
259    compiler_config: &mut CompilerConfiguration,
260) -> impl Iterator<Item = TokenTree> {
261    let mut remaining_stream;
262    loop {
263        remaining_stream = stream.clone();
264        match (stream.next(), stream.next()) {
265            (Some(TokenTree::Punct(p)), Some(TokenTree::Group(group)))
266                if p.as_char() == '#' && group.delimiter() == proc_macro::Delimiter::Bracket =>
267            {
268                let mut attr_stream = group.stream().into_iter();
269                match attr_stream.next() {
270                    Some(TokenTree::Ident(include_ident))
271                        if include_ident.to_string() == "include_path" =>
272                    {
273                        match (attr_stream.next(), attr_stream.next()) {
274                            (
275                                Some(TokenTree::Punct(equal_punct)),
276                                Some(TokenTree::Literal(path)),
277                            ) if equal_punct.as_char() == '=' => {
278                                compiler_config.include_paths.push(extract_path(path));
279                            }
280                            _ => break,
281                        }
282                    }
283                    Some(TokenTree::Ident(library_ident))
284                        if library_ident.to_string() == "library_path" =>
285                    {
286                        match (attr_stream.next(), attr_stream.next(), attr_stream.next()) {
287                            (
288                                Some(TokenTree::Group(group)),
289                                Some(TokenTree::Punct(equal_punct)),
290                                Some(TokenTree::Literal(path)),
291                            ) if group.delimiter() == proc_macro::Delimiter::Parenthesis
292                                && equal_punct.as_char() == '=' =>
293                            {
294                                let library_name = group.stream().into_iter().next().unwrap();
295                                compiler_config
296                                    .library_paths
297                                    .insert(library_name.to_string(), extract_path(path));
298                            }
299                            _ => break,
300                        }
301                    }
302                    Some(TokenTree::Ident(style_ident)) if style_ident.to_string() == "style" => {
303                        match (attr_stream.next(), attr_stream.next()) {
304                            (
305                                Some(TokenTree::Punct(equal_punct)),
306                                Some(TokenTree::Literal(requested_style)),
307                            ) if equal_punct.as_char() == '=' => {
308                                compiler_config.style = requested_style
309                                    .to_string()
310                                    .strip_prefix('\"')
311                                    .unwrap()
312                                    .strip_suffix('\"')
313                                    .unwrap()
314                                    .to_string()
315                                    .into();
316                            }
317                            _ => break,
318                        }
319                    }
320                    _ => break,
321                }
322            }
323            _ => break,
324        }
325    }
326    remaining_stream
327}
328
329/// This macro allows you to use the Slint design markup language inline in Rust code. Within the braces of the macro
330/// you can use place Slint code and the named exported components will be available for instantiation.
331///
332/// For the documentation about the syntax of the language, see
333#[doc = concat!("[The Slint Language Documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint)")]
334///
335/// When Rust 1.88 or later is used, the paths for loading images with `@image-url` and importing `.slint` files
336/// are relative to the `.rs` file that contains the macro.
337/// For compatibility with older rust version, the files are also searched in the manifest directory that contains `Cargo.toml`.
338///
339/// ### Limitations
340///
341/// Within `.slint` files, you can interpolate string literals using `\{...}` syntax.
342/// This is not possible in this macro as this wouldn't parse as a Rust string.
343#[proc_macro]
344pub fn slint(stream: TokenStream) -> TokenStream {
345    let token_iter = stream.into_iter();
346
347    let mut compiler_config =
348        CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust);
349
350    let token_iter = extract_compiler_config(token_iter, &mut compiler_config);
351
352    let mut tokens = vec![];
353    fill_token_vec(token_iter, &mut tokens);
354
355    fn local_file(tokens: &[parser::Token]) -> Option<PathBuf> {
356        tokens.first()?.span?.local_file()
357    }
358
359    let source_file = if let Some(path) = local_file(&tokens) {
360        diagnostics::SourceFileInner::from_path_only(path)
361    } else if let Some(cargo_manifest) = std::env::var_os("CARGO_MANIFEST_DIR") {
362        let mut path: std::path::PathBuf = cargo_manifest.into();
363        path.push("Cargo.toml");
364        diagnostics::SourceFileInner::from_path_only(path)
365    } else {
366        diagnostics::SourceFileInner::from_path_only(Default::default())
367    };
368    let mut diag = BuildDiagnostics::default();
369    let syntax_node = parser::parse_tokens(tokens.clone(), source_file, &mut diag);
370    if diag.has_errors() {
371        return diag.report_macro_diagnostic(&tokens);
372    }
373
374    //println!("{syntax_node:#?}");
375    compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
376    let (root_component, diag, loader) =
377        spin_on::spin_on(compile_syntax_node(syntax_node, diag, compiler_config));
378    //println!("{tree:#?}");
379    if diag.has_errors() {
380        return diag.report_macro_diagnostic(&tokens);
381    }
382
383    let mut result = generator::rust::generate(&root_component, &loader.compiler_config)
384        .unwrap_or_else(|e| {
385            let e_str = e.to_string();
386            quote!(compile_error!(#e_str))
387        });
388
389    // Make sure to recompile if any of the external files changes
390    let reload = diag
391        .all_loaded_files
392        .iter()
393        .filter(|path| path.is_absolute() && !path.ends_with("Cargo.toml"))
394        .filter_map(|p| p.to_str())
395        .map(|p| quote! {const _ : &'static [u8] = ::core::include_bytes!(#p);});
396
397    result.extend(reload);
398    result.extend(quote! {const _ : ::core::option::Option<&'static str> = ::core::option_env!("SLINT_STYLE");});
399
400    let mut result = TokenStream::from(result);
401    if !diag.is_empty() {
402        result.extend(diag.report_macro_diagnostic(&tokens));
403    }
404    result
405}