From 2593a8c14bb89674fb9cc5a31b19d06a000883d3 Mon Sep 17 00:00:00 2001 From: Andrey Kutejko Date: Tue, 27 Apr 2021 02:12:57 +0200 Subject: [PATCH] WIP --- .gitignore | 2 +- gtk/tests/builder_handlers.rs | 145 ++++++++++++++++++++ gtk3-macros/Cargo.toml | 1 + gtk3-macros/src/builder_handlers.rs | 199 ++++++++++++++++++++++++++++ gtk3-macros/src/lib.rs | 18 ++- 5 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 gtk/tests/builder_handlers.rs create mode 100644 gtk3-macros/src/builder_handlers.rs diff --git a/.gitignore b/.gitignore index d45b2a325754..3184d7b8c7fe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ config.status *.o *.swp **Cargo.lock -gtk/tests/* +# gtk/tests/* diff --git a/gtk/tests/builder_handlers.rs b/gtk/tests/builder_handlers.rs new file mode 100644 index 000000000000..d94904935250 --- /dev/null +++ b/gtk/tests/builder_handlers.rs @@ -0,0 +1,145 @@ +#[derive(glib::Downgrade)] +pub struct MyWidget(gtk::Widget); + +#[gtk3_macros::builder_handlers] +impl MyWidget { + fn handler0(&self) {} + + fn handler1(&self, _p: u32) {} + + fn handler2(&self, _x: f64, _y: f64) {} + + fn handler3(&self, x: f64, y: f64) -> f64 { + x + y + } + + // fn handler4(&self, x: f64, y: f64) -> Option { + // if x >= 0.0 && y >= 0.0 { + // Some(x + y) + // } else { + // None + // } + // } +} + +// Generated code +/* +impl MyWidget { + #[allow(clippy)] + fn get_handler( + &self, + signal: &str, + ) -> Option Option + 'static>> { + match signal { + "handler0" => Some({ + #[allow(unused_variables)] + Box::new( + glib::clone!(@weak self as this => move |values: &[glib::Value]| { + this.handler0(); + None + }), + ) + }), + "handler1" => Some({ + #[allow(unused_variables)] + Box::new( + glib::clone!(@weak self as this => move |values: &[glib::Value]| { + this.handler1( + match values[0usize].get_some() { + Ok(value) => value, + Err(error) => { + glib::g_critical!( + "builder handler", + "Handler {} expects an argument of type {} but received `{:?}`: {}.", + "handler1", stringify! (u32), + values[0usize], error + ); + return None; + }, + } + ); + None + }), + ) + }), + "handler2" => Some({ + #[allow(unused_variables)] + Box::new( + glib::clone!(@weak self as this => move |values: &[glib::Value]| { + this.handler2( + match values[0usize].get_some() { + Ok(value) => value, + Err(error) => { + glib::g_critical!( + "builder handler", + "Handler {} expects an argument of type {} but received `{:?}`: {}.", + "handler2", + stringify!(f64), + values[0usize], + error + ); + return None; + }, + }, + match values[1usize].get_some() { + Ok(value) => value, + Err(error) => { + glib::g_critical!( + "builder handler", + "Handler {} expects an argument of type {} but received `{:?}`: {}.", + "handler2", + stringify!(f64), + values[1usize], + error + ); + return None; + }, + } + ); + None + }), + ) + }), + "handler3" => Some({ + #[allow(unused_variables)] + Box::new( + glib::clone!(@weak self as this => move |values: &[glib::Value]| { + let result = this.handler3( + match values[0usize].get_some() { + Ok(value) => value, + Err(error) => { + glib::g_critical!( + "builder handler", + "Handler {} expects an argument of type {} but received `{:?}`: {}.", + "handler3", + stringify!(f64), + values[0usize], + error + ); + return None; + }, + }, + match values[1usize].get_some() { + Ok(value) => value, + Err(error) => { + glib::g_critical!( + "builder handler", + "Handler {} expects an argument of type {} but received `{:?}`: {}.", + "handler3", + stringify!(f64), + values[1usize], + error + ); + return None; + }, + } + ); + Some(glib::value::ToValue::to_value(&result)) + }), + ) + }), + _ => None, + } + } +} +*/ diff --git a/gtk3-macros/Cargo.toml b/gtk3-macros/Cargo.toml index 3ab87ca9e152..a4f903742518 100644 --- a/gtk3-macros/Cargo.toml +++ b/gtk3-macros/Cargo.toml @@ -25,3 +25,4 @@ proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0", default-features = false, features = ["full"] } proc-macro-crate = "1.0" +darling = "0.12" diff --git a/gtk3-macros/src/builder_handlers.rs b/gtk3-macros/src/builder_handlers.rs new file mode 100644 index 000000000000..54ffbbc720a0 --- /dev/null +++ b/gtk3-macros/src/builder_handlers.rs @@ -0,0 +1,199 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use darling::FromMeta; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote, quote_spanned}; +use syn::{ + parse, spanned::Spanned, Attribute, Error, FnArg, ImplItem, ImplItemMethod, ItemImpl, Meta, + MetaList, NestedMeta, PatType, Signature, Type, +}; + +#[derive(Debug, Default, FromMeta)] +#[darling(default)] +pub struct HandlersImplAttributes { + get_handler_fn: Option, +} + +#[derive(Debug, Default, FromMeta)] +#[darling(default)] +struct HandlerAttributes { + name: Option, +} + +#[derive(Debug)] +struct HandlerInfo { + name: String, + sig: Signature, +} + +fn generate_handler(info: &HandlerInfo) -> Result { + let handler_name = &info.name; + let arguments: Vec = info.sig.inputs + .iter() + .skip(1) + .enumerate() + .map(|(index, arg)| { + let arg_type = argument_type(arg)?; + Ok(quote_spanned! { arg.span() => + match values[#index].get() { + Ok(value) => value, + Err(error) => { + glib::g_critical!("builder handler", "Handler {} expects an argument of type {} but received `{:?}`: {}.", #handler_name, stringify!(#arg_type), values[#index], error); + return None; + }, + } + }) + }) + .collect::>()?; + + let signal = &info.name; + let method = &info.sig.ident; + let is_unit = matches!(info.sig.output, syn::ReturnType::Default); + let handler = if is_unit { + quote_spanned! { info.sig.span() => + #signal => Some({ + Box::new(glib::clone!(@weak self as this => @default-return None, move |values: &[glib::Value]| { + this.#method(#(#arguments),*); + None + })) + }), + } + } else { + quote_spanned! { info.sig.span() => + #signal => Some({ + Box::new(glib::clone!(@weak self as this => @default-return None, move |values: &[glib::Value]| { + let result = this.#method(#(#arguments),*); + Some(glib::value::ToValue::to_value(&result)) + })) + }), + } + }; + Ok(handler) +} + +fn combine_errors(error_acc: &mut Option, error: Error) { + match error_acc { + Some(ref mut error_acc) => { + error_acc.combine(error); + } + None => { + error_acc.replace(error); + } + } +} + +fn attributes_to_metas(attributes: Vec) -> Result, Error> { + let mut metas = Vec::new(); + let mut error = None; + for attr in attributes { + let meta = attr.parse_meta()?; + match meta { + Meta::List(MetaList { nested, .. }) => metas.extend(nested), + _ => combine_errors(&mut error, Error::new(attr.span(), "Unexpected attribute")), + } + } + if let Some(error) = error { + Err(error) + } else { + Ok(metas) + } +} + +fn is_assoc(sig: &Signature) -> bool { + sig.inputs + .first() + .map_or(false, |arg| matches!(arg, FnArg::Receiver(..))) +} + +fn argument_type(arg: &FnArg) -> Result<&Type, Error> { + match arg { + FnArg::Typed(PatType { ty, .. }) => Ok(&*ty), + _ => Err(Error::new( + arg.span(), + "Cannot extract type of an argument.", + )), + } +} + +fn generate_connect_method( + attrs: &HandlersImplAttributes, + actions: &[TokenStream2], +) -> ImplItemMethod { + let get_handler_fn = format_ident!( + "{}", + attrs.get_handler_fn.as_deref().unwrap_or("get_handler") + ); + let builder_connect_method = quote! { + #[allow(clippy)] + #[allow(unused_variables, unused_braces)] + fn #get_handler_fn(&self, builder: >k::Builder, signal: &str) -> Option Option + 'static>> { + match signal { + #( + #actions + )* + _ => None, + } + } + }; + parse(builder_connect_method.into()).unwrap() +} + +pub fn handlers( + attrs: HandlersImplAttributes, + mut input: ItemImpl, +) -> Result { + let mut handlers: Vec = Vec::new(); + for item in input.items.iter_mut() { + if let ImplItem::Method(method) = item { + if !is_assoc(&method.sig) { + return Err(Error::new( + method.sig.span(), + "Unsupported signature of method. Only associated methods are supported.", + ) + .to_compile_error() + .into()); + } + + let attributes = + extract_from_vec(&mut method.attrs, |attr| attr.path.is_ident("handler")); + let metas = attributes_to_metas(attributes).map_err(|err| err.to_compile_error())?; + let attrs = HandlerAttributes::from_list(&metas) + .map_err(|err| TokenStream::from(err.write_errors()))?; + + let info = HandlerInfo { + name: attrs.name.unwrap_or_else(|| method.sig.ident.to_string()), + sig: method.sig.clone(), + }; + handlers.push(info); + } + } + + let connects: Vec = handlers + .iter() + .map(generate_handler) + .collect::>() + .map_err(|err| err.to_compile_error())?; + + let connect_method = generate_connect_method(&attrs, &connects); + input.items.push(ImplItem::Method(connect_method)); + + let s = quote!(#input); + // println!("{}", s); + Ok(s.into()) +} + +// TODO: Replace this by Vec::drain_filter as soon as it is stabilized. +fn extract_from_vec(vec: &mut Vec, predicate: impl Fn(&T) -> bool) -> Vec { + let mut i = 0; + let mut result: Vec = Vec::new(); + while i != vec.len() { + if (predicate)(&vec[i]) { + let item = vec.remove(i); + result.push(item); + } else { + i += 1; + } + } + result +} diff --git a/gtk3-macros/src/lib.rs b/gtk3-macros/src/lib.rs index ac5677e26f61..9caa6529efa8 100644 --- a/gtk3-macros/src/lib.rs +++ b/gtk3-macros/src/lib.rs @@ -1,12 +1,15 @@ // Take a look at the license at the top of the repository in the LICENSE file. mod attribute_parser; +mod builder_handlers; mod composite_template_derive; mod util; +use darling::FromMeta; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; -use syn::{parse_macro_input, DeriveInput}; +use syn::DeriveInput; +use syn::{parse_macro_input, AttributeArgs, ItemImpl}; /// Derive macro for using a composite template in a widget. /// @@ -61,3 +64,16 @@ pub fn composite_template_derive(input: TokenStream) -> TokenStream { let gen = composite_template_derive::impl_composite_template(&input); gen.into() } + +#[proc_macro_attribute] +pub fn builder_handlers(args: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemImpl); + let attribute_args = parse_macro_input!(args as AttributeArgs); + let attrs = match builder_handlers::HandlersImplAttributes::from_list(&attribute_args) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(e.write_errors()); + } + }; + builder_handlers::handlers(attrs, input).unwrap_or_else(|err| err) +}