⚠ experimental project ⚠ # gtk-properties-macro This package contains a macro that makes it easier to declare object properties when using [gtk-rs](https://gtk-rs.org/). For a general introduction to GTK properties in Rust, please refer to the ["Properties" chapter of the gtk-rs book](https://gtk-rs.org/gtk4-rs/stable/latest/book/g_object_properties.html#properties). ## 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](https://gtk-rs.org/gtk4-rs/stable/latest/book/g_object_properties.html#adding-properties-to-custom-gobjects). 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: ```rust 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`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/value/struct.Value.html) ``` 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`](https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/value/struct.Value.html) 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` 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, y: Cell, z: Cell, } 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()) } ```