glade_bindgen_gtk4/
lib.rs1use syn::Ident;
2use quote::{quote, format_ident};
3use std::path::{PathBuf, Path};
4use xml::EventReader;
5use xml::reader::XmlEvent;
6use std::fs::File;
7use proc_macro2::TokenStream as TokenStream2;
8use convert_case::{Case, Casing};
9use std::io::Write;
10use std::process::Command;
11use regex::Regex;
12use lazy_static::lazy_static;
13use std::borrow::Cow;
14
15const README: &[u8] = include_bytes!("README.txt");
16
17const HEAD_ANNOTATION: &[u8] = include_bytes!("head_annotation.rs");
18const BUILD_SCRIPT_HEAD_ANNOTATION: &[u8] = include_bytes!("build_script_head_annotation.rs");
19
20pub fn generate_bind_build_script<T: AsRef<Path>>(directory_path: T, static_value: bool) {
21 generate_bind_recursive(&directory_path, true, false, static_value);
22 let path = PathBuf::from(directory_path.as_ref());
23 {
24 let mut path = path.clone();
25 path.push("README_glade-bindgen.txt");
26 std::fs::write(&path, README).unwrap();
27 }
28 {
29 let mut path = path.clone();
30 path.push(".gitignore");
31 std::fs::write(&path, "*.rs").unwrap();
32 }
33 println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
34}
35
36pub fn generate_bind_recursive<T: AsRef<Path>>(directory_path: T, build_script: bool, format: bool, static_value: bool) -> bool { let read_dir = std::fs::read_dir(&directory_path).unwrap();
38 let mut modules = Vec::new();
39 let mut generated_token_streams = Vec::new();
40 for x in read_dir {
41 let dir_entry = x.unwrap();
42 let file_name = dir_entry.file_name().into_string().unwrap();
43 if file_name == "." || file_name == ".."{
44 continue;
45 }
46 if dir_entry.path().is_dir() {
47 if generate_bind_recursive(dir_entry.path(), build_script, format, static_value) {
48 modules.push(file_name);
49 }
50 } else if let Some(name) = remove_ui_extension(&file_name) {
51 let name = format_ident!("{}", name.to_case(Case::Pascal));
52 let file = File::open(dir_entry.path()).unwrap();
53 generated_token_streams.push(generate_bind(name, file, file_name, static_value));
54 }
55 }
56
57 if modules.is_empty() && generated_token_streams.is_empty() {
58 return false;
59 }
60 let mut token_stream = TokenStream2::new();
61 for x in modules {
62 let module = format_ident!("{}", x);
63 token_stream.extend(quote! {
64 pub mod #module;
65 });
66 }
67 for x in generated_token_streams {
68 token_stream.extend::<TokenStream2>(x);
69 }
70
71 let mut mod_path = PathBuf::from(directory_path.as_ref());
72 mod_path.push("mod.rs");
73
74 {
75 let mut mod_file = File::create(&mod_path).unwrap();
76 mod_file.write_all(if build_script {
77 BUILD_SCRIPT_HEAD_ANNOTATION
78 } else {
79 HEAD_ANNOTATION
80 }).unwrap();
81 mod_file.write_all(token_stream.to_string().as_bytes()).unwrap();
82 }
83
84 if format {
85 Command::new("rustfmt").args(&[std::fs::canonicalize(mod_path).unwrap()]).output()
86 .expect("failed to format");
87 }
88
89 true
90}
91
92pub fn generate_bind<T: AsRef<Path>>(name: Ident, file: File, file_include_dir: T, static_value: bool) -> TokenStream2 {
93 let mut objects = TokenStream2::new();
94 let mut objects_new = TokenStream2::new();
95
96 let parser = EventReader::new(file);
97 for e in parser {
98 match e {
99 Ok(XmlEvent::StartElement { name, attributes, .. }) => {
100 if &name.local_name == "object" {
101 let id = attributes.iter().find(| attr | attr.name.local_name == "id");
102 if let Some(id) = id {
103 let class = attributes.iter().find(| attr | attr.name.local_name == "class");
104 if let Some(class) = class {
105 let class = class.value.to_owned();
106 let class_ident = format_ident!("{}", class.replace("Gtk", ""));
107 let id = id.value.to_owned();
108 let id_ident = format_ident!("{}", &id);
109 objects.extend::<TokenStream2>(quote!{
110 pub #id_ident: gtk4::#class_ident,
111 });
112 objects_new.extend::<TokenStream2>(quote! {
113 #id_ident: gtk4::Builder::object(&builder, #id).unwrap(),
114 })
115 }
116 }
117 }
118 }
119 Err(e) => {
120 println!("Error: {}", e);
121 break;
122 }
123 _ => {}
124 }
125 }
126
127 let include_str = format_ident!("include_str");
128 let thread_local = format_ident!("thread_local");
129
130 let include = file_include_dir.as_ref().to_str().unwrap();
131
132 let static_value_token_stream : TokenStream2 = if static_value {
133 quote! {
134 #thread_local! {
135 static OBJECTS: std::sync::Mutex<Option<std::rc::Rc<#name>>> = std::sync::Mutex::new(None);
136 }
137
138 pub fn get() -> std::rc::Rc<Self> {
139 Self::OBJECTS.with(| objects | {
140 let mut objects = objects.lock().unwrap();
141 if objects.is_none() {
142 objects.replace(std::rc::Rc::new(Self::new()));
143 }
144 objects.as_ref().unwrap().clone()
145 })
146 }
147 }
148 } else {
149 TokenStream2::new()
150 };
151
152 let token_stream = quote!{
153 #[allow(dead_code)]
154 pub struct #name {
155 #objects
156 }
157
158 impl #name {
159 #static_value_token_stream
160
161 pub fn new() -> Self {
162 let builder = gtk4::Builder::from_string(#include_str!(#include));
163 Self {
164 #objects_new
165 }
166 }
167 }
168 };
169 token_stream
170}
171fn remove_ui_extension(file_name: &str) -> Option<Cow<'_, str>> {
198 lazy_static! {
199 static ref UI_FILE_REGEX: Regex = Regex::new(r"^(.*)(\.glade|\.ui)$").unwrap();
200 }
201 UI_FILE_REGEX.is_match(file_name).then(|| UI_FILE_REGEX.replace(file_name, "$1"))
202}
203
204#[test]
205fn test_remove_ui_extension() {
206 assert_eq!("foo", remove_ui_extension("foo.ui").unwrap());
207 assert_eq!("bar", remove_ui_extension("bar.glade").unwrap());
208 assert_eq!(None, remove_ui_extension("foo.rs"));
209 assert_eq!(None, remove_ui_extension("foo.glade~"));
210}