1 unstable release
0.1.0 | Sep 4, 2022 |
---|
#649 in Procedural macros
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 objectobject
: the outer objectid
: 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
: aglib::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:
- A type declaration attribute (described below), e.g.
#[int(minimum = 3, maximum = 27)]
- Zero or more attributes of the form
#[doc = "..."]
(the compiler transforms doc comments into these). These doc comments are all concatenated and stored in theblurb
of the param spec. - 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 ofParamSpec*Builder
to use (see table below for supported type tags) explicit_notify
andconstruct
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:
wherestruct 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), } }
cell(x)
is eqivalent toget { self.x.get().to_value() } set { self.x.replace(value.get().unwrap()) }
Dependencies
~1.5MB
~37K SLoC