#gtk #properties #macro #declaration #experimental #flags #block

nightly macro gtk-properties-macro

Experimental property declaration macro for gtk-rs

1 unstable release

0.1.0 Sep 4, 2022

#554 in Procedural macros

MIT license

27KB
463 lines

⚠ experimental project ⚠

gtk-properties-macro

This package contains a macro that makes it easier to declare object properties when using gtk-rs.

For a general introduction to GTK properties in Rust, please refer to the "Properties" chapter of the gtk-rs book.

Usage

Add this to your Cargo.toml:

gtk-properties-macro = "0.1"

⚠ requires rust nightly

Example

This is a minimal example, that is functionally equivalent to this one from the book.

For a more complete (working) example, please see the examples/ directory. If you are interested in the code that is generated by the macro, check out the files in tests/expands/.

Here we go:

use gtk_properties_macro::properties;

impl ObjectImpl for CustomButton {
    properties! {
        #[int]
        "number" => {
            get {
                self.number.get().to_value()
            }
            set {
                let input_number =
                    value.get().expect("The value needs to be of type `i32`.");
                self.number.replace(input_number);
            }
        }
    }

    fn constructed(&self, boj: &Self::Type) {
        ...
    }
}

Let's go through it one by one:

properties! { ... }

here we are invoking the properties macro. It must be called within a impl ObjectImpl for ... block, and will implement three methods: properties, property and set_property.

#[int]
"number" => { ... }

this is a property declaration. In this case it declares a property named "number", which has type "int" ("int" here corresponds to ParamSpecInt, more on that later).

get {
    self.number.get().to_value()
}

this specifies how to "get" the property's value. The block becomes part of the fn property implementation. Within the 'get' block, we have access to:

  • self: the "inner" struct of our object
  • object: the outer object
  • id: ID of this property (usize)
  • pspec: ParamSpec of this property

The block must evaluate to a glib::Value

set {
    let input_number =
        value.get().expect("The value needs to be of type `i32`.");
    self.number.replace(input_number);
}

corresponding "set" block for the property. It becomes part of the fn set_property implementation. Within the 'set' block we have access to:

  • everything from the 'get' block
  • value: a glib::Value containing the value being set.

Motivation

Implementing properties, property and set_property manually has some disadvantages:

  • very verbose, lots of boilerplate code
  • property name has to be repeated multiple times
  • adding a property involves modifying 3 different places (not counting adding any fields to the struct)
  • hard to verify if property flags are consistent with implementation (e.g. property is flagged readwrite, but only has a getter implemented)

Details

General structure

Within the properties! block, a list of property declarations is expected.

Each declaration consists of:

  1. A type declaration attribute (described below), e.g. #[int(minimum = 3, maximum = 27)]
  2. Zero or more attributes of the form #[doc = "..."] (the compiler transforms doc comments into these). These doc comments are all concatenated and stored in the blurb of the param spec.
  3. A property name, and block with implementations: "property-name" => { /* implementation block */ }

Property type declarations

The type declaration is in the form of an attribute. It starts with a "type tag", followed by an (optional) parenthesized list of flags and key/value pairs.

Example:

#[int(construct, nick = "Great Integer", explicit_notify)
  • here int is the type tag, which determines the type of ParamSpec*Builder to use (see table below for supported type tags)
  • explicit_notify and construct are flags, which will be passed to the param spec builder: builder.flags(ParamFlags::CONSTRUCT | ParamFlags::EXPLICIT_NOTIFY)
  • nick = "Great Integer" is a key/value pair, which becomes a method call on the builder: builder.nick("My Number").

There is one exception currently to how these arguments are interpreted: if the type tag is object, the first argument must be a gobject type. Example:

#[object(gtk::Button, more, flags, here, ...)]

Supported Types

Since this is an experiment, only a couple of types are supported at this time:

ParamSpec type type tag
ParamSpecBoolean boolean
ParamSpecBoxed -
ParamSpecChar char
ParamSpecDouble double
ParamSpecEnum -
ParamSpecFlags -
ParamSpecFloat float
ParamSpecGType -
ParamSpecInt int
ParamSpecInt64 int64
ParamSpecLong long
ParamSpecObject object(some::glib::Object)
ParamSpecOverride -
ParamSpecParam -
ParamSpecPointer -
ParamSpecString string
ParamSpecUChar -
ParamSpecUInt -
ParamSpecUInt64 -
ParamSpecULong -
ParamSpecUnichar -
ParamSpecValueArray -
ParamSpecVariant -

Implementation blocks

A property definition must implement at least one of 'get' or 'set'. If only one is implemented, ParamFlags::READABLE or ParamFlags::WRITABLE flags are implicitly set correspondingly. If both are implemented, ParamFlags::READWRITE is implied.

Each block becomes part of the fn property and fn set_property methods respectively.

Example:

impl ObjectImpl for MyObject {
    properties! {
        #[int]
        "my-number" => {
            get { self.my_number.get() } // assuming `my_number` is `Cell<Value>` here for simplicity
            set { self.my_number.replace(value); }
        }
        #[string]
        "my-string" => {
            get { self.my_string.get() }
            set { self.my_string.replace(value); }
        }
    }
}

generates a property function like this:

fn property(&self, object: &Self::Type, id: usize, pspec: ParamSpec) -> Value {
    match id {
        1 => self.my_number().get(),
        2 => self.my_string().get(),
        _ => unimplemented!()
    }
}

and a corresponding set_property function like this:

fn set_property(&self, object: &Self::Type, id: usize, value: Value, pspec: ParamSpec) {
    match id {
        1 => {
            self.my_number().replace(value);
        }
        2 => {
            self.my_string().replace(value);
        }
        _ => unimplemented!()
    }
}

Future ideas

  • allow custom ParamSpec declarations, without having to extends the DSL:
    properties! {
        ...
        // a custom property ('spec' block required), denoted by `_`:
        _ => {
            spec { ParamSpecSomething::builder("my-custom-prop").build() }
            get { ... }
            set { ... }
        }
        ...
    }
    
  • support ParamSpecValueArray, by parsing nested declaration as first arg:
    properties! {
        ...
        // sth like ParamSpecValueArray::builder("my-string-array", ParamSpecString::builder("my-string").build()).flags(...).build()
        #[array(string(name = "my-string"), explicit_notify)]
        "my-string-array" => {
            ...
        }
        ...
    }
    
  • if many properties correspond to simple fields of the inner object struct, the get/set blocks could get repetitive. Possible shorthand:
    struct MyObject {
      x: Cell<i32>,
      y: Cell<i32>,
      z: Cell<i32>,
    }
    
    impl ObjectImpl for MyObject {
        properties! {
            #[int] "x" => cell(x),
            #[int] "y" => cell(y),
            #[int] "z" => cell(z),
        }
    }
    
    where cell(x) is eqivalent to
    get { self.x.get().to_value() }
    set { self.x.replace(value.get().unwrap()) }
    

Dependencies

~1.5MB
~33K SLoC