Skip to content

Commit 03155c0

Browse files
committed
Add support for ext_trait in properties macro
1 parent 751e1dd commit 03155c0

File tree

3 files changed

+162
-42
lines changed

3 files changed

+162
-42
lines changed

glib-macros/src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,7 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream {
873873
/// | `set [= expr]` | Specify that the property is writable and use `PropertySet::set` [or optionally set a custom internal setter] | | `#[property(set)]`, `#[property(set = set_prop)]`, or `[property(set = \|_, val\| {})]` |
874874
/// | `override_class = expr` | The type of class of which to override the property from | | `#[property(override_class = SomeClass)]` |
875875
/// | `override_interface = expr` | The type of interface of which to override the property from | | `#[property(override_interface = SomeInterface)]` |
876-
/// | `nullable` | Whether to use `Option<T>` in the wrapper's generated setter | | `#[property(nullable)]` |
876+
/// | `nullable` | Whether to use `Option<T>` in the generated setter method | | `#[property(nullable)]` |
877877
/// | `member = ident` | Field of the nested type where property is retrieved and set | | `#[property(member = author)]` |
878878
/// | `construct_only` | Specify that the property is construct only. This will not generate a public setter and only allow the property to be set during object construction. The use of a custom internal setter is supported. | | `#[property(get, construct_only)]` or `#[property(get, set = set_prop, construct_only)]` |
879879
/// | `builder(<required-params>)[.ident]*` | Used to input required params or add optional Param Spec builder fields | | `#[property(builder(SomeEnum::default()))]`, `#[builder().default_value(1).minimum(0).maximum(5)]`, etc. |
@@ -885,15 +885,20 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream {
885885
/// You might hit a roadblock when declaring properties with this macro because you want to use a name that happens to be a Rust keyword. This may happen with names like `loop`, which is a pretty common name when creating things like animation handlers.
886886
/// To use those names, you can make use of the raw identifier feature of Rust. Simply prefix the identifier name with `r#` in the struct declaration. Internally, those `r#`s are stripped so you can use its expected name in [`ObjectExt::property`] or within GtkBuilder template files.
887887
///
888-
/// # Generated wrapper methods
888+
/// # Generated methods
889889
/// The following methods are generated on the wrapper type specified on `#[properties(wrapper_type = ...)]`:
890890
/// * `$property()`, when the property is readable
891891
/// * `set_$property()`, when the property is writable and not construct-only
892892
/// * `connect_$property_notify()`
893893
/// * `notify_$property()`
894894
///
895-
/// Notice: You can't reimplement the generated methods on the wrapper type,
896-
/// but you can change their behavior using a custom internal getter/setter.
895+
/// ## Extension trait
896+
/// You can choose to move the method definitions to a trait by using `#[properties(wrapper_type = super::MyType, ext_trait = MyTypePropertiesExt)]`.
897+
/// The trait name is optional, and defaults to `MyTypePropertiesExt`, where `MyType` is extracted from the wrapper type.
898+
/// Note: The trait is defined in the same module where the `#[derive(Properties)]` call happens, and is implemented on the wrapper type.
899+
///
900+
/// Notice: You can't reimplement the generated methods on the wrapper type, unless you move them to a trait.
901+
/// You can change the behavior of the generated getter/setter methods by using a custom internal getter/setter.
897902
///
898903
/// # Internal getters and setters
899904
/// By default, they are generated for you. However, you can use a custom getter/setter

glib-macros/src/properties.rs

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,61 @@ use syn::parenthesized;
1111
use syn::parse::Parse;
1212
use syn::punctuated::Punctuated;
1313
use syn::spanned::Spanned;
14-
use syn::LitStr;
1514
use syn::Token;
15+
use syn::{parse_quote_spanned, LitStr};
1616

1717
pub struct PropsMacroInput {
1818
wrapper_ty: syn::Path,
19+
ext_trait: Option<Option<syn::Ident>>,
1920
ident: syn::Ident,
2021
props: Vec<PropDesc>,
2122
}
2223

23-
pub struct PropertiesAttr {
24-
_wrapper_ty_token: syn::Ident,
25-
_eq: Token![=],
24+
pub struct PropertiesAttrs {
2625
wrapper_ty: syn::Path,
26+
// None => no ext trait,
27+
// Some(None) => derive the ext trait from the wrapper type,
28+
// Some(Some(ident)) => use the given ext trait Ident
29+
ext_trait: Option<Option<syn::Ident>>,
2730
}
2831

29-
impl Parse for PropertiesAttr {
32+
impl Parse for PropertiesAttrs {
3033
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
34+
let mut wrapper_ty = None;
35+
let mut ext_trait = None;
36+
37+
while !input.is_empty() {
38+
let ident = input.parse::<syn::Ident>()?;
39+
if ident == "wrapper_type" {
40+
let _eq = input.parse::<Token![=]>()?;
41+
wrapper_ty = Some(input.parse::<syn::Path>()?);
42+
} else if ident == "ext_trait" {
43+
if input.peek(Token![=]) {
44+
let _eq = input.parse::<Token![=]>()?;
45+
let ident = input.parse::<syn::Ident>()?;
46+
ext_trait = Some(Some(ident));
47+
} else {
48+
ext_trait = Some(None);
49+
}
50+
}
51+
if input.peek(Token![,]) {
52+
input.parse::<Token![,]>()?;
53+
}
54+
}
55+
3156
Ok(Self {
32-
_wrapper_ty_token: input.parse()?,
33-
_eq: input.parse()?,
34-
wrapper_ty: input.parse()?,
57+
wrapper_ty: wrapper_ty.ok_or_else(|| {
58+
syn::Error::new(input.span(), "missing #[properties(wrapper_type = ...)]")
59+
})?,
60+
ext_trait,
3561
})
3662
}
3763
}
3864

3965
impl Parse for PropsMacroInput {
4066
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
4167
let derive_input: syn::DeriveInput = input.parse()?;
42-
let wrapper_ty = derive_input
68+
let attrs = derive_input
4369
.attrs
4470
.iter()
4571
.find(|x| x.path().is_ident("properties"))
@@ -49,7 +75,7 @@ impl Parse for PropsMacroInput {
4975
"missing #[properties(wrapper_type = ...)]",
5076
)
5177
})?;
52-
let wrapper_ty: PropertiesAttr = wrapper_ty.parse_args()?;
78+
let attrs: PropertiesAttrs = attrs.parse_args()?;
5379
let props: Vec<_> = match derive_input.data {
5480
syn::Data::Struct(struct_data) => parse_fields(struct_data.fields)?,
5581
_ => {
@@ -60,7 +86,8 @@ impl Parse for PropsMacroInput {
6086
}
6187
};
6288
Ok(Self {
63-
wrapper_ty: wrapper_ty.wrapper_ty,
89+
wrapper_ty: attrs.wrapper_ty,
90+
ext_trait: attrs.ext_trait,
6491
ident: derive_input.ident,
6592
props,
6693
})
@@ -529,7 +556,7 @@ fn strip_raw_prefix_from_name(name: &LitStr) -> LitStr {
529556
)
530557
}
531558

532-
fn expand_wrapper_getset_properties(props: &[PropDesc]) -> TokenStream2 {
559+
fn expand_impl_getset_properties(props: &[PropDesc]) -> Vec<syn::ImplItemFn> {
533560
let crate_ident = crate_ident_new();
534561
let defs = props.iter().map(|p| {
535562
let name = &p.name;
@@ -538,7 +565,8 @@ fn expand_wrapper_getset_properties(props: &[PropDesc]) -> TokenStream2 {
538565
let ty = &p.ty;
539566

540567
let getter = p.get.is_some().then(|| {
541-
quote!(pub fn #ident(&self) -> <#ty as #crate_ident::Property>::Value {
568+
let span = p.attrs_span;
569+
parse_quote_spanned!(span=> pub fn #ident(&self) -> <#ty as #crate_ident::Property>::Value {
542570
self.property::<<#ty as #crate_ident::Property>::Value>(#stripped_name)
543571
})
544572
});
@@ -560,51 +588,50 @@ fn expand_wrapper_getset_properties(props: &[PropDesc]) -> TokenStream2 {
560588
std::borrow::Borrow::borrow(&value)
561589
)
562590
};
563-
quote!(pub fn #ident<'a>(&self, value: #set_ty) {
591+
let span = p.attrs_span;
592+
parse_quote_spanned!(span=> pub fn #ident<'a>(&self, value: #set_ty) {
564593
self.set_property_from_value(#stripped_name, &::std::convert::From::from(#upcasted_borrowed_value))
565594
})
566595
});
567-
let span = p.attrs_span;
568-
quote_spanned!(span=>
569-
#getter
570-
#setter
571-
)
596+
[getter, setter]
572597
});
573-
quote!(#(#defs)*)
598+
defs.flatten() // flattens []
599+
.flatten() // removes None
600+
.collect::<Vec<_>>()
574601
}
575602

576-
fn expand_wrapper_connect_prop_notify(props: &[PropDesc]) -> TokenStream2 {
603+
fn expand_impl_connect_prop_notify(props: &[PropDesc]) -> Vec<syn::ImplItemFn> {
577604
let crate_ident = crate_ident_new();
578-
let connection_fns = props.iter().map(|p| {
605+
let connection_fns = props.iter().map(|p| -> syn::ImplItemFn {
579606
let name = &p.name;
580607
let stripped_name = strip_raw_prefix_from_name(name);
581608
let fn_ident = format_ident!("connect_{}_notify", name_to_ident(name));
582609
let span = p.attrs_span;
583-
quote_spanned!(span=> pub fn #fn_ident<F: Fn(&Self) + 'static>(&self, f: F) -> #crate_ident::SignalHandlerId {
610+
parse_quote_spanned!(span=> pub fn #fn_ident<F: Fn(&Self) + 'static>(&self, f: F) -> #crate_ident::SignalHandlerId {
584611
self.connect_notify_local(::core::option::Option::Some(#stripped_name), move |this, _| {
585612
f(this)
586613
})
587614
})
588615
});
589-
quote!(#(#connection_fns)*)
616+
connection_fns.collect::<Vec<_>>()
590617
}
591618

592-
fn expand_wrapper_notify_prop(props: &[PropDesc]) -> TokenStream2 {
619+
fn expand_impl_notify_prop(props: &[PropDesc]) -> Vec<syn::ImplItemFn> {
593620
let crate_ident = crate_ident_new();
594-
let emit_fns = props.iter().map(|p| {
621+
let emit_fns = props.iter().map(|p| -> syn::ImplItemFn {
595622
let name = strip_raw_prefix_from_name(&p.name);
596623
let fn_ident = format_ident!("notify_{}", name_to_ident(&name));
597624
let span = p.attrs_span;
598625
let enum_ident = name_to_enum_ident(name.value());
599-
quote_spanned!(span=> pub fn #fn_ident(&self) {
626+
parse_quote_spanned!(span=> pub fn #fn_ident(&self) {
600627
self.notify_by_pspec(
601628
&<<Self as #crate_ident::object::ObjectSubclassIs>::Subclass
602629
as #crate_ident::subclass::object::DerivedObjectProperties>::derived_properties()
603630
[DerivedPropertiesEnum::#enum_ident as usize]
604631
);
605632
})
606633
});
607-
quote!(#(#emit_fns)*)
634+
emit_fns.collect::<Vec<_>>()
608635
}
609636

610637
fn name_to_enum_ident(name: String) -> syn::Ident {
@@ -661,11 +688,55 @@ pub fn impl_derive_props(input: PropsMacroInput) -> TokenStream {
661688
let fn_properties = expand_properties_fn(&input.props);
662689
let fn_property = expand_property_fn(&input.props);
663690
let fn_set_property = expand_set_property_fn(&input.props);
664-
let getset_properties = expand_wrapper_getset_properties(&input.props);
665-
let connect_prop_notify = expand_wrapper_connect_prop_notify(&input.props);
666-
let notify_prop = expand_wrapper_notify_prop(&input.props);
691+
let getset_properties = expand_impl_getset_properties(&input.props);
692+
let connect_prop_notify = expand_impl_connect_prop_notify(&input.props);
693+
let notify_prop = expand_impl_notify_prop(&input.props);
667694
let properties_enum = expand_properties_enum(&input.props);
668695

696+
let rust_interface = if let Some(ext_trait) = input.ext_trait {
697+
let trait_ident = if let Some(ext_trait) = ext_trait {
698+
ext_trait
699+
} else {
700+
format_ident!(
701+
"{}PropertiesExt",
702+
wrapper_type.segments.last().unwrap().ident
703+
)
704+
};
705+
let signatures = getset_properties
706+
.iter()
707+
.chain(connect_prop_notify.iter())
708+
.chain(notify_prop.iter())
709+
.map(|item| &item.sig);
710+
let trait_def = quote! {
711+
pub trait #trait_ident {
712+
#(#signatures;)*
713+
}
714+
};
715+
let impls = getset_properties
716+
.into_iter()
717+
.chain(connect_prop_notify)
718+
.chain(notify_prop)
719+
.map(|mut item| {
720+
item.vis = syn::Visibility::Inherited;
721+
item
722+
});
723+
quote! {
724+
#trait_def
725+
impl #trait_ident for #wrapper_type {
726+
#(#impls)*
727+
}
728+
}
729+
} else {
730+
quote! {
731+
#[allow(dead_code)]
732+
impl #wrapper_type {
733+
#(#getset_properties)*
734+
#(#connect_prop_notify)*
735+
#(#notify_prop)*
736+
}
737+
}
738+
};
739+
669740
let expanded = quote! {
670741
use #crate_ident::{PropertyGet, PropertySet, ToValue};
671742

@@ -677,13 +748,7 @@ pub fn impl_derive_props(input: PropsMacroInput) -> TokenStream {
677748
#fn_set_property
678749
}
679750

680-
#[allow(dead_code)]
681-
impl #wrapper_type {
682-
#getset_properties
683-
#connect_prop_notify
684-
#notify_prop
685-
}
686-
751+
#rust_interface
687752
};
688753
proc_macro::TokenStream::from(expanded)
689754
}

glib-macros/tests/properties.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,56 @@ fn props() {
388388
);
389389
}
390390

391+
mod ext_trait {
392+
use glib::subclass::object::DerivedObjectProperties;
393+
use glib::ObjectExt;
394+
395+
use glib::subclass::{prelude::ObjectImpl, types::ObjectSubclass};
396+
use glib_macros::Properties;
397+
use std::cell::RefCell;
398+
399+
pub mod imp {
400+
use super::*;
401+
402+
#[derive(Properties, Default)]
403+
#[properties(wrapper_type = super::Author, ext_trait)]
404+
pub struct Author {
405+
#[property(get, set)]
406+
firstname: RefCell<String>,
407+
#[property(get, set)]
408+
lastname: RefCell<String>,
409+
}
410+
411+
#[glib::derived_properties]
412+
impl ObjectImpl for Author {}
413+
414+
#[glib::object_subclass]
415+
impl ObjectSubclass for Author {
416+
const NAME: &'static str = "Author";
417+
type Type = super::Author;
418+
}
419+
}
420+
421+
glib::wrapper! {
422+
pub struct Author(ObjectSubclass<imp::Author>);
423+
}
424+
impl Author {
425+
pub fn new() -> Self {
426+
glib::Object::builder().build()
427+
}
428+
}
429+
}
430+
431+
#[test]
432+
fn ext_trait() {
433+
use ext_trait::imp::AuthorPropertiesExt;
434+
let author = ext_trait::Author::new();
435+
AuthorPropertiesExt::set_firstname(&author, "John");
436+
AuthorPropertiesExt::set_lastname(&author, "Doe");
437+
assert_eq!(AuthorPropertiesExt::firstname(&author), "John");
438+
assert_eq!(AuthorPropertiesExt::lastname(&author), "Doe");
439+
}
440+
391441
#[cfg(test)]
392442
mod kw_names {
393443
mod imp {

0 commit comments

Comments
 (0)