vgtk 0.1.0

A declarative UI framework for GTK

A declarative UI framework built on GTK and Gtk-rs.

Overview

vgtk is a GUI framework built on GTK using what might be called the "Model-View-Update" pattern, as popularised in Elm and Redux, in addition to a component model similar to React. Its primary inspiration is the Yew web framework for Rust, from which it inherits most of its more specific ideas.

To facilitate writing GTK UIs in a declarative style, vgtk implements an algorithm similar to DOM diffing, but for GTK's widget tree, which has turned out to be considerably less trivial than diffing a well structured tree like the DOM, but as a first draft at least it gets the job done.

More importantly, vgtk also provides the gtk! macro allowing you to write your declarative UI in a syntax very similar to JSX.

Show Me!

use vgtk::{ext::*, gtk, run, Component, UpdateAction, VNode};
use vgtk::lib::{gtk::*, gio::ApplicationFlags};

#[derive(Clone, Default, Debug)]
struct Model {
counter: usize,
}

#[derive(Clone, Debug)]
enum Message {
Inc,
Exit,
}

impl Component for Model {
type Message = Message;
type Properties = ();

fn update(&mut self, message: Message) -> UpdateAction<Self> {
match message {
Message::Inc => {
self.counter += 1;
UpdateAction::Render
}
Message::Exit => {
vgtk::quit();
UpdateAction::None
}
}
}

fn view(&self) -> VNode<Model> {
gtk! {
<Application::new_unwrap(None, ApplicationFlags::empty())>
<Window border_width=20 on destroy=|_| Message::Exit>
<HeaderBar title="inc!" show_close_button=true />
<Box spacing=10 halign=Align::Center>
<Label label=self.counter.to_string() />
<Button label="inc!" image="add" always_show_image=true
on clicked=|_| Message::Inc />
</Box>
</Window>
</Application>
}
}
}

fn main() {
std::process::exit(run::<Model>());
}

Prerequisites

The vgtk documentation assumes you already have a passing familiarity with GTK and its Rust bindings. It makes little to no effort to explain how GTK works or to catalogue which widgets are available. Please refer to the Gtk-rs documentation or that of GTK proper for this.

The Component Model

The core idea of vgtk is the Component. A component, in practical terms, is a composable tree of Gtk widgets, often a window, reflecting a block of application state. You can write your application as a single component, but you can also embed a component inside another component, which makes sense for parts of your UI you tend to repeat, or just for making an easier to use interface for a common Gtk widget type.

Your application starts with a component that manages an Application object. This Application in turn will have one or more Windows attached to it, either directly inside the component or as subcomponents. Windows in turn contain widget trees.

You can think of a component as an MVC system, if that's something you're familiar with: it contains some application state (the Model), a method for rendering that state into a tree of GTK widgets (the View) and a method for updating that state based on external inputs like user interaction (the Controller). You can also think of it as mapping almost directly to a React component, if you're more familiar with that, even down to the way it interacts with the JSX syntax.

Building A Component

A component in vgtk is something which implements the Component trait, providing the two crucial methods view and update. Your top level component should have a view function which returns a GTK Application object, or, rather, a "virtual DOM" tree which builds one.

The view function's job is to examine the current state of the component (usually contained within the type of the Component itself) and return a UI tree which reflects it. This is its only job, and however much you might be tempted to, it must not do anything else, especially anything that might block the thread or cause a delayed result.

Responding to user interaction, or other external inputs, is the job of the update function. This takes an argument of the type Component::Message and updates the component's state according to the contents of the message. This is the only place you're allowed to modify the contents of your component, and every way to change it should be expressed as a message you can send to your update function.

update returns an UpdateAction describing one of three outcomes: either, None, meaning nothing significant changed as a result of the message and we don't need to update the UI, or Render, meaning you made a change which should be reflected in the UI, causing the framework to call your view method and re-render the UI. Finally, you can also return Defer with a Future in case you need to do some I/O or a similar asynchronous task - the Future should resolve to a Component::Message which will be passed along to update when the Future resolves.

Signal Handlers

Other than UpdateAction::Defer, where do these messages come from? Usually, they will be triggered by user interaction with the UI. Using the gtk! macro, you can attach signal handlers to GTK signals which respond to a signal by sending a message to the current component.

For instance, a GTK Button has a clicked signal which is triggered when the user clicks on the button, as the name suggests. Looking at the connect_clicked method, we see that it takes a single &Self argument, representing the button being clicked. In order to listen to this signal, we attach a closure with a similar function signature to the button using the on syntax. The closure always takes the same arguments as the connect_* callback, but instead of returning nothing it returns a message of the component's message type. This message will be passed to the component's update method by the framework.

# use vgtk::{gtk, VNode, Component};
# use vgtk::lib::gtk::{Button, ButtonExt};
# #[derive(Clone, Debug)] enum Message { ButtonWasClicked }
# #[derive(Default)] struct Comp;
# impl Component for Comp { type Message = Message; type Properties = (); fn view(&self) -> VNode<Self> {
gtk! {
<Button label="Click me" on clicked=|_| Message::ButtonWasClicked />
}
# }}

This will cause a Message::ButtonWasClicked message to be sent to your component's update function when the user clicks the button.

Signal handlers can also be declared as async, which will cause the framework to wrap the handler in an async {} block and await the message result before passing it on to your update function. For instance, this very contrived example shows a message dialog asking the user to confirm clicking the button before sending the ButtonWasClicked message.

# use vgtk::{gtk, VNode, Component};
# use vgtk::lib::gtk::{Button, ButtonExt, DialogFlags, MessageType, ButtonsType};
# #[derive(Clone, Debug)] enum Message { ButtonWasClicked }
# #[derive(Default)] struct Comp;
# impl Component for Comp { type Message = Message; type Properties = (); fn view(&self) -> VNode<Self> {
gtk! {
<Button label="Click me" on clicked=async |_| {
vgtk::message_dialog(
vgtk::current_window().as_ref(),
DialogFlags::MODAL, MessageType::Info, ButtonsType::Ok, true,
"Please confirm that you clicked the button."
).await;
Message::ButtonWasClicked
} />
}
# }}

The gtk! Syntax

The syntax for the gtk! macro is similar to JSX, but with a number of necessary extensions.

A GTK widget (or, in fact, any GLib object, but most objects require widget children) can be constructed using an element tag. Attributes on that tag correspond to get_* and set_* methods on the GTK widget. Thus, to construct a GTK Button calling set_label to set its label:

# use vgtk::{gtk, VNode};
# use vgtk::lib::gtk::{Button, ButtonExt};
# fn view() -> VNode<()> {
gtk! {
<Button label="Click me" />
}
# }

A GTK container is represented by an open/close element tag, with child tags representing its children.

# use vgtk::{gtk, VNode};
# use vgtk::lib::gtk::{Button, ButtonExt, Box, BoxExt, Orientation, OrientableExt};
# fn view() -> VNode<()> {
gtk! {
<Box orientation=Orientation::Horizontal>
<Button label="Left click" />
<Button label="Right click" />
</Box>
}
# }

If a widget has a constructor that takes arguments, you can use that constructor in place of the element's tag name. This syntax should only be used in cases where a widget simply cannot be constructed using properties alone, because the differ isn't able to update arguments that may have changed in constructors once the widget has been instantiated. It should be reserved only for when it's absolutely necessary, such as when constructing an Application, which doesn't implement Buildable and therefore can't be constructed in any way other than through a constructor method.

# use vgtk::{gtk, VNode, ext::ApplicationHelpers};
# use vgtk::lib::{gtk::Application, gio::ApplicationFlags};
# fn view() -> VNode<()> {
gtk! {
<Application::new_unwrap(None, ApplicationFlags::empty()) />
}
# }

Sometimes, a widget has a property which must be set through its parent, such as a child's expand and fill properties inside a Box. These properties correspond to set_child_* and get_child_* methods on the parent, and are represented as attributes on the child with the parent's type as a namespace, like this:

# use vgtk::{gtk, VNode};
# use vgtk::lib::gtk::{Button, ButtonExt, Box, BoxExt};
# fn view() -> VNode<()> {
gtk! {
<Box>
<Button label="Click me" Box::expand=true Box::fill=true />
</Box>
}
# }

The final addition to the attribute syntax pertains to when you need to qualify an ambiguous method name. For instance, a MenuButton implements both WidgetExt and MenuButtonExt, both of which contains a set_direction method. In order to let the compiler know which one you mean, you can qualify it with an @ and the type name, like this:

# use vgtk::{gtk, VNode};
# use vgtk::lib::gtk::{MenuButton, MenuButtonExt, WidgetExt, ArrowType, TextDirection};
# fn view1() -> VNode<()> { gtk!{
<MenuButton @MenuButtonExt::direction=ArrowType::Down />
# }} fn view2() -> VNode<()> { gtk! {
<MenuButton @WidgetExt::direction=TextDirection::Ltr />
# }}

Interpolation

The gtk! macro's parser tries to be smart about recognising Rust expressions as attribute values, but it's not perfect. If the parser chokes on some particularly complicated Rust expression, you can always wrap an attribute's value in a {} block, as per JSX.

This curly bracket syntax is also used to dynamically insert child widgets into a tree. You can insert a code block in place of a child widget, which should return an iterator of widgets that will be appended by the macro when rendering the virtual tree.

For instance, to dynamically generate a series of buttons, you can do this:

# use vgtk::{gtk, VNode};
# use vgtk::lib::gtk::{Button, ButtonExt, Box, BoxExt, Orientation};
# fn view() -> VNode<()> {
gtk! {
<Box>
{
(1..=5).map(|counter| {
gtk! { <Button label=format!("Button #{}", counter) /> }
})
}
</Box>
}
# }

Subcomponents

Components are designed to be composable, so you can place one component inside another. The gtk! syntax for that looks like this:

<@Subcomponent attribute_1="hello" attribute_2=1337 />

The subcomponent name (prefixed by @ to distinguish it from a GTK object) maps to the type of the component, and each attribute maps directly to a property on its Component::Properties type. When a subcomponent is constructed, the framework calls its create method with the property object constructed from its attributes as an argument.

A subcomponent needs to implement create and change in addition to update and view. The default implementations of these methods will panic with a message telling you to implement them.

Subcomponents do not support signal handlers, because a component is not a GTK object. You'll have to use the Callback type to communicate between a subcomponent and its parent.

This is what a very simple button subcomponent might look like:

# use vgtk::{gtk, VNode, UpdateAction, Component, Callback};
# use vgtk::lib::gtk::{Button, ButtonExt};
#[derive(Clone, Debug, Default)]
pub struct MyButton {
pub label: String,
pub on_clicked: Option<Callback<()>>,
}

#[derive(Clone, Debug)]
pub enum MyButtonMessage {
Clicked
}

impl Component for MyButton {
type Message = MyButtonMessage;
type Properties = Self;

fn create(props: Self) -> Self {
props
}

fn change(&mut self, props: Self) -> UpdateAction<Self> {
*self = props;
UpdateAction::Render
}

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
match msg {
MyButtonMessage::Clicked => {
if let Some(ref callback) = self.on_clicked {
callback.send(());
}
}
}
UpdateAction::None
}

fn view(&self) -> VNode<Self> {
gtk! {
<Button label=self.label.clone() on clicked=|_| MyButtonMessage::Clicked />
}
}
}

Note that because this component doesn't have any state other than its properties, we just make Self::Properties equal to Self, there's no need to keep two identical types around for this purpose. Note also that the callback passes a value of type (), because the clicked signal doesn't contain any useful information besides the fact that it's being sent.

This is how you'd use this subcomponent with a callback inside the view method of a parent component:

# use vgtk::{gtk, VNode, Component, Callback};
# use vgtk::lib::gtk::{Button, ButtonExt, Box, BoxExt, Orientation, Label, LabelExt};
# #[derive(Clone, Debug, Default)]
# pub struct MyButton {
#     pub label: String,
#     pub on_clicked: Option<Callback<()>>,
# }
# impl Component for MyButton {
#     type Message = ();
#     type Properties = Self;
#     fn view(&self) -> VNode<Self> { todo!() }
# }
# #[derive(Clone, Debug)] enum ParentMessage { ButtonClicked }
# #[derive(Default)] struct Parent;
# impl Component for Parent { type Message = ParentMessage; type Properties = ();
fn view(&self) -> VNode<Self> {
gtk! {
<Box>
<Label label="Here is a button:" />
<@MyButton label="Click me!" on_clicked=|_| ParentMessage::ButtonClicked />
</Box>
}
}
# }

Note that the return type of the on_clicked callback is the message type of the parent component - when the subcomponent is constructed, the parent component will wire any callback up to its update function for you automatically with a bit of unsafe trickery, so that the subcomponent doesn't have to carry the information about what type of parent component it lives within inside its type signature. It'll just work, with nary a profunctor in sight.

Logging

vgtk uses the log crate for debug output. You'll need to provide your own logger for this; the example projects show how to set up pretty_env_logger for logging to the standard output. To enable it, set the RUST_LOG environment variable to debug when running the examples. You can also use the value vgtk=debug to turn on debug output only for vgtk, if you have other components using the logging framework. At log level debug, it will log the component messages received by your components, which can be extremely helpful when trying to track down a bug in your component's interactions. At log level trace, you'll also get a lot of vgtk internal information that's likely only useful if you're debugging the framework.

Work In Progress

While this framework is currently sufficiently usable that we can implement TodoMVC in it, there are likely to be a lot of rough edges still to be uncovered. In particular, a lot of properties on GTK objects don't map cleanly to get_* and set_* methods in the Gtk-rs mappings, as required by the gtk! macro, which has necessitated the collection of hacks in vgtk::ext. There are likely many more to be found in widgets as yet unused.

As alluded to previously, the diffing algorithm is also complicated by the irregular structure of the GTK widget tree. Not all child widgets are added through the Container API, and while most of the exceptions are already implemented, there will be more. There's also a lot of room yet for optimisation in the diffing algorithm itself, which is currently not nearly as clever as the state of the art in the DOM diffing world.

Not to mention the documentation effort.

In short, pull requests are welcome.