Skip to main content

wxdragon_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use std::collections::HashMap;
4use syn::{Error, Ident, LitStr, Token, parse_macro_input};
5
6/// A procedural macro that generates a Rust struct for XRC-defined UI with all named widgets.
7///
8/// This macro reads an XRC file at compile time, parses it to extract all named widgets,
9/// and generates a Rust struct with typed fields for accessing all UI components.
10/// The root object (Frame, Dialog, or Panel) is automatically detected from the XRC file.
11///
12/// # Syntax
13///
14/// ```ignore
15/// include_xrc!("path/to/file.xrc", StructName);
16/// ```
17///
18/// # Arguments
19///
20/// * `path` - Path to the XRC file relative to the current crate root
21/// * `struct_name` - Name for the generated Rust struct  
22///
23/// # Generated Code
24///
25/// The macro generates a struct with:
26/// - A field for the root object (Frame, Dialog, or Panel) - automatically detected
27/// - Fields for all named child widgets found in the XRC
28/// - A `new()` method that loads the XRC and initializes all fields
29/// - An `xrc_id()` helper method for getting XRC IDs
30///
31/// # Example
32///
33/// Given an XRC file `dialog.xrc`:
34/// ```xml
35/// <?xml version="1.0" encoding="UTF-8"?>
36/// <resource>
37///   <object class="wxFrame" name="main_frame">
38///     <title>My Frame</title>
39///     <object class="wxPanel" name="main_panel">
40///       <object class="wxButton" name="test_button">
41///         <label>Click Me!</label>
42///       </object>
43///       <object class="wxTextCtrl" name="input_field">
44///       </object>
45///     </object>
46///   </object>
47/// </resource>
48/// ```
49///
50/// Usage:
51/// ```ignore
52/// include_xrc!("dialog.xrc", MyFrameUI);
53///
54/// // This generates:
55/// pub struct MyFrameUI {
56///     pub main_frame: Frame,      // Root object - auto-detected
57///     pub main_panel: Panel,
58///     pub test_button: Button,
59///     pub input_field: TextCtrl,
60///     _resource: XmlResource,
61/// }
62///
63/// impl MyFrameUI {
64///     pub const XRC_DATA: &'static str = "..."; // Embedded XRC content
65///     
66///     pub fn new(parent: Option<&dyn WxWidget>) -> Self {
67///         // Implementation that loads XRC and finds all widgets
68///     }
69///     
70///     pub fn xrc_id(name: &str) -> i32 {
71///         // Helper to get XRC IDs
72///     }
73/// }
74/// ```
75#[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
85/// Parsed input for the include_xrc macro
86struct 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        // Parse "path/to/file.xrc"
94        let xrc_path: LitStr = input.parse()?;
95        input.parse::<Token![,]>()?;
96
97        // Parse StructName
98        let struct_name: Ident = input.parse()?;
99
100        // No more root parameter - we'll auto-detect it
101
102        Ok(XrcMacroInput {
103            xrc_path: xrc_path.value(),
104            struct_name,
105        })
106    }
107}
108
109/// XRC object information extracted from XML
110#[derive(Debug, Clone)]
111struct XrcObject {
112    name: String,
113    class: String,
114    children: Vec<XrcObject>,
115}
116
117/// Mapping from XRC class names to wxDragon Rust types
118fn get_class_mapping() -> HashMap<&'static str, &'static str> {
119    let mut map = HashMap::new();
120
121    // Window types
122    map.insert("wxDialog", "wxdragon::dialogs::Dialog");
123    map.insert("wxFrame", "wxdragon::widgets::Frame");
124    map.insert("wxPanel", "wxdragon::widgets::Panel");
125
126    // Controls
127    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    // StaticBitmap uses platform-aware XRC handler at C++ level
150    // On Windows: creates wxGenericStaticBitmap, but we still treat it as StaticBitmap in Rust
151    // On other platforms: creates native wxStaticBitmap
152    // Both have the same interface, so we always use StaticBitmap wrapper
153    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    // Note: wxWebView doesn't have XRC support in wxWidgets
185
186    // AUI widgets
187    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    // Frame-related widgets
194    map.insert("wxToolBar", "wxdragon::widgets::ToolBar");
195    map.insert("wxStatusBar", "wxdragon::widgets::StatusBar");
196
197    // Toolbar tools
198    map.insert("tool", "wxdragon::widgets::Tool");
199
200    // Menu system
201    map.insert("wxMenuBar", "wxdragon::menus::MenuBar");
202    map.insert("wxMenu", "wxdragon::menus::Menu");
203    map.insert("wxMenuItem", "wxdragon::menus::MenuItem");
204
205    // Fallback for unknown types - treat as Window
206    map.insert("unknown", "wxdragon::window::Window");
207
208    map
209}
210
211/// Generate the complete XRC struct implementation
212fn generate_xrc_struct(input: XrcMacroInput) -> syn::Result<proc_macro2::TokenStream> {
213    // Read and parse the XRC file for widget analysis
214    let xrc_content = read_xrc_file(&input.xrc_path)?;
215    let xrc_objects = parse_xrc_content(&xrc_content)?;
216
217    // Find root object
218    let root_object = find_root_object(&xrc_objects)?;
219
220    // Collect all named objects for field generation
221    let mut all_objects = Vec::new();
222    collect_named_objects(root_object, &mut all_objects);
223
224    // Filter out sizers and other non-widget objects that don't support XRC
225    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            // Skip Menu objects - they're part of MenuBar
230             && obj.class != "wxMenu"
231        })
232        .collect();
233
234    // Separate tools and menu items from other widgets for special handling
235    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    // Generate the struct and implementation
244    let struct_name = &input.struct_name;
245    let class_mapping = get_class_mapping();
246
247    // Generate struct fields for all named objects
248    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    // Generate field initialization in new() method
257    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 }, // Default to frame
262    };
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    // Generate initialization for regular widgets first
268    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            // Root object is loaded directly
274            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            // Regular widgets are found within the root - explicitly specify the widget type
280            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    // Generate initialization for MenuBars (they're loaded separately, not as child windows)
292    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    // Generate initialization for tools after toolbars are loaded
303    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        // Find the parent toolbar from XRC hierarchy
308        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    // Generate initialization for menu items after the root frame is loaded
323    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    // Generate Drop impl: auto-destroy the root object if the auto_destroy_root flag is set
339    let drop_impl = quote! {
340        impl Drop for #struct_name {
341            fn drop(&mut self) {
342                // Ensure the XRC-created object is destroyed to free native resources
343                // only when opted-in via auto_destroy flag.
344                if self.auto_destroy_root {
345                    // Use UFCS to avoid requiring trait imports in user crates.
346                    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            /// The embedded XRC data from the file
362            pub const XRC_DATA: &'static str = include_str!(#xrc_path);
363
364            /// Create a new instance by loading the embedded XRC
365            pub fn new(parent: Option<&dyn wxdragon::window::WxWidget>, auto_destroy_root: bool) -> Self {
366                let resource = wxdragon::xrc::XmlResource::get();
367
368                // Initialize platform-aware StaticBitmap handler BEFORE default handlers
369                // to ensure it gets registered first
370                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                // Initialize MenuBars (loaded separately from XRC)
380                #(#menubar_initializers)*
381
382                // Initialize tools after toolbars are loaded
383                #(#tool_initializers)*
384
385                // Initialize menu items after the root frame is loaded
386                #(#menu_item_initializers)*
387
388                Self {
389                    #(#field_assignments,)*
390                    auto_destroy_root,
391                    _resource: resource,
392                }
393            }
394
395            /// Get XRC ID for a control name
396            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
407/// Read XRC file content from the filesystem during macro expansion
408fn read_xrc_file(path: &str) -> syn::Result<String> {
409    // Try to resolve the file the same way include_str! would
410    // include_str! looks for files relative to the current source file
411
412    // For procedural macros, we need to handle the fact that CARGO_MANIFEST_DIR
413    // might refer to the macro crate, not the invoking crate. We use multiple strategies.
414
415    let possible_paths = vec![
416        // 1. Try path as-is from current working directory
417        std::path::PathBuf::from(path),
418        // 2. Try from the current working directory (which should be the invoking crate)
419        std::env::current_dir()
420            .map(|cwd| cwd.join(path))
421            .unwrap_or_else(|_| std::path::PathBuf::from(path)),
422        // 3. For "../" patterns, try resolving from current working dir + "src"
423        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        // 4. Try CARGO_MANIFEST_DIR if available (invoking crate's manifest dir)
431        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        // 5. For "../" from src directory pattern (most common case)
435        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    // If none worked, give a helpful error
453    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
467/// Parse XRC XML content to extract object hierarchy
468fn 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                // Parse attributes
488                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                        // Always add to parent, even if object doesn't have a name
504                        // This preserves the hierarchy for unnamed intermediate objects
505                        parent.children.push(obj);
506                    } else {
507                        // Top-level object - only add if it has a name or children
508                        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
525/// Find the root object to load (automatically detect Frame, Dialog, or Panel)
526fn find_root_object(objects: &[XrcObject]) -> syn::Result<&XrcObject> {
527    // Look for the first Frame, Dialog, or Panel object
528    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
541/// Recursively collect all named objects from the hierarchy
542fn 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
552/// Find the parent toolbar name for a tool in the XRC hierarchy.
553/// Returns None if no toolbar parent is found.
554fn 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}