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