Skip to main content

gtk4_macros/
composite_template_derive.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3#[cfg(feature = "xml_validation")]
4use std::collections::HashMap;
5
6use proc_macro2::{Span, TokenStream};
7#[cfg(feature = "xml_validation")]
8use quick_xml::name::QName;
9use quote::quote;
10use syn::{Data, Error, Result};
11
12#[cfg(feature = "blueprint")]
13use crate::blueprint::*;
14use crate::{attribute_parser::*, util::*};
15
16fn gen_set_template(source: &TemplateSource, crate_ident: &proc_macro2::Ident) -> TokenStream {
17    match source {
18        TemplateSource::File(file) => {
19            let template = if file.ends_with(".blp") {
20                if cfg!(feature = "blueprint") {
21                    let Some(local_file) = Span::call_site().local_file() else {
22                        return Error::new(Span::call_site(), "File not found")
23                            .into_compile_error();
24                    };
25                    let local_file = local_file.to_str();
26                    quote! {
27                        #crate_ident::gtk4_macros::include_blueprint!(#local_file, #file).as_bytes()
28                    }
29                } else {
30                    panic!("blueprint feature is disabled")
31                }
32            } else {
33                quote! {
34                    include_bytes!(#file)
35                }
36            };
37
38            quote! {
39                #crate_ident::subclass::widget::WidgetClassExt::set_template_static(
40                        klass,
41                        #template,
42                );
43            }
44        }
45        TemplateSource::Resource(resource) => quote! {
46            #crate_ident::subclass::widget::WidgetClassExt::set_template_from_resource(
47                klass,
48                &#resource,
49            );
50        },
51        TemplateSource::Xml(template) => quote! {
52            #crate_ident::subclass::widget::WidgetClassExt::set_template_static(
53                klass,
54                #template.as_bytes(),
55            );
56        },
57        #[cfg(feature = "blueprint")]
58        TemplateSource::Blueprint(blueprint) => {
59            let template =
60                compile_blueprint(blueprint.as_bytes()).expect("can't compile blueprint");
61
62            quote! {
63                #crate_ident::subclass::widget::WidgetClassExt::set_template_static(
64                    klass,
65                    #template.as_bytes(),
66                );
67            }
68        }
69    }
70}
71
72#[cfg(feature = "xml_validation")]
73fn check_template_fields(source: &TemplateSource, fields: &[AttributedField]) -> Result<()> {
74    #[allow(unused_assignments)]
75    let xml = match source {
76        TemplateSource::Xml(template) => template,
77        _ => return Ok(()),
78    };
79
80    let mut reader = quick_xml::Reader::from_str(xml);
81    let mut ids_left = fields
82        .iter()
83        .map(|field| match field.attr.ty {
84            FieldAttributeType::TemplateChild => (field.id(), field.id_span()),
85        })
86        .collect::<HashMap<_, _>>();
87
88    loop {
89        use quick_xml::events::Event;
90
91        let event = reader.read_event();
92        let elem = match &event {
93            Ok(Event::Start(e)) => Some(e),
94            Ok(Event::Empty(e)) => Some(e),
95            Ok(Event::Eof) => break,
96            Err(e) => {
97                return Err(Error::new(
98                    Span::call_site(),
99                    format!(
100                        "Failed reading template XML at position {}: {:?}",
101                        reader.buffer_position(),
102                        e
103                    ),
104                ));
105            }
106            _ => None,
107        };
108        if let Some(e) = elem {
109            let name = e.name();
110            if name == QName(b"object") || name == QName(b"template") {
111                let id = e
112                    .attributes()
113                    .find_map(|a| a.ok().and_then(|a| (a.key == QName(b"id")).then_some(a)));
114                let id = id.as_ref().and_then(|a| std::str::from_utf8(&a.value).ok());
115                if let Some(id) = id {
116                    ids_left.remove(id);
117                }
118            }
119        }
120    }
121
122    if let Some((name, span)) = ids_left.into_iter().next() {
123        return Err(Error::new(
124            span,
125            format!("Template child with id `{name}` not found in template XML",),
126        ));
127    }
128
129    Ok(())
130}
131
132fn gen_template_child_bindings(fields: &[AttributedField]) -> TokenStream {
133    let crate_ident = crate_ident_new();
134
135    let recurse = fields.iter().map(|field| match field.attr.ty {
136        FieldAttributeType::TemplateChild => {
137            let mut value_id = None::<&str>;
138            let ident = &field.ident;
139            let mut value_internal = false;
140            field.attr.args.iter().for_each(|arg| match arg {
141                FieldAttributeArg::Id(value, _) => {
142                    value_id = Some(value);
143                }
144                FieldAttributeArg::Internal(internal) => {
145                    value_internal = *internal;
146                }
147            });
148
149            let value_id = if let Some(value_id) = value_id {
150                value_id
151            } else {
152                &ident.to_string()
153            };
154
155            quote! {
156                klass.bind_template_child_with_offset(
157                    &#value_id,
158                    #value_internal,
159                    #crate_ident::offset_of!(Self => #ident),
160                );
161            }
162        }
163    });
164
165    quote! {
166        #(#recurse)*
167    }
168}
169
170fn gen_template_child_type_checks(fields: &[AttributedField]) -> TokenStream {
171    let crate_ident = crate_ident_new();
172
173    let recurse = fields.iter().map(|field| match field.attr.ty {
174        FieldAttributeType::TemplateChild => {
175            let ty = &field.ty;
176            let ident = &field.ident;
177            let type_err = format!("Template child with id `{}` has incompatible type. XML has {{:?}}, struct has {{:?}}", field.id());
178            quote! {
179                let ty = <<#ty as ::std::ops::Deref>::Target as #crate_ident::glib::prelude::StaticType>::static_type();
180                let child_ty = #crate_ident::glib::prelude::ObjectExt::type_(::std::ops::Deref::deref(&imp.#ident));
181                if !child_ty.is_a(ty) {
182                    panic!(#type_err, child_ty, ty);
183                }
184            }
185        }
186    });
187
188    quote! {
189        #(#recurse)*
190    }
191}
192
193pub fn impl_composite_template(input: &syn::DeriveInput) -> Result<TokenStream> {
194    let name = &input.ident;
195    let crate_ident = crate_ident_new();
196
197    let template = match parse_template_source(input) {
198        Ok(v) => Some(v),
199        Err(e) => {
200            return Err(Error::new(
201                Span::call_site(),
202                format!(
203                    "{e}: derive(CompositeTemplate) requires #[template(...)] to specify 'file', 'resource', or 'string'"
204                ),
205            ));
206        }
207    };
208
209    let allow_without_attribute = template
210        .as_ref()
211        .map(|t| t.allow_template_child_without_attribute)
212        .unwrap_or(false);
213    let source = template.as_ref().map(|t| &t.source);
214
215    let set_template = source.map(|s| gen_set_template(s, &crate_ident));
216
217    let fields = match input.data {
218        Data::Struct(ref s) => Some(&s.fields),
219        _ => {
220            return Err(Error::new(
221                Span::call_site(),
222                "derive(CompositeTemplate) only supports structs",
223            ));
224        }
225    };
226
227    let attributed_fields = match fields.map(|f| parse_fields(f, allow_without_attribute)) {
228        Some(fields) => fields?,
229        None => vec![],
230    };
231
232    #[cfg(feature = "xml_validation")]
233    {
234        if let Some(source) = source {
235            check_template_fields(source, &attributed_fields)?;
236        }
237    }
238    let template_children = gen_template_child_bindings(&attributed_fields);
239    let checks = gen_template_child_type_checks(&attributed_fields);
240
241    Ok(quote! {
242        impl #crate_ident::subclass::widget::CompositeTemplate for #name {
243            fn bind_template(klass: &mut Self::Class) {
244                #set_template
245
246                unsafe {
247                    #template_children
248                }
249            }
250            fn check_template_children(widget: &<Self as #crate_ident::glib::subclass::prelude::ObjectSubclass>::Type) {
251                let imp = #crate_ident::subclass::prelude::ObjectSubclassIsExt::imp(widget);
252                #checks
253            }
254        }
255    })
256}