Skip to main content

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