declarative

A proc-macro library that implements a generic DSL to create complex reactive view code easier to edit and maintain.
To use it, add to your Cargo.toml:
[dependencies.declarative]
version = '0.7.3'
To learn how to use macros, the quick way is to see the usage, or clone the repository to read the source code of the examples in alphabetical order and run them like this:
cargo run --example EXAMPLE_NAME
The examples depend on gtk-rs, so you should familiarize yourself with gtk-rs a bit before (gtk-rs book). Using Rust Analyzer's macro expansion tool would help to better understand by seeing the code generated by the macros.
In addition to macro features, the examples also show some usage patterns (templates, components, Elm, etc.). GTK has a pattern of its own due to its object orientation and declarative integrates well, but there is no example about it (it would be verbose and exclusive to GTK, while declarative is not GTK based).
The following is an implementation of the Elm architecture with gtk-rs:

use declarative::{block as view, clone, construct};
use gtk::{glib, prelude::*};
enum Msg { Increase, Decrease }
macro_rules! send { [$msg:expr => $tx:expr] => [$tx.send_blocking($msg).unwrap()] }
fn start(app: >k::Application) {
let (tx, rx) = async_channel::bounded(1);
let mut count = 0;
view![ gtk::ApplicationWindow window {
application: app
title: "My Application"
titlebar: >k::HeaderBar::new()
child: &_ @ gtk::Box {
orientation: gtk::Orientation::Vertical
spacing: 6
margin_top: 6
margin_bottom: 6
margin_start: 6
margin_end: 6
~
append: &_ @ gtk::Label {
label: "Count unchanged"
'bind set_label: &format!("The count is: {count}")
}
append: &_ @ gtk::Button {
label: "Increase" ~
connect_clicked: clone![tx; move |_| send!(Msg::Increase => tx)]
}
append: &_ @ gtk::Button::with_label("Decrease") {
connect_clicked: move |_| send!(Msg::Decrease => tx)
}
'consume refresh = move |count| bindings!()
}
} ];
let update = |count: &mut u8, msg| match msg {
Msg::Increase => *count = count.wrapping_add(1),
Msg::Decrease => *count = count.wrapping_sub(1),
};
glib::spawn_future_local(async move {
while let Ok(msg) = rx.recv().await {
update(&mut count, msg); refresh(count); }
});
window.present()
}
fn main() -> glib::ExitCode {
let app = gtk::Application::default();
app.connect_activate(start);
app.run()
}
To execute, run:
cargo run --example y_readme
Usage
This is a non-exhaustive summary of how declarative expands in the most common use cases. The code snippets on the left expand into the code blocks on the right, and assume the following at the beginning of the file:
#![allow(unused)]
use declarative::{block, construct, view};
use gtk::{prelude::*, Orientation::*, glib::object::IsA};
block! {
gtk::Frame { } gtk::Button { label: "Click" }
gtk::Label { label: "Text"; lines: 2 }
gtk::Window::new() { #[cfg(debug_assertions)]
add_css_class: "devel"
}
}
Items defined with the builder pattern are expanded in reverse order:
let gtk_window_new_3 = gtk::Window::new();
let gtk_label_2 = <gtk::Label>::builder().label("Text").lines(2).build();
let gtk_button_1 = <gtk::Button>::builder().label("Click").build();
let gtk_frame_0 = <gtk::Frame>::builder().build();
#[cfg(debug_assertions)]
gtk_window_new_3.add_css_class("devel");
The underscore (_) in an argument is a placeholder for the variable name of the item followed by the at sign (@).
block!(gtk::Frame {
child: &_ @ gtk::ScrolledWindow {
child: &_ @ gtk::Button { label: "Click" }
}
label_widget: &_ @ gtk::Label { }
});
Reverse order allows child items in the builder pattern.
let gtk_label_3 = <gtk::Label>::builder().build();
let gtk_button_2 = <gtk::Button>::builder().label("Click").build();
let gtk_scrolledwindow_1 = <gtk::ScrolledWindow>::builder()
.child(>k_button_2)
.build();
let gtk_frame_0 = <gtk::Frame>::builder()
.child(>k_scrolledwindow_1)
.label_widget(>k_label_3)
.build();
block! {
gtk::Box {
set_orientation: Vertical
set_spacing: 6
set_margin_start: 6
}!
gtk::Box::new(Horizontal, 6) {
set_margin_start: 6
}
}
In this case the items are expanded in the same order:
let gtk_box_0 = <gtk::Box>::default();
let gtk_box_new_1 = gtk::Box::new(Horizontal, 6);
gtk_box_0.set_orientation(Vertical);
gtk_box_0.set_spacing(6);
gtk_box_0.set_margin_start(6);
gtk_box_new_1.set_margin_start(6);
As always, the comma (,) separates arguments:
block!(gtk::Grid::new() {
attach: &_, 0, 0, 1, 1 @ gtk::Label { }
attach: &_, 1, 0, 1, 1 @ gtk::Label { }
});
let gtk_grid_new_0 = gtk::Grid::new();
let gtk_label_2 = <gtk::Label>::builder().build();
let gtk_label_1 = <gtk::Label>::builder().build();
gtk_grid_new_0.attach(>k_label_1, 0, 0, 1, 1);
gtk_grid_new_0.attach(>k_label_2, 1, 0, 1, 1);
Basically one at sign for each underscore. The first underscore is for the first @ item, and the last underscore is for the last @ item.
block!(gtk::Notebook::new() {
append_page: &_, Some(&_)
@ gtk::Button { label: "Click 1" }
@ gtk::Label { label: "Tab 1" }
append_page_menu: &_, Some(&_), Some(&_)
@ gtk::Button { label: "Click 2" }
@ gtk::Label { label: "Tab 2" }
@ gtk::Label { label: "Menu 2" }
});
let gtk_notebook_new_0 = gtk::Notebook::new();
let gtk_label_5 = <gtk::Label>::builder().label("Menu 2").build();
let gtk_label_4 = <gtk::Label>::builder().label("Tab 2").build();
let gtk_button_3 = <gtk::Button>::builder().label("Click 2").build();
let gtk_label_2 = <gtk::Label>::builder().label("Tab 1").build();
let gtk_button_1 = <gtk::Button>::builder().label("Click 1").build();
gtk_notebook_new_0.append_page(>k_button_1, Some(>k_label_2));
gtk_notebook_new_0.append_page_menu(>k_button_3, Some(>k_label_4), Some(>k_label_5));
The last underscore (_) without a corresponding at sign (@) indicates the variable name of the current item as an argument to a function (not a method).
fn main() {
block! {
gtk::Button { label: "Click"; set_all_margins: 6, &_ }
gtk::Label { label: "Text"; set_all_margins: 12, &_ }
}
}
fn set_all_margins(px: i32, widget: &impl IsA<gtk::Widget>) {
block!(ref widget { set_margin_bottom: px
set_margin_end: px
set_margin_start: px
set_margin_top: px
}); }
fn main() {
let gtk_label_1 = <gtk::Label>::builder().label("Text").build();
let gtk_button_0 = <gtk::Button>::builder().label("Click").build();
set_all_margins(6, >k_button_0);
set_all_margins(12, >k_label_1);
}
fn set_all_margins(px: i32, widget: &impl IsA<gtk::Widget>) {
let _ = widget;
widget.set_margin_bottom(px);
widget.set_margin_end(px);
widget.set_margin_start(px);
widget.set_margin_top(px);
}
block!(gtk::Box::new(Vertical, 6) vert_box {
append: &_ @ gtk::Button my_button { }
});
let vert_box = gtk::Box::new(Vertical, 6);
let my_button = <gtk::Button>::builder().build();
vert_box.append(&my_button);
block! {
gtk::Box {
orientation: Vertical
~ set_spacing: 6
}
gtk::Box {
orientation: Horizontal
build; ~~ set_spacing: 12
}
}
Methods outside the builder pattern are called in the same order.
let gtk_box_1 = <gtk::Box>::builder()
.orientation(Horizontal)
.build();
let gtk_box_0 = <gtk::Box>::builder()
.orientation(Vertical)
.build();
gtk_box_0.set_spacing(6);
gtk_box_1.set_spacing(12);
block! {
gtk::Box {
orientation: Vertical
~> set_spacing: 6
}
gtk::Box {
orientation: Horizontal
build;
~~>
set_spacing: 12
}
}
let gtk_box_0 = <gtk::Box>::builder()
.orientation(Vertical)
.build();
let gtk_box_1 = <gtk::Box>::builder()
.orientation(Horizontal)
.build();
gtk_box_0.set_spacing(6);
gtk_box_1.set_spacing(12);
block! { gtk::Box::builder() {
orientation: Vertical
spacing: 6
}!
gtk::Box::builder() {
orientation: Horizontal
~ set_spacing: 12
}!
}
This way also defines variables in reverse order, unless > is added after ~ or ~~:
let gtk_box_builder_1 = gtk::Box::builder()
.orientation(Horizontal)
.build();
let gtk_box_builder_0 = gtk::Box::builder()
.orientation(Vertical)
.spacing(6)
.build();
gtk_box_builder_1.set_spacing(12);
block!(gtk::Stack stack {
bind_property: "hexpand", &stack, "vexpand"
'back { bidirectional; sync_create; }
add_child: &_ @ gtk::Label { label: "Label" }
'back { set_title: "Page 1"; set_name: "page_1" }!
add_child: &_ @ gtk::Button { } 'back my_page { set_title: "Page 2" }!
}!);
let stack = <gtk::Stack>::default();
let gtk_button_3 = <gtk::Button>::builder().build();
let gtk_label_1 = <gtk::Label>::builder().label("Label").build();
stack.bind_property("hexpand", &stack, "vexpand")
.bidirectional()
.sync_create()
.build();
let back_2 = stack.add_child(>k_label_1);
back_2.set_title("Page 1");
back_2.set_name("page_1");
let my_page = stack.add_child(>k_button_3);
my_page.set_title("Page 2");
let device = "phone";
block!(gtk::Box {
spacing: if device == "phone" { 6 } else { 12 }
if device == "laptop" {
set_margin_end: 12
set_orientation: Horizontal
} else {
set_margin_bottom: 12
set_orientation: Vertical
}
match device { "phone" => set_tooltip_text: Some("phone")
"laptop" => {
set_tooltip_text: Some("laptop")
set_css_classes: &["laptop"]
}
_ => { set_hexpand: false; set_vexpand: false }
}
margin_end: 6; margin_bottom: 6
});
let gtk_box_0 = <gtk::Box>::builder()
.spacing(if device == "phone" { 6 } else { 12 })
.margin_end(6)
.margin_bottom(6)
.build();
if device == "laptop" {
gtk_box_0.set_margin_end(12);
gtk_box_0.set_orientation(Horizontal);
} else {
gtk_box_0.set_margin_bottom(12);
gtk_box_0.set_orientation(Vertical);
}
match device {
"phone" => {
gtk_box_0.set_tooltip_text(Some("phone"));
}
"laptop" => {
gtk_box_0.set_tooltip_text(Some("laptop"));
gtk_box_0.set_css_classes(&["laptop"]);
}
_ => {
gtk_box_0.set_hexpand(false);
gtk_box_0.set_vexpand(false);
}
}
block!(gtk::Box root {
spacing: 6
'bind set_margin_top: root.spacing()
'bind set_margin_bottom: root.spacing()
'bind { set_margin_start: root.spacing()
set_margin_end: root.spacing()
} ~
append: &_ @ gtk::Label label { }
connect_spacing_notify: move |root| {
let text = format!("spacing is {}", root.spacing());
bindings! { } label.set_label(&text);
}
});
let label = <gtk::Label>::builder().build();
let root = <gtk::Box>::builder().spacing(6).build();
root.append(&label);
root.connect_spacing_notify(move |root| {
let text = alloc::__export::must_use({
alloc::fmt::format(alloc::__export::format_args!(
"spacing is {}",
root.spacing()
))
});
root.set_margin_top(root.spacing());
root.set_margin_bottom(root.spacing());
{
root.set_margin_start(root.spacing());
root.set_margin_end(root.spacing());
}
label.set_label(&text);
});
block!(gtk::Box root {
spacing: 6
~ 'bind #set_margin_top: root.spacing()
'bind #set_margin_bottom: root.spacing()
'bind #{
set_margin_start: root.spacing()
set_margin_end: root.spacing()
}
connect_spacing_notify: |root| bindings!()
});
Bindings will be expanded as usual (like a normal property assignment) and in the bindings!() placeholder macro.
let root = <gtk::Box>::builder().spacing(6).build();
root.set_margin_top(root.spacing());
root.set_margin_bottom(root.spacing());
{
root.set_margin_start(root.spacing());
root.set_margin_end(root.spacing());
}
root.connect_spacing_notify(|root| {
root.set_margin_top(root.spacing());
root.set_margin_bottom(root.spacing());
{
root.set_margin_start(root.spacing());
root.set_margin_end(root.spacing());
}
});
let mut resets = 0;
block!(gtk::Box root {
margin_top: 6; margin_start: 6
~
'bind #set_margin_bottom: root.margin_top()
'consume top_to_bottom = |root: >k::Box| bindings!()
'bind #set_margin_end: root.margin_start()
'consume start_to_end = |root: >k::Box| bindings!()
connect_margin_top_notify: top_to_bottom
connect_margin_start_notify: start_to_end
'bind { set_margin_top: 6; set_margin_start: 6 }
'consume mut reset = || { resets += 1; bindings!() }
});
reset()
let mut resets = 0;
let root = <gtk::Box>::builder().margin_top(6).margin_start(6).build();
root.set_margin_bottom(root.margin_top());
let top_to_bottom = |root: >k::Box| {
root.set_margin_bottom(root.margin_top());
};
root.set_margin_end(root.margin_start());
let start_to_end = |root: >k::Box| {
root.set_margin_end(root.margin_start());
};
root.connect_margin_top_notify(top_to_bottom);
root.connect_margin_start_notify(start_to_end);
let mut reset = || {
resets += 1;
{
{
root.set_margin_top(6);
root.set_margin_start(6);
}
}
};
reset()
#[view(gtk::ToggleButton root {
label: "any label"
'bind set_label: argument
'bind set_active: !root.is_active()
})] fn main() {
expand_view_here! { } let toggle = move |argument| bindings!();
toggle("new label");
}
fn main() {
let root = <gtk::ToggleButton>::builder().label("any label").build();
let toggle = move |argument| {
root.set_label(argument);
root.set_active(!root.is_active());
};
toggle("new label");
}
#[view] mod module { use super::*;
fn function() {
expand_view_here! { } let toggle = move |argument| bindings!();
toggle("new label");
}
view!(gtk::ToggleButton root {
label: "any label"
'bind set_label: argument
'bind set_active: !root.is_active()
});
}
mod module {
use super::*;
fn function() {
let root = <gtk::ToggleButton>::builder().label("any label").build();
let toggle = move |argument| {
root.set_label(argument);
root.set_active(!root.is_active());
};
toggle("new label");
}
}
A template is simply a struct that references some widgets in the view. Several structs can be defined, but each one is followed by one or more items.
#[view]
mod module {
pub fn template(text: &str) -> Template {
use super::*;
expand_view_here! { }
Template { int: 7, float: 7.0, root, button, label }
}
view! { pub struct Template { int: i32, pub float: f32 }
gtk::Box pub root { append: &_ @ gtk::Button { label: text }
append: &_ @ gtk::Button pub button { }
append: &_ @ gtk::Label ref label { } }! }
}
fn main() {
block!(gtk::Frame { child: &_.root @ module::template("text") {
root.set_spacing: 6
button.set_label: "Button"
ref root { set_margin_top: 6
set_margin_bottom: 6
}
}
});
}
mod module {
pub fn template(text: &str) -> Template {
use super::*;
let root = <gtk::Box>::default();
let label = <gtk::Label>::builder().build();
let button = <gtk::Button>::builder().build();
let gtk_button_0 = <gtk::Button>::builder().label(text).build();
root.append(>k_button_0);
root.append(&button);
root.append(&label);
Template { int: 7, float: 7.0, root, button, label }
}
pub struct Template {
int: i32,
pub float: f32,
pub root: gtk::Box,
pub button: gtk::Button,
label: gtk::Label,
}
}
fn main() {
let module_template_1 = module::template("text");
let gtk_frame_0 = <gtk::Frame>::builder()
.child(&module_template_1.root)
.build();
module_template_1.root.set_spacing(6);
module_template_1.button.set_label("Button");
let _ = module_template_1.root;
module_template_1.root.set_margin_top(6);
module_template_1.root.set_margin_bottom(6);
}
#[view { // a struct is declared inside the view
pub struct Template { int: i32, pub float: f32 }
// due to `pub` this item is included in the...
gtk::Box pub root { // template as a public field
append: &_ @ gtk::Button { label: text }
append: &_ @ gtk::Button pub button { }
// the first button is not included
append: &_ @ gtk::Label ref label { } // ...
}! // due to `ref` the label is included as private
}]
pub fn template(text: &str) -> Template {
expand_view_here! { }
Template { int: 7, float: 7.0, root, button, label }
}
fn main() {
block!(gtk::Frame { child: &_.root @ template("text") {
root.set_spacing: 6
button.set_label: "Button"
ref root { set_margin_top: 6
set_margin_bottom: 6
}
}
});
}
Almost the same code as the previous row is generated, but the view code is written in an attribute to avoid using a module.
pub struct Template {
int: i32,
pub float: f32,
pub root: gtk::Box,
pub button: gtk::Button,
label: gtk::Label,
}
pub fn template(text: &str) -> Template {
let root = <gtk::Box>::default();
let label = <gtk::Label>::builder().build();
let button = <gtk::Button>::builder().build();
let gtk_button_0 = <gtk::Button>::builder().label(text).build();
root.append(>k_button_0);
root.append(&button);
root.append(&label);
Template { int: 7, float: 7.0, root, button, label }
}
fn main() {
let template_1 = template("text");
let gtk_frame_0 = <gtk::Frame>::builder().child(&template_1.root).build();
template_1.root.set_spacing(6);
template_1.button.set_label("Button");
let _ = template_1.root;
template_1.root.set_margin_top(6);
template_1.root.set_margin_bottom(6);
}
#[view(pub, int: i32, pub float: f32)]
impl Template {
fn new(text: &str) -> Self {
expand_view_here! { }
Self { int: 7, float: 7.0, root, label }
}
view!(gtk::Box ref root {
append: &_ @ gtk::Button { label: text }
append: &_ @ gtk::Label pub label { }
}!);
}
The attribute is used for impl, where the first argument is the visibility of the struct to be generated (optional) and the rest of the arguments are additional fields, although it is also possible to leave the attribute empty for impl and define the struct in the view. Both ways expand equally:
pub struct Template {
int: i32,
pub float: f32,
root: gtk::Box,
pub label: gtk::Label,
}
impl Template {
fn new(text: &str) -> Self {
let root = <gtk::Box>::default();
let label = <gtk::Label>::builder()
.build();
let gtk_button_0 = <gtk::Button>::builder()
.label(text).build();
root.append(>k_button_0);
root.append(&label);
Self { int: 7, float: 7.0, root, label }
}
}
#[view] impl Template { view! {
pub struct Template { int: i32, pub float: f32 }
gtk::Box ref root {
append: &_ @ gtk::Button { label: text }
append: &_ @ gtk::Label pub label { }
}!
}
fn new(text: &str) -> Self {
expand_view_here! { }
Self { int: 7, float: 7.0, root, label }
}
}
In addition to visibility, the first attribute argument can be the name of a template struct, or its generics, or two of them at once, or all three at once.
#[view(pub Template<'a, T>, text: &'a T)]
mod module {
type Any = Box<dyn std::any::Any>;
fn template<'a, T>(text: &'a T) -> Template<'a, T> {
use super::*;
expand_view_here! { }
let frame = Box::new(frame);
Template { root, text, my_page, frame }
}
view!(gtk::Stack pub root {
add_child: &_ @ gtk::Frame ref frame as Any { }
'back pub my_page as gtk::StackPage { }!
}!);
}
mod module {
type Any = Box<dyn std::any::Any>;
fn template<'a, T>(text: &'a T) -> Template<'a, T> {
use super::*;
let root = <gtk::Stack>::default();
let frame = <gtk::Frame>::builder().build();
let my_page = root.add_child(&frame);
let frame = Box::new(frame);
Template { root, text, my_page }
}
pub struct Template<'a, T> {
text: &'a T,
pub root: gtk::Stack,
frame: Any,
pub my_page: gtk::StackPage,
}
}
struct CustomBox { expanded: bool, margin: i32,
name: &'static str,
}
#[view(Template)]
impl CustomBox {
fn start(self) -> Template {
expand_view_here! { } Template { root, frame } }
view!(gtk::Box ref root {
margin_start: self.margin
margin_end: self.margin
hexpand: self.expanded
vexpand: self.expanded
~
append: &_ @ gtk::Label {
label: format!("Name: {}", self.name)
}
append: &_ @gtk::Frame ref frame { }
});
}
fn main() {
block!(gtk::Frame {
child: &_.root @ CustomBox {
expanded: true; margin: 12; name: "First"
~
root.set_spacing: 6
frame.set_child: Some(&_) @ gtk::Label { }
}? });
}
struct CustomBox {
expanded: bool,
margin: i32,
name: &'static str,
}
struct Template {
root: gtk::Box,
frame: gtk::Frame,
}
impl CustomBox {
fn start(self) -> Template {
let frame = <gtk::Frame>::builder().build();
let gtk_label_0 = <gtk::Label>::builder()
.label(alloc::__export::must_use({
alloc::fmt::format(alloc::__export::format_args!("Name: {}", self.name))
}))
.build();
let root = <gtk::Box>::builder()
.margin_start(self.margin)
.margin_end(self.margin)
.hexpand(self.expanded)
.vexpand(self.expanded)
.build();
root.append(>k_label_0);
root.append(&frame);
Template { root, frame }
}
}
fn main() {
let gtk_label_2 = <gtk::Label>::builder().build();
let custombox_1 = (CustomBox {
expanded: true,
margin: 12,
name: "First",
}).start();
let gtk_frame_0 = <gtk::Frame>::builder().child(&custombox_1.root).build();
custombox_1.root.set_spacing(6);
custombox_1.frame.set_child(Some(>k_label_2));
}
Basic maintenance
The following commands must be executed and must not give any problems:
cargo check -p declarative-macros
cargo clippy -p declarative-macros
cargo test -p declarative-macros
cargo check
cargo clippy
cargo test
If you need a changelog, maybe the commit log will help (the last ones try to have the most important details).
License
Licensed under either of Apache License, Version 2.0 (Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0) or MIT license (MIT.txt or http://opensource.org/licenses/MIT) at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.