glade_bindgen_gtk4/
lib.rs

1use 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 { //true if need to include in tree
37	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}
171/*
172struct Args(Ident, LitStr);
173
174impl syn::parse::Parse for Args {
175	fn parse<'a>(input: &'a ParseBuffer<'a>) -> Result<Self, syn::Error> {
176		let type1 = input.parse()?;
177		input.parse::<Token![,]>()?;
178		let type2 = input.parse()?;
179		Ok(Args(type1, type2))
180	}
181}
182
183#[proc_macro]
184pub fn include_glade(args: TokenStream) -> TokenStream {
185	let args: Args = syn::parse(args).unwrap();
186	let span = args.0.span();
187	let name = args.0;
188	let file_include_dir = args.1.value();
189	let mut file_path = span.unwrap().source_file().path().parent().unwrap().to_owned();
190	file_path.push(&file_include_dir);
191	let file = File::open(file_path).unwrap();
192	generate_bind(name, file, file_include_dir)
193}
194*/
195
196/* Remove the UI extension of a file, and return its bare name */
197fn 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}