gtk4_macros/
composite_template_derive.rs1#[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}