use proc_macro::TokenStream;
use quote::quote;
use std::collections::HashMap;
use syn::{Error, Ident, LitStr, Token, parse_macro_input};
#[proc_macro]
pub fn include_xrc(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as XrcMacroInput);
match generate_xrc_struct(input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
struct XrcMacroInput {
xrc_path: String,
struct_name: Ident,
}
impl syn::parse::Parse for XrcMacroInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let xrc_path: LitStr = input.parse()?;
input.parse::<Token![,]>()?;
let struct_name: Ident = input.parse()?;
Ok(XrcMacroInput {
xrc_path: xrc_path.value(),
struct_name,
})
}
}
#[derive(Debug, Clone)]
struct XrcObject {
name: String,
class: String,
children: Vec<XrcObject>,
}
fn get_class_mapping() -> HashMap<&'static str, &'static str> {
let mut map = HashMap::new();
map.insert("wxDialog", "wxdragon::dialogs::Dialog");
map.insert("wxFrame", "wxdragon::widgets::Frame");
map.insert("wxPanel", "wxdragon::widgets::Panel");
map.insert("wxButton", "wxdragon::widgets::Button");
map.insert("wxCheckBox", "wxdragon::widgets::CheckBox");
map.insert("wxComboBox", "wxdragon::widgets::ComboBox");
map.insert("wxTextCtrl", "wxdragon::widgets::TextCtrl");
map.insert("wxStaticText", "wxdragon::widgets::StaticText");
map.insert("wxListBox", "wxdragon::widgets::ListBox");
map.insert("wxChoice", "wxdragon::widgets::Choice");
map.insert("wxSlider", "wxdragon::widgets::Slider");
map.insert("wxGauge", "wxdragon::widgets::Gauge");
map.insert("wxSpinCtrl", "wxdragon::widgets::SpinCtrl");
map.insert("wxSpinCtrlDouble", "wxdragon::widgets::SpinCtrlDouble");
map.insert("wxSpinButton", "wxdragon::widgets::SpinButton");
map.insert("wxTreeCtrl", "wxdragon::widgets::TreeCtrl");
map.insert("wxTreeListCtrl", "wxdragon::widgets::TreeListCtrl");
map.insert("wxNotebook", "wxdragon::widgets::Notebook");
map.insert("wxSimplebook", "wxdragon::widgets::SimpleBook");
map.insert("wxRadioButton", "wxdragon::widgets::RadioButton");
map.insert("wxRadioBox", "wxdragon::widgets::RadioBox");
map.insert("wxToggleButton", "wxdragon::widgets::ToggleButton");
map.insert("wxBitmapButton", "wxdragon::widgets::BitmapButton");
map.insert("wxBitmapToggleButton", "wxdragon::widgets::BitmapToggleButton");
map.insert("wxStaticBitmap", "wxdragon::widgets::StaticBitmap");
map.insert("wxStaticLine", "wxdragon::widgets::StaticLine");
map.insert("wxStaticBox", "wxdragon::widgets::StaticBox");
map.insert("wxScrollBar", "wxdragon::widgets::ScrollBar");
map.insert("wxScrolledWindow", "wxdragon::widgets::ScrolledWindow");
map.insert("wxSplitterWindow", "wxdragon::widgets::SplitterWindow");
map.insert("wxCollapsiblePane", "wxdragon::widgets::CollapsiblePane");
map.insert("wxCheckListBox", "wxdragon::widgets::CheckListBox");
map.insert("wxRearrangeList", "wxdragon::widgets::RearrangeList");
map.insert("wxTreebook", "wxdragon::widgets::Treebook");
map.insert("wxListCtrl", "wxdragon::widgets::ListCtrl");
map.insert("wxGrid", "wxdragon::widgets::Grid");
map.insert("wxHyperlinkCtrl", "wxdragon::widgets::HyperlinkCtrl");
map.insert("wxSearchCtrl", "wxdragon::widgets::SearchCtrl");
map.insert("wxStyledTextCtrl", "wxdragon::widgets::StyledTextCtrl");
map.insert("wxActivityIndicator", "wxdragon::widgets::ActivityIndicator");
map.insert("wxAnimationCtrl", "wxdragon::widgets::AnimationCtrl");
map.insert("wxBitmapComboBox", "wxdragon::widgets::BitmapComboBox");
map.insert("wxCalendarCtrl", "wxdragon::widgets::CalendarCtrl");
map.insert("wxColourPickerCtrl", "wxdragon::widgets::ColourPickerCtrl");
map.insert("wxCommandLinkButton", "wxdragon::widgets::CommandLinkButton");
map.insert("wxDatePickerCtrl", "wxdragon::widgets::DatePickerCtrl");
map.insert("wxDirPickerCtrl", "wxdragon::widgets::DirPickerCtrl");
map.insert("wxEditableListBox", "wxdragon::widgets::EditableListBox");
map.insert("wxFileCtrl", "wxdragon::widgets::FileCtrl");
map.insert("wxFilePickerCtrl", "wxdragon::widgets::FilePickerCtrl");
map.insert("wxFontPickerCtrl", "wxdragon::widgets::FontPickerCtrl");
map.insert("wxMediaCtrl", "wxdragon::widgets::MediaCtrl");
map.insert("wxTimePickerCtrl", "wxdragon::widgets::TimePickerCtrl");
map.insert("wxRichTextCtrl", "wxdragon::widgets::RichTextCtrl");
map.insert("wxAuiManager", "wxdragon::widgets::AuiManager");
map.insert("wxAuiNotebook", "wxdragon::widgets::AuiNotebook");
map.insert("wxAuiToolBar", "wxdragon::widgets::AuiToolBar");
map.insert("wxAuiMDIParentFrame", "wxdragon::widgets::AuiMDIParentFrame");
map.insert("wxAuiMDIChildFrame", "wxdragon::widgets::AuiMDIChildFrame");
map.insert("wxToolBar", "wxdragon::widgets::ToolBar");
map.insert("wxStatusBar", "wxdragon::widgets::StatusBar");
map.insert("tool", "wxdragon::widgets::Tool");
map.insert("wxMenuBar", "wxdragon::menus::MenuBar");
map.insert("wxMenu", "wxdragon::menus::Menu");
map.insert("wxMenuItem", "wxdragon::menus::MenuItem");
map.insert("unknown", "wxdragon::window::Window");
map
}
fn generate_xrc_struct(input: XrcMacroInput) -> syn::Result<proc_macro2::TokenStream> {
let xrc_content = read_xrc_file(&input.xrc_path)?;
let xrc_objects = parse_xrc_content(&xrc_content)?;
let root_object = find_root_object(&xrc_objects)?;
let mut all_objects = Vec::new();
collect_named_objects(root_object, &mut all_objects);
let widget_objects: Vec<_> = all_objects
.into_iter()
.filter(|obj| {
!obj.class.contains("Sizer") && !obj.class.contains("sizeritem") && !obj.class.contains("spacer")
&& obj.class != "wxMenu"
})
.collect();
let (tool_objects, remaining_objects): (Vec<_>, Vec<_>) = widget_objects.iter().partition(|obj| obj.class == "tool");
let (menu_item_objects, remaining_objects2): (Vec<_>, Vec<_>) =
remaining_objects.into_iter().partition(|obj| obj.class == "wxMenuItem");
let (menubar_objects, non_special_objects): (Vec<_>, Vec<_>) =
remaining_objects2.into_iter().partition(|obj| obj.class == "wxMenuBar");
let struct_name = &input.struct_name;
let class_mapping = get_class_mapping();
let struct_fields = widget_objects.iter().map(|obj| {
let field_name = Ident::new(&obj.name, proc_macro2::Span::call_site());
let type_str = class_mapping.get(obj.class.as_str()).unwrap_or(&"wxdragon::window::Window");
let field_type: syn::Type = syn::parse_str(type_str).unwrap();
quote! { pub #field_name: #field_type }
});
let root_load_method = match root_object.class.as_str() {
"wxDialog" => quote! { load_dialog },
"wxFrame" => quote! { load_frame },
"wxPanel" => quote! { load_panel },
_ => quote! { load_frame }, };
let root_field_name = Ident::new(&root_object.name, proc_macro2::Span::call_site());
let xrc_path = &input.xrc_path;
let non_special_initializers = non_special_objects.iter().map(|obj| {
let field_name = Ident::new(&obj.name, proc_macro2::Span::call_site());
let obj_name_lit = &obj.name;
if obj.name == root_object.name {
quote! {
let #field_name = resource.#root_load_method(parent, #obj_name_lit)
.unwrap_or_else(|| panic!("Failed to load XRC root object: {}", #obj_name_lit));
}
} else {
let type_str = class_mapping.get(obj.class.as_str()).unwrap_or(&"wxdragon::window::Window");
let widget_type: syn::Type = syn::parse_str(type_str).unwrap();
quote! {
let #field_name = #root_field_name
.find_child_by_xrc_name::<#widget_type>(#obj_name_lit)
.unwrap_or_else(|| panic!("Failed to find XRC child: {}", #obj_name_lit));
}
}
});
let menubar_initializers = menubar_objects.iter().map(|menubar_obj| {
let field_name = Ident::new(&menubar_obj.name, proc_macro2::Span::call_site());
let _menubar_name_lit = &menubar_obj.name;
quote! {
let #field_name = #root_field_name.get_menu_bar()
.unwrap_or_else(|| panic!("Failed to get MenuBar from Frame"));
}
});
let tool_initializers = tool_objects.iter().map(|tool_obj| {
let field_name = Ident::new(&tool_obj.name, proc_macro2::Span::call_site());
let tool_name_lit = &tool_obj.name;
let toolbar_name = root_object
.children
.iter()
.find_map(|child| find_toolbar_parent_for_tool(child, &tool_obj.name))
.or_else(|| find_toolbar_parent_for_tool(root_object, &tool_obj.name))
.unwrap_or("main_toolbar");
let toolbar_field = Ident::new(toolbar_name, proc_macro2::Span::call_site());
quote! {
let #field_name = #toolbar_field.get_tool_by_name(#tool_name_lit)
.unwrap_or_else(|| panic!("Failed to find tool: {}", #tool_name_lit));
}
});
let menu_item_initializers = menu_item_objects.iter().map(|menu_item_obj| {
let field_name = Ident::new(&menu_item_obj.name, proc_macro2::Span::call_site());
let menu_item_name_lit = &menu_item_obj.name;
quote! {
let #field_name = wxdragon::menus::MenuItem::from_xrc_name(#root_field_name.window_handle(), #menu_item_name_lit)
.unwrap_or_else(|| panic!("Failed to find menu item: {}", #menu_item_name_lit));
}
});
let field_assignments = widget_objects.iter().map(|obj| {
let field_name = Ident::new(&obj.name, proc_macro2::Span::call_site());
quote! { #field_name }
});
let drop_impl = quote! {
impl Drop for #struct_name {
fn drop(&mut self) {
if self.auto_destroy_root {
self.#root_field_name.destroy();
}
}
}
};
let generated = quote! {
#[allow(non_snake_case)]
pub struct #struct_name {
#(#struct_fields,)*
auto_destroy_root: bool,
_resource: wxdragon::xrc::XmlResource,
}
impl #struct_name {
pub const XRC_DATA: &'static str = include_str!(#xrc_path);
pub fn new(parent: Option<&dyn wxdragon::window::WxWidget>, auto_destroy_root: bool) -> Self {
let resource = wxdragon::xrc::XmlResource::get();
resource.init_platform_aware_staticbitmap_handler();
resource.init_all_handlers();
resource.load_from_string(Self::XRC_DATA)
.unwrap_or_else(|err| panic!("Failed to load XRC data: {}", err));
#(#non_special_initializers)*
#(#menubar_initializers)*
#(#tool_initializers)*
#(#menu_item_initializers)*
Self {
#(#field_assignments,)*
auto_destroy_root,
_resource: resource,
}
}
pub fn xrc_id(name: &str) -> i32 {
wxdragon::xrc::XmlResource::get_xrc_id(name)
}
}
#drop_impl
};
Ok(generated)
}
fn read_xrc_file(path: &str) -> syn::Result<String> {
let possible_paths = vec![
std::path::PathBuf::from(path),
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|_| std::path::PathBuf::from(path)),
if path.starts_with("../") {
std::env::current_dir()
.map(|cwd| cwd.join("src").join(path))
.unwrap_or_else(|_| std::path::PathBuf::from(path))
} else {
std::path::PathBuf::from(path)
},
std::env::var("CARGO_MANIFEST_DIR")
.map(|manifest_dir| std::path::PathBuf::from(manifest_dir).join(path))
.unwrap_or_else(|_| std::path::PathBuf::from(path)),
std::env::var("CARGO_MANIFEST_DIR")
.map(|manifest_dir| {
if path.starts_with("../") {
std::path::PathBuf::from(manifest_dir).join(path.trim_start_matches("../"))
} else {
std::path::PathBuf::from(manifest_dir).join("src").join(path)
}
})
.unwrap_or_else(|_| std::path::PathBuf::from(path)),
];
for full_path in &possible_paths {
if let Ok(content) = std::fs::read_to_string(full_path) {
return Ok(content);
}
}
Err(Error::new(
proc_macro2::Span::call_site(),
format!(
"Failed to read XRC file '{}' for macro analysis. \
The file will be embedded using include_str! at compile time, \
but the macro needs to read it now to generate widget fields. \
\nTried paths: {:?} \
\n\nNote: Use paths relative to your crate root or source file, just like include_str!",
path,
possible_paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>()
),
))
}
fn parse_xrc_content(content: &str) -> syn::Result<Vec<XrcObject>> {
use quick_xml::Reader;
use quick_xml::events::Event;
let mut reader = Reader::from_str(content);
reader.config_mut().trim_text(true);
let mut objects = Vec::new();
let mut stack = Vec::new();
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) if e.name().as_ref() == b"object" => {
let mut obj = XrcObject {
name: String::new(),
class: String::new(),
children: Vec::new(),
};
for attr in e.attributes() {
let attr = attr.map_err(|e| Error::new(proc_macro2::Span::call_site(), format!("XML parsing error: {e}")))?;
match attr.key.as_ref() {
b"name" => obj.name = String::from_utf8_lossy(&attr.value).into_owned(),
b"class" => obj.class = String::from_utf8_lossy(&attr.value).into_owned(),
_ => {}
}
}
stack.push(obj);
}
Ok(Event::End(ref e)) if e.name().as_ref() == b"object" => {
if let Some(obj) = stack.pop() {
if let Some(parent) = stack.last_mut() {
parent.children.push(obj);
} else {
if !obj.name.is_empty() || !obj.children.is_empty() {
objects.push(obj);
}
}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(Error::new(proc_macro2::Span::call_site(), format!("XML parsing error: {e}")));
}
_ => {}
}
}
Ok(objects)
}
fn find_root_object(objects: &[XrcObject]) -> syn::Result<&XrcObject> {
objects
.iter()
.find(|obj| {
obj.class == "wxFrame" || obj.class == "wxDialog" || obj.class == "wxPanel"
})
.ok_or_else(|| {
Error::new(
proc_macro2::Span::call_site(),
"No root Frame, Dialog, or Panel object found in XRC. Make sure your XRC file contains a top-level wxFrame, wxDialog, or wxPanel object.",
)
})
}
fn collect_named_objects(obj: &XrcObject, result: &mut Vec<XrcObject>) {
if !obj.name.is_empty() {
result.push(obj.clone());
}
for child in &obj.children {
collect_named_objects(child, result);
}
}
fn find_toolbar_parent_for_tool<'a>(obj: &'a XrcObject, tool_name: &str) -> Option<&'a str> {
let is_toolbar = obj.class == "wxToolBar" || obj.class == "wxAuiToolBar";
for child in &obj.children {
if child.class == "tool" && child.name == tool_name && is_toolbar && !obj.name.is_empty() {
return Some(&obj.name);
}
if let Some(parent_name) = find_toolbar_parent_for_tool(child, tool_name) {
return Some(parent_name);
}
}
None
}