1use proc_macro::TokenStream;
2use quote::quote;
3use std::collections::HashMap;
4use syn::{Error, Ident, LitStr, Token, parse_macro_input};
5
6#[proc_macro]
76pub fn include_xrc(input: TokenStream) -> TokenStream {
77 let input = parse_macro_input!(input as XrcMacroInput);
78
79 match generate_xrc_struct(input) {
80 Ok(tokens) => tokens.into(),
81 Err(err) => err.to_compile_error().into(),
82 }
83}
84
85struct XrcMacroInput {
87 xrc_path: String,
88 struct_name: Ident,
89}
90
91impl syn::parse::Parse for XrcMacroInput {
92 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
93 let xrc_path: LitStr = input.parse()?;
95 input.parse::<Token![,]>()?;
96
97 let struct_name: Ident = input.parse()?;
99
100 Ok(XrcMacroInput {
103 xrc_path: xrc_path.value(),
104 struct_name,
105 })
106 }
107}
108
109#[derive(Debug, Clone)]
111struct XrcObject {
112 name: String,
113 class: String,
114 children: Vec<XrcObject>,
115}
116
117fn get_class_mapping() -> HashMap<&'static str, &'static str> {
119 let mut map = HashMap::new();
120
121 map.insert("wxDialog", "wxdragon::dialogs::Dialog");
123 map.insert("wxFrame", "wxdragon::widgets::Frame");
124 map.insert("wxPanel", "wxdragon::widgets::Panel");
125
126 map.insert("wxButton", "wxdragon::widgets::Button");
128 map.insert("wxCheckBox", "wxdragon::widgets::CheckBox");
129 map.insert("wxComboBox", "wxdragon::widgets::ComboBox");
130 map.insert("wxTextCtrl", "wxdragon::widgets::TextCtrl");
131 map.insert("wxStaticText", "wxdragon::widgets::StaticText");
132 map.insert("wxListBox", "wxdragon::widgets::ListBox");
133 map.insert("wxChoice", "wxdragon::widgets::Choice");
134 map.insert("wxSlider", "wxdragon::widgets::Slider");
135 map.insert("wxGauge", "wxdragon::widgets::Gauge");
136 map.insert("wxSpinCtrl", "wxdragon::widgets::SpinCtrl");
137 map.insert("wxSpinCtrlDouble", "wxdragon::widgets::SpinCtrlDouble");
138 map.insert("wxSpinButton", "wxdragon::widgets::SpinButton");
139 map.insert("wxTreeCtrl", "wxdragon::widgets::TreeCtrl");
140 map.insert("wxTreeListCtrl", "wxdragon::widgets::TreeListCtrl");
141 map.insert("wxNotebook", "wxdragon::widgets::Notebook");
142 map.insert("wxSimplebook", "wxdragon::widgets::SimpleBook");
143 map.insert("wxRadioButton", "wxdragon::widgets::RadioButton");
144 map.insert("wxRadioBox", "wxdragon::widgets::RadioBox");
145 map.insert("wxToggleButton", "wxdragon::widgets::ToggleButton");
146 map.insert("wxBitmapButton", "wxdragon::widgets::BitmapButton");
147 map.insert("wxBitmapToggleButton", "wxdragon::widgets::BitmapToggleButton");
148
149 map.insert("wxStaticBitmap", "wxdragon::widgets::StaticBitmap");
154
155 map.insert("wxStaticLine", "wxdragon::widgets::StaticLine");
156 map.insert("wxStaticBox", "wxdragon::widgets::StaticBox");
157 map.insert("wxScrollBar", "wxdragon::widgets::ScrollBar");
158 map.insert("wxScrolledWindow", "wxdragon::widgets::ScrolledWindow");
159 map.insert("wxSplitterWindow", "wxdragon::widgets::SplitterWindow");
160 map.insert("wxCollapsiblePane", "wxdragon::widgets::CollapsiblePane");
161 map.insert("wxCheckListBox", "wxdragon::widgets::CheckListBox");
162 map.insert("wxRearrangeList", "wxdragon::widgets::RearrangeList");
163 map.insert("wxTreebook", "wxdragon::widgets::Treebook");
164 map.insert("wxListCtrl", "wxdragon::widgets::ListCtrl");
165 map.insert("wxGrid", "wxdragon::widgets::Grid");
166 map.insert("wxHyperlinkCtrl", "wxdragon::widgets::HyperlinkCtrl");
167 map.insert("wxSearchCtrl", "wxdragon::widgets::SearchCtrl");
168 map.insert("wxStyledTextCtrl", "wxdragon::widgets::StyledTextCtrl");
169 map.insert("wxActivityIndicator", "wxdragon::widgets::ActivityIndicator");
170 map.insert("wxAnimationCtrl", "wxdragon::widgets::AnimationCtrl");
171 map.insert("wxBitmapComboBox", "wxdragon::widgets::BitmapComboBox");
172 map.insert("wxCalendarCtrl", "wxdragon::widgets::CalendarCtrl");
173 map.insert("wxColourPickerCtrl", "wxdragon::widgets::ColourPickerCtrl");
174 map.insert("wxCommandLinkButton", "wxdragon::widgets::CommandLinkButton");
175 map.insert("wxDatePickerCtrl", "wxdragon::widgets::DatePickerCtrl");
176 map.insert("wxDirPickerCtrl", "wxdragon::widgets::DirPickerCtrl");
177 map.insert("wxEditableListBox", "wxdragon::widgets::EditableListBox");
178 map.insert("wxFileCtrl", "wxdragon::widgets::FileCtrl");
179 map.insert("wxFilePickerCtrl", "wxdragon::widgets::FilePickerCtrl");
180 map.insert("wxFontPickerCtrl", "wxdragon::widgets::FontPickerCtrl");
181 map.insert("wxMediaCtrl", "wxdragon::widgets::MediaCtrl");
182 map.insert("wxTimePickerCtrl", "wxdragon::widgets::TimePickerCtrl");
183 map.insert("wxRichTextCtrl", "wxdragon::widgets::RichTextCtrl");
184 map.insert("wxAuiManager", "wxdragon::widgets::AuiManager");
188 map.insert("wxAuiNotebook", "wxdragon::widgets::AuiNotebook");
189 map.insert("wxAuiToolBar", "wxdragon::widgets::AuiToolBar");
190 map.insert("wxAuiMDIParentFrame", "wxdragon::widgets::AuiMDIParentFrame");
191 map.insert("wxAuiMDIChildFrame", "wxdragon::widgets::AuiMDIChildFrame");
192
193 map.insert("wxToolBar", "wxdragon::widgets::ToolBar");
195 map.insert("wxStatusBar", "wxdragon::widgets::StatusBar");
196
197 map.insert("tool", "wxdragon::widgets::Tool");
199
200 map.insert("wxMenuBar", "wxdragon::menus::MenuBar");
202 map.insert("wxMenu", "wxdragon::menus::Menu");
203 map.insert("wxMenuItem", "wxdragon::menus::MenuItem");
204
205 map.insert("unknown", "wxdragon::window::Window");
207
208 map
209}
210
211fn generate_xrc_struct(input: XrcMacroInput) -> syn::Result<proc_macro2::TokenStream> {
213 let xrc_content = read_xrc_file(&input.xrc_path)?;
215 let xrc_objects = parse_xrc_content(&xrc_content)?;
216
217 let root_object = find_root_object(&xrc_objects)?;
219
220 let mut all_objects = Vec::new();
222 collect_named_objects(root_object, &mut all_objects);
223
224 let widget_objects: Vec<_> = all_objects
226 .into_iter()
227 .filter(|obj| {
228 !obj.class.contains("Sizer") && !obj.class.contains("sizeritem") && !obj.class.contains("spacer")
229 && obj.class != "wxMenu"
231 })
232 .collect();
233
234 let (tool_objects, remaining_objects): (Vec<_>, Vec<_>) = widget_objects.iter().partition(|obj| obj.class == "tool");
236
237 let (menu_item_objects, remaining_objects2): (Vec<_>, Vec<_>) =
238 remaining_objects.into_iter().partition(|obj| obj.class == "wxMenuItem");
239
240 let (menubar_objects, non_special_objects): (Vec<_>, Vec<_>) =
241 remaining_objects2.into_iter().partition(|obj| obj.class == "wxMenuBar");
242
243 let struct_name = &input.struct_name;
245 let class_mapping = get_class_mapping();
246
247 let struct_fields = widget_objects.iter().map(|obj| {
249 let field_name = Ident::new(&obj.name, proc_macro2::Span::call_site());
250 let type_str = class_mapping.get(obj.class.as_str()).unwrap_or(&"wxdragon::window::Window");
251
252 let field_type: syn::Type = syn::parse_str(type_str).unwrap();
253 quote! { pub #field_name: #field_type }
254 });
255
256 let root_load_method = match root_object.class.as_str() {
258 "wxDialog" => quote! { load_dialog },
259 "wxFrame" => quote! { load_frame },
260 "wxPanel" => quote! { load_panel },
261 _ => quote! { load_frame }, };
263
264 let root_field_name = Ident::new(&root_object.name, proc_macro2::Span::call_site());
265 let xrc_path = &input.xrc_path;
266
267 let non_special_initializers = non_special_objects.iter().map(|obj| {
269 let field_name = Ident::new(&obj.name, proc_macro2::Span::call_site());
270 let obj_name_lit = &obj.name;
271
272 if obj.name == root_object.name {
273 quote! {
275 let #field_name = resource.#root_load_method(parent, #obj_name_lit)
276 .unwrap_or_else(|| panic!("Failed to load XRC root object: {}", #obj_name_lit));
277 }
278 } else {
279 let type_str = class_mapping.get(obj.class.as_str()).unwrap_or(&"wxdragon::window::Window");
281 let widget_type: syn::Type = syn::parse_str(type_str).unwrap();
282
283 quote! {
284 let #field_name = #root_field_name
285 .find_child_by_xrc_name::<#widget_type>(#obj_name_lit)
286 .unwrap_or_else(|| panic!("Failed to find XRC child: {}", #obj_name_lit));
287 }
288 }
289 });
290
291 let menubar_initializers = menubar_objects.iter().map(|menubar_obj| {
293 let field_name = Ident::new(&menubar_obj.name, proc_macro2::Span::call_site());
294 let _menubar_name_lit = &menubar_obj.name;
295
296 quote! {
297 let #field_name = #root_field_name.get_menu_bar()
298 .unwrap_or_else(|| panic!("Failed to get MenuBar from Frame"));
299 }
300 });
301
302 let tool_initializers = tool_objects.iter().map(|tool_obj| {
304 let field_name = Ident::new(&tool_obj.name, proc_macro2::Span::call_site());
305 let tool_name_lit = &tool_obj.name;
306
307 let toolbar_name = root_object
309 .children
310 .iter()
311 .find_map(|child| find_toolbar_parent_for_tool(child, &tool_obj.name))
312 .or_else(|| find_toolbar_parent_for_tool(root_object, &tool_obj.name))
313 .unwrap_or("main_toolbar");
314 let toolbar_field = Ident::new(toolbar_name, proc_macro2::Span::call_site());
315
316 quote! {
317 let #field_name = #toolbar_field.get_tool_by_name(#tool_name_lit)
318 .unwrap_or_else(|| panic!("Failed to find tool: {}", #tool_name_lit));
319 }
320 });
321
322 let menu_item_initializers = menu_item_objects.iter().map(|menu_item_obj| {
324 let field_name = Ident::new(&menu_item_obj.name, proc_macro2::Span::call_site());
325 let menu_item_name_lit = &menu_item_obj.name;
326
327 quote! {
328 let #field_name = wxdragon::menus::MenuItem::from_xrc_name(#root_field_name.window_handle(), #menu_item_name_lit)
329 .unwrap_or_else(|| panic!("Failed to find menu item: {}", #menu_item_name_lit));
330 }
331 });
332
333 let field_assignments = widget_objects.iter().map(|obj| {
334 let field_name = Ident::new(&obj.name, proc_macro2::Span::call_site());
335 quote! { #field_name }
336 });
337
338 let drop_impl = quote! {
340 impl Drop for #struct_name {
341 fn drop(&mut self) {
342 if self.auto_destroy_root {
345 self.#root_field_name.destroy();
347 }
348 }
349 }
350 };
351
352 let generated = quote! {
353 #[allow(non_snake_case)]
354 pub struct #struct_name {
355 #(#struct_fields,)*
356 auto_destroy_root: bool,
357 _resource: wxdragon::xrc::XmlResource,
358 }
359
360 impl #struct_name {
361 pub const XRC_DATA: &'static str = include_str!(#xrc_path);
363
364 pub fn new(parent: Option<&dyn wxdragon::window::WxWidget>, auto_destroy_root: bool) -> Self {
366 let resource = wxdragon::xrc::XmlResource::get();
367
368 resource.init_platform_aware_staticbitmap_handler();
371
372 resource.init_all_handlers();
373
374 resource.load_from_string(Self::XRC_DATA)
375 .unwrap_or_else(|err| panic!("Failed to load XRC data: {}", err));
376
377 #(#non_special_initializers)*
378
379 #(#menubar_initializers)*
381
382 #(#tool_initializers)*
384
385 #(#menu_item_initializers)*
387
388 Self {
389 #(#field_assignments,)*
390 auto_destroy_root,
391 _resource: resource,
392 }
393 }
394
395 pub fn xrc_id(name: &str) -> i32 {
397 wxdragon::xrc::XmlResource::get_xrc_id(name)
398 }
399 }
400
401 #drop_impl
402 };
403
404 Ok(generated)
405}
406
407fn read_xrc_file(path: &str) -> syn::Result<String> {
409 let possible_paths = vec![
416 std::path::PathBuf::from(path),
418 std::env::current_dir()
420 .map(|cwd| cwd.join(path))
421 .unwrap_or_else(|_| std::path::PathBuf::from(path)),
422 if path.starts_with("../") {
424 std::env::current_dir()
425 .map(|cwd| cwd.join("src").join(path))
426 .unwrap_or_else(|_| std::path::PathBuf::from(path))
427 } else {
428 std::path::PathBuf::from(path)
429 },
430 std::env::var("CARGO_MANIFEST_DIR")
432 .map(|manifest_dir| std::path::PathBuf::from(manifest_dir).join(path))
433 .unwrap_or_else(|_| std::path::PathBuf::from(path)),
434 std::env::var("CARGO_MANIFEST_DIR")
436 .map(|manifest_dir| {
437 if path.starts_with("../") {
438 std::path::PathBuf::from(manifest_dir).join(path.trim_start_matches("../"))
439 } else {
440 std::path::PathBuf::from(manifest_dir).join("src").join(path)
441 }
442 })
443 .unwrap_or_else(|_| std::path::PathBuf::from(path)),
444 ];
445
446 for full_path in &possible_paths {
447 if let Ok(content) = std::fs::read_to_string(full_path) {
448 return Ok(content);
449 }
450 }
451
452 Err(Error::new(
454 proc_macro2::Span::call_site(),
455 format!(
456 "Failed to read XRC file '{}' for macro analysis. \
457 The file will be embedded using include_str! at compile time, \
458 but the macro needs to read it now to generate widget fields. \
459 \nTried paths: {:?} \
460 \n\nNote: Use paths relative to your crate root or source file, just like include_str!",
461 path,
462 possible_paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>()
463 ),
464 ))
465}
466
467fn parse_xrc_content(content: &str) -> syn::Result<Vec<XrcObject>> {
469 use quick_xml::Reader;
470 use quick_xml::events::Event;
471
472 let mut reader = Reader::from_str(content);
473 reader.config_mut().trim_text(true);
474
475 let mut objects = Vec::new();
476 let mut stack = Vec::new();
477
478 loop {
479 match reader.read_event() {
480 Ok(Event::Start(ref e)) if e.name().as_ref() == b"object" => {
481 let mut obj = XrcObject {
482 name: String::new(),
483 class: String::new(),
484 children: Vec::new(),
485 };
486
487 for attr in e.attributes() {
489 let attr = attr.map_err(|e| Error::new(proc_macro2::Span::call_site(), format!("XML parsing error: {e}")))?;
490
491 match attr.key.as_ref() {
492 b"name" => obj.name = String::from_utf8_lossy(&attr.value).into_owned(),
493 b"class" => obj.class = String::from_utf8_lossy(&attr.value).into_owned(),
494 _ => {}
495 }
496 }
497
498 stack.push(obj);
499 }
500 Ok(Event::End(ref e)) if e.name().as_ref() == b"object" => {
501 if let Some(obj) = stack.pop() {
502 if let Some(parent) = stack.last_mut() {
503 parent.children.push(obj);
506 } else {
507 if !obj.name.is_empty() || !obj.children.is_empty() {
509 objects.push(obj);
510 }
511 }
512 }
513 }
514 Ok(Event::Eof) => break,
515 Err(e) => {
516 return Err(Error::new(proc_macro2::Span::call_site(), format!("XML parsing error: {e}")));
517 }
518 _ => {}
519 }
520 }
521
522 Ok(objects)
523}
524
525fn find_root_object(objects: &[XrcObject]) -> syn::Result<&XrcObject> {
527 objects
529 .iter()
530 .find(|obj| {
531 obj.class == "wxFrame" || obj.class == "wxDialog" || obj.class == "wxPanel"
532 })
533 .ok_or_else(|| {
534 Error::new(
535 proc_macro2::Span::call_site(),
536 "No root Frame, Dialog, or Panel object found in XRC. Make sure your XRC file contains a top-level wxFrame, wxDialog, or wxPanel object.",
537 )
538 })
539}
540
541fn collect_named_objects(obj: &XrcObject, result: &mut Vec<XrcObject>) {
543 if !obj.name.is_empty() {
544 result.push(obj.clone());
545 }
546
547 for child in &obj.children {
548 collect_named_objects(child, result);
549 }
550}
551
552fn find_toolbar_parent_for_tool<'a>(obj: &'a XrcObject, tool_name: &str) -> Option<&'a str> {
555 let is_toolbar = obj.class == "wxToolBar" || obj.class == "wxAuiToolBar";
556
557 for child in &obj.children {
558 if child.class == "tool" && child.name == tool_name && is_toolbar && !obj.name.is_empty() {
559 return Some(&obj.name);
560 }
561 if let Some(parent_name) = find_toolbar_parent_for_tool(child, tool_name) {
562 return Some(parent_name);
563 }
564 }
565
566 None
567}