Bodil dot lol


Building TodoMVC With vgtk

The vgtk project started out as a side effect of one of my "must write a text editor" phases, as many things do. It triggers a review of the state of UI development in my current favourite language, and sometimes it triggers a ground-up attempt to construct an ad-hoc, informally-specified, bug-ridden, slow implementation of half of Elm, when I decide the state of the art is insufficient for my tastes. Usually, if the ground is sparsely trodden, I never get further than building some of the developer tooling necessary to build the UI tooling I need to build my text editor.

In this latest case, thanks to the superlative state of Rust's developer tooling—and its low level bindings for at least one sufficiently qualified UI toolkit—I've gotten to the point where I've been able to build UI tooling that appeals to my idea of what UI tooling should look and feel like. So far, I've yet to build the text editor, but I've come to accept by now that it will only ever exist as a Platonic ideal, serving only as a motivating force to get me started building more useful things.

I'll try to introduce vgtk, idea by idea, by way of a tutorial. You'll need to have a working knowledge of Rust to follow along. You shouldn't need to know GTK already, but you may need to be prepared to consult GTK documentation to understand certain things fully.

Where Did It Come From? #

At first glance, a vgtk code example (this one from the project's README file) tends to look a little disorienting:

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>
    }
}

What the eye sees is React. When stepping back, it's clearly Rust code, but the JSX like syntax of the view function tends to draw your attention and make you wonder if you're seeing Rust or something else. This is intentional: JSX has grown from that weird idea that made people dismiss React out of hand to practically a de facto standard of declaring HTML based UI components, and it turns out that it translates well to any UI toolkit based around tree structures, which happens to be all of them, so (thanks to Rust's powerful procedural macros letting us embed very nearly any DSL we want into Rust code) we can leverage the familiarity of JSX to reduce our learning curve somewhat. We need to extend JSX's syntax a little bit to work around the peculiarities of GTK, but it still looks largely like JSX. And, like JSX, it translates directly into code. In Rust, that means we can type check the markup: if you pass the wrong kind of argument to an attribute, for instance, or mistype the name of an element, you will get a type error from the compiler.

As well as the JSX syntax, vgtk has borrowed a slightly simplified version of React's component model. vgtk is structured around Components, which can be nested inside the JSX structures of each other, just like in React, in order to build reusable UI components. This model is very similar to the one found in the Yew web framework for Rust, which is perhaps the chief source of inspiration for vgtk. If you like what you see here, but you want to do web development instead, you'll like Yew a lot.

Components follow the "Model-View-Update" pattern of the Elm architecture. Like in Elm and Redux, data flows in a single direction through the component: the view function describes how and which user input is handled, user input flows into the update function to adjust the component state (the "Model"), the update function feeds state into the view function, and so on in an endless cycle. If you've heard of MVC, "Model-View-Controller," this is an expression of that basic pattern, but more constrained—in a good way, as it turns out—in how it handles data flow.

Buzzword Compliant Rust #

GTK is built on top of GLib's event loop, which, it turns out, is like a battle hardened C version of async-std or Tokio (at least if you consider it along with GIO). The Gtk-rs project provides, in addition to the GTK bindings, a fully functional Future executor on top of GLib, which means that we can use Rust's async/await style of async programming when writing GTK based applications. vgtk embraces this: Components are async tasks, signal handlers are async functions, and the update function can spawn async jobs as needed.

A drawback here is that GLib and GIO bindings aren't quite as nice to use as async-std and Tokio, from a Rust programmer's perspective, and I might wish for a companion library to vgtk which provides a more ergonomic interface to GIO for when your components need to talk to networks and file systems. I might even wish that we could have async I/O libraries that were independent of the executors they run on, but this seems to remain a utopian ideal.

Getting Started #

Enough talk, let's write some code. First, you'll need a working Rust environment and a working GTK installation. For the latter, it's an easy install unless you're on Windows, for which I can only say it's possible, I've gotten it working on multiple Windows machines, but you must follow the instructions to the letter, as tedious as it is.

Before we start, be aware that the Rust compiler is going to complain at you about "recursion limit reached" a couple of times as you work through this tutorial. Whenever this happens, it tells you to add an attribute to your crate, that looks like #![recursion_limit = "some number"]. Just add that as a new line at the top of your file, or replace the previous one if it's happened before, and rustc will stop complaining. Don't worry about it, it's a limit rustc enforces to keep things spiralling out of control, but we've got a lot of room to expand before it gets to be a problem. We're not taking it anywhere near problem territory, but it's a truth generally acknowledged that anything you can do in Rust that's really worth doing will exceed the default recursion limit.

We'll use cargo generate to start a new vgtk project. If you don't have cargo generate installed, I recommend that you do so now (cargo install cargo-generate), or check out the cargo-template-vgtk repo manually.

$ cargo generate --git https://github.com/bodil/cargo-template-vgtk --name vgtk-todomvc
$ cd vgtk-todomvc
$ cargo run

After a while, your project will finish compiling, and your very first shadow of a vgtk application should appear on your desktop:

Let's open the src/main.rs file that cargo-generate made for us and see what's in it.

First of all, there's a model:

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

There's no state needed for the little window with a label in it that you just saw, so the model, for now, contains nothing.

Next, there's the message enum:

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

The only enum variant is Message::Exit, which is all the application does right now: when you click the window close button, it exits.

Next, there's the Component implementation. This is where things get interesting.

We note the two associated type declarations, one for the message type we just declared and one for the component's properties. A top level component does not have properties, so the unit type () is used.

type Message = Message;
type Properties = ();

Next, there's the update function. All it does is respond to the Message::Exit message by exiting the application. It returns UpdateAction::None, which tells the system that it doesn't need to re-render the component when this happens.

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
    match msg {
        Message::Exit => {
            vgtk::quit();
            UpdateAction::None
        }
    }
}

And then there's the view function. This is where you declare how the application is rendered. The gtk! macro takes a markup tree and turns it into a bit of Rust code that generates a VNode<Model>, as you can see the return type requires. A VNode is something similar to a DOM node, except it describes the state of a tree of GTK widgets, without actually constructing any. The vgtk runtime will take this and render it into an actual tree of GTK widgets, or use it to efficiently update a previously rendered one.

fn view(&self) -> VNode<Model> {
    gtk! {
        <Application::new_unwrap(Some("com.example.vgtk-tutorial"), ApplicationFlags::empty())>
            <Window border_width=20 on destroy=|_| Message::Exit>
                <Label label="vgtk-tutorial" />
            </Window>
        </Application>
    }
}

There's a lot of syntax here. Let's start at the outer level with the Application widget.

Your top level component should render into an Application widget, containing any number of Windows. Each Window contains either one or two children: an optional header widget (in our case, we don't use one, which gets us system standard window decorations instead) plus a content widget—that's the Label.

Mostly, elements look the same as HTML elements. In some cases, we need to call special constructors, and Application is one of these cases. In fact, we need to call the new_unwrap constructor, which isn't even in Gtk-rs, but is provided by one of vgtk's helper traits. This is because the usual constructor, Application::new, returns a Result and the gtk! macro expects an Application, so new_unwrap just calls the regular constructor and then unwraps the result.

The gtk! macro accepts regular Rust constructor syntax in place of just an element name for this purpose, as you can see above. In general, though, try to avoid using constructors unless you really have to, because the values provided to a constructor can't be changed once constructed, so vgtk simply ignores these values when it's updating the widget tree after initial construction. On the other hand, values in element attributes will be updated properly.

If you're familiar with JSX, you'll also note that the gtk! macro is slightly smarter than JSX in that, instead of wrapping values in a {} block, you can provide Rust expressions directly, as with the border_width=20 attribute on the Window element. gtk!'s parser can handle most Rust expressions, but it isn't perfect, so if you throw something at it that it doesn't know how to handle, you can always just wrap it in a {} block.

The final bit of syntax to note is the signal handler on the Window: on destroy=|_| Message::Exit. Signal handlers start with the on keyword, and tells vgtk, in this case, that when the Window's destroy signal is emitted, we should call the provided callback function. This maps directly to the window's connect_destroy method, and the arguments to the callback are the same as for the callback that method takes. In this case, the only argument is a reference to the widget emitting the signal, which we don't care about, hence the _. The return value is different from the GTK callback, though: you should return a message of your component's message type, rather than nothing, to let the component know what to do next.

Finally, there's the main() function, which is just a bit of boilerplate to get the application up and running. It first initialises logging using pretty_env_logger, a plugin for the log framework which vgtk uses to provide debug logging. You can use any logging frontend you like, but this one is a good default as it's uncomplicated and, as its name suggests, pretty.

Next, we call the vgtk::run function with our component (Model) as a type argument, which spins up the GTK main loop and constructs our application. Note that you don't provide an instance of your component, just the type of it. vgtk will instantiate it when the time comes using its Default implementation.

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

GTK's Event Model #

Wait, "signals?"

Signals #

Signals are how GTK widgets communicate. Each widget has a number of named signals (which are conceptually identical to events on DOM elements) that are emitted when certain things happen, to which you can attach callback functions. You've already seen destroy, which is a signal common to all GTK widgets, and is emitted when the widget is—as you might have guessed already, if you can translate from Dalek—being removed. We use it above to find out when the user has closed the window, which means it's time to exit the application's main loop.

Generally, when a widget provides a signal, it will have a connect_signal_name method letting you attach callbacks to it. When you use the on signal_name=callback syntax in the gtk! macro, what it really does, in effect, is generate a call to widget.connect_signal_name(callback) for you. If you're wondering what signals a widget provides, your best bet is usually to look for the connect_signal_name methods. The Gtk-rs documentation is generally pretty bad at describing them other than through their connect methods. The original GTK docs are more comprehensive, and slightly easier to navigate, but beware that, while the signals are the same, they describe the C API, not Rust.

vgtk modifies the signal handler callbacks a little, though: instead of having no return value, you're supposed to return a value of your component's Component::Message type instead. This value will be sent directly to the component's update function, allowing you to respond to the signal.

Moreover, you can declare callbacks async, allowing you to do all sorts of things before eventually returning a Component::Message value for your update function. Beware of putting too much complexity into your signal callbacks, though, it's probably better to organise complex operations elsewhere.

Properties #

In addition to named signals, there are also named properties. These map directly to the attributes you've seen on elements in the gtk! macro, and are used to configure your widgets, or to contain user state. Generally, a property will have a pair of get_property and set_property methods, and this is what the gtk! macro maps them to. For instance, the label attribute above on <Label label="vgtk-tutorial" /> causes the gtk! macro to generate calls to get_label() and set_label() on the Label widget.

In an ideal world, when a GTK widget declares a property, the Gtk-rs bindings always come with get_property and set_property methods to match it. In the real world, however, this isn't always the case, which is why the vgtk::ext module exists. It tries to work around the inconsistencies and provide properly named getters and setters for everything, but it's far from complete. If you find something obvious missing, bug reports or pull requests are very welcome.1

Adding Component State #

Now that we understand GTK a little better, let's try extending our app to actually manage some state.

What kind of state?

Obviously, we're going to make a todo list app, as the article's title promised, and as is the Law of UI toolkit tutorials.

So let's add a list of strings to our Model:

#[derive(Clone, Debug, Default)]
struct Model {
    tasks: Vec<String>,
}

We're going to have to update our view function to render that list. A good GTK widget for holding a list of things is ListBox, so we're going to replace our label with that.

But first, as we have no way of adding to that list yet, let's make a custom Default implementation that starts us off with a couple of strings to render. Take the Default out of the list of derives for Model and add an implementation for it like this:

#[derive(Clone, Debug)]
struct Model {
    tasks: Vec<String>,
}

impl Default for Model {
    fn default() -> Self {
        Self {
            tasks: vec![
                "Call Joe".to_string(),
                "Call Mike".to_string(),
                "Call Robert".to_string(),
                "Get Robert to fix the bug".to_string(),
            ],
        }
    }
}

Now, to render that, we're going to construct the aforementioned ListBox. Inside it, we make a ListBoxRow for each item in our list. A ListBoxRow takes one child widget, which can be whatever we like. We're rendering strings, so we're going to use a Label, like the one we had before.

fn view(&self) -> VNode<Model> {
    gtk! {
        <Application::new_unwrap(Some("com.example.vgtk-tutorial"), ApplicationFlags::empty())>
            <Window border_width=20 on destroy=|_| Message::Exit>
                <ListBox>
                    {
                        self.tasks.iter().map(|task| gtk! {
                            <ListBoxRow>
                                <Label label=task.clone() />
                            </ListBoxRow>
                        })
                    }
                </ListBox>
            </Window>
        </Application>
    }
}

This uses the gtk! macro's interpolation syntax, much like how you'd insert child elements programmatically in JSX. Instead of a child element tag, we use a {} code block. This code block should return an iterator of VNode<Model>—the same type your gtk! macro produces. So we get an iterator for our list, and map it to render a ListBoxRow using another gtk! macro. Note that we need to clone() that string for the label property, because task is a reference that won't outlive the iterator.

Let's cargo run that and see what happens.

That's our list in that list box! Try it out, you can highlight each item with the mouse or the keyboard.

Adding A Checkbox #

It's supposed to be a todo list, though, so it needs a checkbox to indicate whether each item is done, and we need the model to know whether the checkbox should be checked or not.

That means a list of strings is no longer adequate. Let's make a new type which contains all the state we need for a single task.

#[derive(Clone, Debug)]
struct Task {
    text: String,
    done: bool,
}

impl Task {
    fn new<S: ToString>(text: S, done: bool) -> Self {
        Self {
            text: text.to_string(),
            done,
        }
    }
}

We'll need to update our model accordingly:

#[derive(Clone, Debug)]
struct Model {
    tasks: Vec<Task>,
}

impl Default for Model {
    fn default() -> Self {
        Self {
            tasks: vec![
                Task::new("Call Joe", true),
                Task::new("Call Mike", true),
                Task::new("Call Robert", false),
                Task::new("Get Robert to fix the bug", false),
            ],
        }
    }
}

Given that we now have a Task type, we can teach it how to render itself, which also makes the main view function a little tidier. Add a render() method to the Task implementation like this:

fn render(&self) -> VNode<Model> {
    gtk! {
        <ListBoxRow>
            <Label label=self.text.clone() />
        </ListBoxRow>
    }
}

If we update the Model's view function accordingly, the code block now becomes a lot cleaner:

fn view(&self) -> VNode<Model> {
    gtk! {
        <Application::new_unwrap(Some("com.example.vgtk-tutorial"), ApplicationFlags::empty())>
            <Window border_width=20 on destroy=|_| Message::Exit>
                <ListBox>
                    {
                        self.tasks.iter().map(Task::render)
                    }
                </ListBox>
            </Window>
        </Application>
    }
}

But what about the checkbox? Let's update the Task's render function to include it. GTK provides a CheckButton widget for this purpose, but a ListBoxRow only accepts one widget, so it's time to introduce the Box. A Box is a layout widget which takes any number of children, and lays them out next to each other. It's the <div> of the GTK world. It can be configured to lay out its children either horizontally or vertically, but horizontal is the default, so we won't bother specifying it here.

fn render(&self) -> VNode<Model> {
    gtk! {
        <ListBoxRow>
            <Box>
                <CheckButton active=self.done />
                <Label label=self.text.clone() />
            </Box>
        </ListBoxRow>
    }
}

Let's run it and see how it looks now:

That's our list, with checkboxes, and they reflect the state we created: Joe and Mike are checked, while the remaining tasks are not. You can even go and check and uncheck the items as you like, because those checkboxes are real buttons. They won't update the state when you do, though, which is going to be the focus of our next problem.

Reacting To User Input #

According to the TodoMVC spec, which is the law in these parts, a task item that has been checked should be rendered with a strikeout effect. Let's see if we can add that to our render method.

Label supports a subset of HTML markup, if we set the use_markup=true property, and we can use this to render our strikeout effect. Let's extend our render method to do this, and adjust the alpha value of the text down a bit so it looks greyed out as well. Let's also add a label() method to Task to render that string for us, keeping our render method clean.

fn label(&self) -> String {
    if self.done {
        format!(
            "<span strikethrough=\"true\" alpha=\"50%\">{}</span>",
            self.text
        )
    } else {
        self.text.clone()
    }
}

fn render(&self) -> VNode<Model> {
    gtk! {
        <ListBoxRow>
            <Box>
                <CheckButton active=self.done />
                <Label label=self.label() use_markup=true />
            </Box>
        </ListBoxRow>
    }
}

And now it looks like this:

However, if you try and check or uncheck the items, you'll notice that the strikeouts don't change along with the checkbox state. We'll need to actually update our model in response to the checkboxes to achieve this. The first thing we need to do is add a message to describe an item changing state:

#[derive(Clone, Debug)]
enum Message {
    Exit,
    Toggle { index: usize },
}

This tells the model that the user wants to toggle the task at index index in our list. We'll have to change our update function accordingly to handle it:

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
    match msg {
        Message::Exit => {
            vgtk::quit();
            UpdateAction::None
        }
        Message::Toggle { index } => {
            self.tasks[index].done = !self.tasks[index].done;
            UpdateAction::Render
        }
    }
}

Note that when we handle the Toggle message, we return UpdateAction::Render, to let the system know that the model has changed and it should therefore re-render the component to reflect it. When vgtk gets an UpdateAction::Render response from an update function, what happens next is that it calls the component's view function, and updates the widget tree to match it.

We also have to get the system to actually send that Message::Toggle message, but in order to be able to send the message, the Task::render() function will need to know what index it's at. We'll have to update the component's view function to give it that information.

    fn view(&self) -> VNode<Model> {
        gtk! {
            <Application::new_unwrap(Some("com.example.vgtk-tutorial"), ApplicationFlags::empty())>
                <Window border_width=20 on destroy=|_| Message::Exit>
                    <ListBox>
                        {
                            self.tasks.iter().enumerate().map(|(index, task)| task.render(index))
                        }
                    </ListBox>
                </Window>
            </Application>
        }
    }

And, finally, we update the Task's render function to take the index as an argument, and—this is the crucial bit—to add a signal handler to send the message. The signal you want is toggled.

fn render(&self, index: usize) -> VNode<Model> {
    gtk! {
        <ListBoxRow>
            <Box>
                <CheckButton active=self.done on toggled=|_| Message::Toggle { index } />
                <Label label=self.label() use_markup=true />
            </Box>
        </ListBoxRow>
    }
}

Try cargo run again, and you'll notice the strikeouts update in sync with the checkboxes!

Even more excitingly, we can turn on console logging (remember that pretty_env_logger::init() line?) and watch the messages being sent. You can enable logging by setting the environment variable RUST_LOG to debug (or trace if you really like massive amounts of debug output).

If you're using bash or zsh, you can set this for your cargo run invocation by prepending the variable assignment, like this:

$ RUST_LOG=debug cargo run

If you're a PowerShell user or a discerning Fish shell user, use the env command like this:

$ env RUST_LOG=debug cargo run

And the result, in its full glory, comes out something like this, when you play around with checking and unchecking a few boxes before closing the app:

$ env RUST_LOG=debug cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/vgtk-tutorial`
DEBUG vgtk > Application has activated.
DEBUG vgtk::component > Component mounted: vgtk_tutorial::Model
DEBUG vgtk::scope     > Scope::send_message vgtk_tutorial::Model: Toggle { index: 1 }
DEBUG vgtk::scope     > Scope::send_message vgtk_tutorial::Model: Toggle { index: 3 }
DEBUG vgtk::scope     > Scope::send_message vgtk_tutorial::Model: Toggle { index: 2 }
DEBUG vgtk::scope     > Scope::send_message vgtk_tutorial::Model: Toggle { index: 2 }
DEBUG vgtk::scope     > Scope::send_message vgtk_tutorial::Model: Exit
DEBUG vgtk::component > Component unmounted: vgtk_tutorial::Model

An Input Box #

This clearly has the makings of a wonderful app, but there's a chance not everyone is going to need to deal with Joe, Mike and Robert all the time, so it might be best if we add the ability for users to add and remove tasks. The first part of that will involve adding an input box into which the user can type their new tasks.

The GTK input box widget is called Entry. To fit one into our widget tree, we're going to have to insert a vertical Box to contain the current ListBox as well as our input box.

fn view(&self) -> VNode<Model> {
    gtk! {
        <Application::new_unwrap(Some("com.example.vgtk-tutorial"), ApplicationFlags::empty())>
            <Window border_width=20 on destroy=|_| Message::Exit>
                <Box orientation=Orientation::Vertical spacing=10>
                    <Entry placeholder_text="What needs to be done?" />
                    <ListBox Box::fill=true Box::expand=true>
                        {
                            self.tasks.iter().enumerate().map(|(index, task)| task.render(index))
                        }
                    </ListBox>
                </Box>
            </Window>
        </Application>
    }
}

There are a few new things to note here. First, this time we want a vertical box, so we have to specify the orientation property. This takes a value of type gtk::Orientation. We also specify the spacing property to add a little bit of space between the input box and the list box.

We've also added some unusual looking properties to the list box: Box::fill and Box::expand. Properties using this syntax are called child properties, which, in GTK, aren't actually set on the widget in question, but on its parent. The gtk! macro needs to know what type the parent is in order to generate the right code, and the Box:: prefix serves to specify the parent type as well as to distinguish this as a child property.

The fill and expand properties let the parent know that the child wants all the space it can get, while their absence implies the child is content with minimal space in the layout direction. We want the task list to expand to fit the window as we resize it, while we want the input box to stay the same height whatever the size of the window. Try resizing the window to see how the task list expands. Then try temporarily removing the fill and expand properties from the task list and seeing how it behaves differently.

Now, to make the input box actually respond to input. We can safely let the input box handle its own internal state as the user is typing, we just want to know when the new task is ready to be added. For this, we'll listen to the activate signal. This is triggered when the user hits the Enter key. The signal doesn't emit what the user entered, just the fact that it was activated, but every signal handler receives the widget that emitted it as its first argument, so we can use this to inspect the contents of the widget.

First, though, we'll need another Message to let the model know we want to add a task. We'll call it Add.

#[derive(Clone, Debug)]
enum Message {
    Exit,
    Toggle { index: usize },
    Add { task: String },
}

We need to extend the model's update function to respond to this message:

match msg {
    ...
    Message::Add { task } => {
        self.tasks.push(Task::new(task, false));
        UpdateAction::Render
    }
}

Finally, we add the activate listener to the Entry widget:

<Entry placeholder_text="What needs to be done?"
        on activate=|entry| {
            entry.select_region(0, -1);
            Message::Add {
                task: entry.get_text().unwrap().to_string()
            }
        } />

This callback has a side effect in addition to the message being sent to the model: it selects the entered text, so the user can more easily start writing a new task. The select_region method takes two arguments: the start of the selection and the length of it. The special value of -1 for the length means to select to the end of the entered string.

We can now enter new tasks:

A Scrollable List #

However, as you might have noticed if you tried it, adding to the list causes the window to grow, because we're increasing the minimum layout size of the list when we add to it. It scale a little better if the list were scrollable instead. To accomplish this, we're going to add a ScrolledWindow around the ListBox. Despite its name, ScrolledWindow isn't actually a window, it's a container widget that wraps a single child and adds scrollbars to it when neccesary to let it fit on screen instead of endlessly growing the window.

While we're at it, let's also turn off the selection indicator on the ListBox, because we're not actually going to need it for anything, and it's cramping our aesthetic a little bit.

So let's replace the <ListBox> in our view function with this:

<ScrolledWindow Box::fill=true Box::expand=true>
    <ListBox selection_mode=SelectionMode::None>
        {
            self.tasks.iter().enumerate().map(|(index, task)| task.render(index))
        }
    </ListBox>
</ScrolledWindow>

Note that we moved the Box::fill and Box::expand attributes up from the list box to the ScrolledWindow, because the list box is no longer the Box's child, the ScrolledWindow is. We don't need any layout properties on the list box as a child of ScrolledWindow, it knows how to take care of its children on its own.

The property we use to turn off the selection indicator is selection_mode.

But now it ends up looking really cramped!

The ScrolledWindow only requests the minimal amount of space it needs to render itself, rather than the size of the list box, so the minimum width of the window has now decreased significantly. Unless we tell it otherwise, our window is going to open as small as it possibly can. Therefore, we're going to have to tell it to start out a little bigger.

Let's update the <Window> tag in the view function like this:

<Window default_width=800 default_height=600
        border_width=20 on destroy=|_| Message::Exit>
    ...
</Window>

This will set the starting size of your window to the standard SVGA display size of 800x600. This ought to be enough for anyone.

Adventures With Icon Themes #

We also want to be able to delete tasks. One way to do this would be to re-enable that list box selection indicator we just turned off, and add a "delete" button or menu item somewhere that works on the current selection, but the TodoMVC spec has different ideas: we're going to add an individual delete button to each task item in the list. This has the advantage of being quite a bit easier to do, so let's roll with it.

As you've come to expect by now, that means, first of all, a new message in our message type.

#[derive(Clone, Debug)]
enum Message {
    Exit,
    Toggle { index: usize },
    Add { task: String },
    Delete { index: usize },

Along with the corresponding addition to our update function:

match msg {
    ...
    Message::Delete { index } => {
        self.tasks.remove(index);
        UpdateAction::Render
    }
}

Next, we add a button to the Task type's render function:

fn render(&self, index: usize) -> VNode<Model> {
    gtk! {
        <ListBoxRow>
            <Box>
                <CheckButton active=self.done on toggled=|_| Message::Toggle { index } />
                <Label label=self.label() use_markup=true />
                <Button label="Delete" on clicked=|_| Message::Delete { index } />
            </Box>
        </ListBoxRow>
    }
}

And that's it! There's now a button next to each item that you can click to remove it, and it's as easy as just three quick additions to our code.

Except it looks really bad. Really, really bad.

That button is way too big, and it needs to be aligned along the right edge of the list box, not just plonked down in the middle of the row wherever the label ends. It would also be nice if, instead of a big glaring boldface "delete," we could have a discrete little icon indicating a deletion.

So, let's re-style the input box row a little bit. Your first thought might be that we could just use Box::fill and Box::expand to make the label grow to fill all the space that isn't strictly needed by the buttons on either side, which should right align the button neatly. That was my thought too, but even when adding a justify property to the label, there seems to be no way to keep the text left aligned instead of centered, so a different plan is required.

Fortunately, it's very straightforward: Box has another child property called pack_type which takes a value of type PackType to indicate which end of the Box to add the child widget. We can set pack_type=PackType::End on the delete button, and it'll be added right at the end of the box, just like we wanted.

Now, let's see if we can style the button a little less obtrusively. First of all, we can remove the button border by setting the relief property to ReliefStyle::None.

We can also replace the label text with an icon, at least in theory—GTK can pull icons out of system icon themes by name, if your system has the idea of GTK icon themes. If you're on a system with a GNOME desktop, or something vaguely similar to one, you're good. If you're on Windows, I really hope you followed those GTK installation instructions to the letter, especially the bit where it told you to grab a GNOME icon set and extract it in the right place, or you're probably not going to see any icons at all.

Let's update the <Button> tag in the Task render function:

<Button Box::pack_type=PackType::End
        relief=ReliefStyle::None image="edit-delete"
        on clicked=|_| Message::Delete { index } />

That looks much better.

Making A Subcomponent #

The TodoMVC spec calls for a row of filter buttons below the list box, allowing you to toggle between displaying all tasks, just the active ones, or just the completed ones. We could implement this by adding three toggle buttons directly, but this seems like a great opportunity to show off some abstraction.

Thinking ahead, don't you think we're going to need that pattern a lot in our app? A series of mutually exclusive buttons to choose between filters? Well, no, this list is all there is to this app and we're not going to need to re-use this at all, but what if we did?

It's time for a subcomponent.

You already know how to build components—this application we've built is a component. A subcomponent differs from a top level component in two notable ways: one, it doesn't have to return an Application, it can return any widget or GLib object, and two, where a top level component is always constructed using its Default implementation, a subcomponent can be configured using properties, just like GTK widgets.

Do you remember the Properties type on the Component trait? For a top level component, as we already noted, this is unused and you should just set it to (). For a subcomponent, though, you can make a struct type which contains named properties that the gtk! macro will map your subcomponent tag's attributes onto. We'll see how that works in a moment.

A common pattern when making subcomponents that don't need their own internal state other than their properties is to make the Component::Properties type Self. That way, every bit of state maps directly and automatically to a property, and there's no need to make two different types, one for the component and one for its properties. We're going to use that pattern here for our button bar subcomponent.

Let's add the struct for it:

#[derive(Clone, Debug, Default)]
pub struct Radio {
    pub labels: &'static [&'static str],
    pub active: usize,
}

It has two bits of state: a list of button labels, and a number indicating which button is active.

We also need a message type for it. We'll leave it empty for now.

#[derive(Clone, Debug)]
enum RadioMessage {}

Now, let's implement Component on it. It should be fairly straightforward, but there's something new: because it's a subcomponent, we're going to need to implement create and change in addition to the view and update methods we've already seen. create is called when the subcomponent is first constructed, and change when there are new properties coming down from the parent. Luckily, because the properties type is the same as the component type itself, these are very concise to implement.

impl Component for Radio {
    type Message = RadioMessage;
    type Properties = Self;

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

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

    fn view(&self) -> VNode<Self> {
        gtk! {
            <Box spacing=10>
                {
                    self.labels.iter().enumerate().map(|(index, label)| gtk! {
                        <ToggleButton label={ *label }
                                      active={ index == self.active } />
                    })
                }
            </Box>
        }
    }
}

We could have been smarter about the change method, which could return UpdateAction::None if the new properties are identical to the current set, but let's leave that as an exercise.

  • Extend the change() method to check if the new properties are different from the current state, and only return UpdateAction::Render if they are.

Looking at the view function, there's a new widget, ToggleButton, which works just like CheckButton except it's styled to look like a regular button, not a checkbox. We iterate over the list of labels in our properties and make one ToggleButton for each one, making sure the one with the same index as self.active is selected. We stuff all of these in a horizontal Box with a little bit of padding between them. Nothing very new or surprising here so far.

Now let's insert our new subcomponent into the top level component. In the view function for the top level component (your Model), add a Box containing our subcomponent just after your ScrolledWindow in your container box:

    ...
    </ScrolledWindow>
    <Box>
        <@Radio Box::center_widget=true active=0
                labels=["All","Active","Completed"].as_ref() />
    </Box>
</Box>

Note that, instead of just putting a & in front of that array to get us a reference to it, we use .as_ref() instead, which gets around an unfortunate side effect of vgtk's smart casting mechanisms where Rust's type checker will complain at you that a reference to a sized array is different from a reference to an array. It's worth remembering this trick for whenever you have a component or widget which takes a &[A] for some A and you want to feed it an array literal.

Other than that, we note a peculiar difference in the tag syntax: There's a @ in front of the subcomponent name, to distinguish it from a GTK widget. Another thing to note about subcomponents is that they cannot have children, so they're always given as single tags. They can also not have on signal handlers attached, because signals are unique to GTK widgets. This means we're going to have to find a different way to communicate with our subcomponents, but let's get back to that in a bit.

The center_widget=true property is also worth noting. If you've guessed this centres the subcomponent inside the box, you're right, but we could have achieved the same effect with a Box::fill=true. We're going to add a few widgets on either side of the subcomponent, though, and if we relied on fill its position would be affected by the widths of those widgets. center_widget is a special property on Box which takes the widget out of the normal layout flow and always positions it in the exact centre of the box.

OK, let's try running that and see how it turns out.

There's no logic in the buttons, though, you can just toggle them all individually with no effect on the list, and we're going to have to fix that.

Communicating With A Subcomponent #

We already noted that subcomponents don't have signal handlers, because they're not GTK widgets and hence they don't actually have signals. vgtk provides a different mechanism to communicate between components: Callbacks.

Let's add one to our subcomponent's model. First, we need to add a use declaration for it, because the app template didn't already do it for us. Add it to the one that pulls in the other vgtk types like this:

use vgtk::{gtk, run, Callback, Component, UpdateAction, VNode};

Then add a property to our subcomponent's model:

#[derive(Clone, Debug, Default)]
struct Radio {
    labels: &'static [&'static str],
    active: usize,
    on_changed: Callback<usize>,
}

The idea here is that whenever the user changes the selection on the subcomponent's radio buttons, we trigger the on_changed callback with the index of the newly selected button (that's the usize). In order to respond to the user changing the selection, though, we need the usual: a signal handler and a message.

First, the message:

#[derive(Clone, Debug)]
enum RadioMessage {
    Changed(usize),
}

Then, the signal handler, in the subcomponent's view function:

fn view(&self) -> VNode<Self> {
    gtk! {
        <Box spacing=10>
            {
                self.labels.iter().enumerate().map(|(index, label)| gtk! {
                    <ToggleButton label={ *label }
                                    active={ index == self.active }
                                    on toggled=|_| RadioMessage::Changed(index) />
                })
            }
        </Box>
    }
}

And, finally, we need to add an update function to the subcomponent, responding to the message by invoking the callback.

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
    match msg {
        RadioMessage::Changed(index) => {
            self.on_changed.send(index);
            UpdateAction::Render
        }
    }
}

The UpdateAction::Render is especially important in this case, because the current state of the GTK widget tree will be that both the button you just toggled and the previously active button will be selected. Triggering a re-render will make sure only the button you tell the subcomponent is the active one is selected.

Speaking of which, the last thing we have to do to get this all working is to have our top level component actually respond to that callback by updating its idea of which button is the active one. Right now, we don't even track that, we just told the subcomponent that active=0, so we're going to have to update our model a bit. We add the filter property to the Model struct, and to the Default implementation, like this:

#[derive(Clone, Debug)]
struct Model {
    tasks: Vec<Task>,
    filter: usize,
}

impl Default for Model {
    fn default() -> Self {
        Self {
            tasks: vec![
                ...
            ],
            filter: 0,
        }
    }
}

Next, as is customary, we need a message to let us know the user has changed the filter:

#[derive(Clone, Debug)]
enum Message {
    ...
    Filter { filter: usize },
}

The update function needs a match clause to respond to our new message by updating the model:

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
    match msg {
        ...
        Message::Filter{filter} =>{
            self.filter = filter;
            UpdateAction::Render
        }
    }
}

And, finally, we need to attach a callback to our subcomponent to find out when the filter has been changed. This is where the callback finally comes into play. Don't forget to also tell the subcomponent about the new filter field in the model, via the active property!

<@Radio Box::center_widget=true active=self.filter
        labels=["All", "Active", "Completed"].as_ref()
        on changed=|filter| Message::Filter { filter } />

Now cargo run that and see how it turned out. Try clicking some of the filter buttons—you should see that this time, only one of them will select at any one time, and it's the one tracked by our model.

On the other hand, while the subcomponent now works the way it's supposed to, it doesn't actually filter anything yet. We don't have to add anything more to our model to get there, though, we just have to update the view function to take the filter into account when we build the list of tasks.

Let's first make a method on Model to filter the list for us:

impl Model {
    fn filter_task(&self, task: &Task) -> bool {
        match self.filter {
            // "All"
            0 => true,
            // "Active"
            1 => !task.done,
            // "Completed"
            2 => task.done,
            // index out of bounds
            _ => unreachable!(),
        }
    }
}

We'll use that method in our view function to filter the task list, by updating the iterator inside our ListBox with a filter() call:

<ListBox selection_mode=SelectionMode::None>
    {
        self.tasks.iter().filter(|task| self.filter_task(task))
            .enumerate().map(|(index, task)| task.render(index))
    }
</ListBox>

Now try cargo run again—the list should now update to reflect the filters you choose.

  • The filter_task() method we just wrote isn't very nice. It would be much better if we could make the filter type an enum, rather than a usize, and better still if we could generalise the Radio subcomponent over that enum type, so that there's never a usize in sight. See how far you can get towards that goal. (The TodoMVC example in the vgtk git repo offers one solution, using the strum crate for maximum ease of use via some fancy derives.)

Nearly There #

To be fully TodoMVC compliant, we need three more items: one, a label telling us how many tasks remain to be done; two, a button which deletes all completed tasks; three, a button which toggles all tasks.

First, the label. It's the easiest to add, because the user doesn't interact with it so we don't need any messages. We can just add it to our view function directly. Let's render the label in another helper method on our model, though:

impl Model {
    fn items_left(&self) -> String {
        let left = self.tasks.iter().filter(|task| !task.done).count();
        let plural = if left == 1 { "item" } else { "items" };
        format!("{} {} left", left, plural)
    }

    ...
}

Try running that, and verify that the number updates as you select and deselect tasks.

Notice also how the center_widget property from earlier is paying off: the label changes width a lot as you select and deselect tasks, but the filter widget stays in the exact same position throughout.

Next, we want that button we can click to immediately clear out all completed tasks from our list. This button should not be present if there are no completed tasks to clear out. We can do this using the same kind of code block as when we created the task list itself by using an iterator, but in this case it's either one item or zero, not a whole list of things.

Let's first make a helper method in order to keep the view function tidy:

impl Model {
    fn count_completed(&self) -> usize {
        self.tasks.iter().filter(|task| task.done).count()
    }

    ...
}

Next, we need, as always, a new message:

#[derive(Clone, Debug)]
enum Message {
    ...
    Cleanup,
}

A corresponding update function update is, as always, also needed:

    fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
        match msg {
            ...
            Message::Cleanup => {
                self.tasks.retain(|task| !task.done);
                UpdateAction::Render
            }
        }
    }

And now we can add the button to our view function—or not add it, as the case may be. We need the gtk_if! macro for this, which we don't have in scope yet, so you'll need to update your use statement again:

use vgtk::{gtk, gtk_if, run, Callback, Component, UpdateAction, VNode};

In the view function, we add this block to the bottom Box just after the radio subcomponent.

<Box>
    <Label label=self.items_left() />
    <@Radio Box::center_widget=true active=self.filter
            labels=["All", "Active", "Completed"].as_ref()
            on changed=|filter| Message::Filter { filter } />
    {
        gtk_if!(self.count_completed() > 0 => {
            <Button label="Clear completed" Box::pack_type=PackType::End
                    on clicked=|_| Message::Cleanup />
        })
    }
</Box>

The gtk_if! macro takes a conditional expression and a widget tag, and only inserts the widget if the expression is true.

Note also the Box::pack_type=PackType::End property. This causes the widget to be inserted on the right side of the box, instead of on the left just after the label. Remember the radio button bar has the center_widget=true property, so it always appears in the middle, and anything without an explicit Box::pack_type=PackType::End goes to the left of it.

Behold! the button:

The best part? When you click it, it will disappear. Tick off another task, and it's back. This right here is what programming is all about.

We're just missing one thing now to be fully TodoMVC compliant, but it's not terribly exciting so we'll leave it as an exercise.

  • There's one item left to get to a complete standards compliant TodoMVC app: a button to the left of the input box which lets you toggle all your tasks in one go. It should work like this: if all your tasks are currently done, it should set them all to not done. If any task is set to not done, it should set them all to done. See if you can implement this button.

Making A Menu #

One thing TodoMVC traditionally doesn't have, but that every self respecting desktop application should, is a menu bar. It's not that this app would particularly benefit from one, it's just that making a menu bar with vgtk isn't very straightforward and it'll be good to see how it's supposed to be done. Making a menu bar with GTK isn't really that straightforward in the first place, so the more examples, the merrier.

We're not going to go overboard with this, though. Let's just add a simple hamburger menu to the window with two menu items on it: "About" and "Quit." As an added bonus, this means we're also going to get to see how to display a modal dialog.

Oh Why Is GTK So Weird #

But first! Let me tell you about GTK widget trees. If you've done web development, you might think the DOM is unnecessarily complicated. It's not, really, at least not in its structure: it's a regular tree structure, where each element has a list of children and that's the only way DOM elements nest.

GTK, on the other hand... you'd think, well, every GTK element which can have children implements Container, and that provides a clear API for how widgets nest. This is partially true, in that most widgets that can have children implement Container and its API works for that, even if it's not entirely clear. But some widgets can have children added in other ways, quite outside of the Container API—you've already seen one of them, in the Box::center_widget property. What goes on there, under the hood, is that Box has a special method, set_center_widget(), which adds a widget into a special position, and it's outside of the Container API. This is just one of many exceptions you'll encounter when using GTK widgets. Its API is definitely showing signs of the age of the toolkit—it's a good quarter century old by now, and it's designed for a language, C, that's almost another quarter century older than that again. (That said, it's still one of only a very small handful of cross platform GUI toolkits that are actually mature enough for general use, and whose aesthetic has evolved with the times instead of still looking like it did in the 90s.)

The reason I mention this (other than to generally vent about how hard it was to write a virtual widget tree differ because of this) is that we're about to see another special case: Window::set_titlebar. GTK windows allow you to provide a custom title bar instead of the system default, and it comes ready made with one type of custom title bar (in heavy use in GNOME apps) that lets us add that hamburger menu in a convenient spot right next to the window controls.

The vgtk widget tree model tries its best to incorporate all those unfortunate special cases of adding child widgets found in GTK. There's a special hack for center_widget in vgtk::ext, but generally, when a container widget has weird APIs, vgtk just adds rules about what kind of children, and how many, it can take, and it figures out how to add them based on these factors. Window takes either one or two children. If one, it's the window's contents, but you can also give it two children. If you do, vgtk will take the second one as the window's contents, and add the first one with set_titlebar.

Title Bar Time #

So let's try adding a custom title bar. There's one in GTK already that's exactly what we want: HeaderBar. We'll just go ahead and add it to our window:

<Window default_width=800 default_height=600
        border_width=20 on destroy=|_| Message::Exit>
    <HeaderBar title="Erlang: The Todo List" show_close_button=true />
    <Box orientation=Orientation::Vertical spacing=10>
        ...
    </Box>
</Window>

Let's run it and see what it looks like.

See the difference? It's a little larger now, and we have a custom title, but it's designed to blend in.

The big difference, though, is that we can add child widgets to it. But before we get as far as adding the menu button, we'll need to build the menu bar to attach to it. And to do that, first we need some Actions to put in it.

Actions #

Actions are predefined actions the user can trigger, at the application or window level. They're meant as an abstraction that can be represented in many different ways, including in the application's D-Bus interface if you're on a system that supports it, but most obviously as menu items and keyboard shortcuts.

We can declare Actions with the gtk! macro, either as children of the top level application or as children of a window, but we'd have to swap our Window for an ApplicationWindow in order to be able to attach actions to it. We're not going to bother, though, as the only two actions we want work fine at the application level.

The difference between application level actions and window level actions is that the latter affect only the active window, assuming your app can have more than one window open, while application level actions would affect every window in the app. In our case, the "about" action opens an about dialog independent of any other windows, and the "quit" action quits the entire app, windows along with it. There's also the thing where we only have a single window anyway, but it's good to understand the difference.

Let's add these two actions to our application. Where do we start? Messages, of course. We already have a suitable message for the "quit" action, Message::Exit, but we'll need a new one for the "about" action.

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

We'll need to handle it in the update function, or the app won't even compile, but let's just have it do nothing for now. We're going to circle back later and add a proper dialog once we've got the menu up and running.

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
    match msg {
        ...
        Message::About => UpdateAction::None,
    }
}

Now we can add our two actions to the view function:

fn view(&self) -> VNode<Model> {
    gtk! {
        <Application::new_unwrap(Some("com.example.vgtk-tutorial"), ApplicationFlags::empty())>
            <SimpleAction::new("quit", None) Application::accels=["<Ctrl>q"].as_ref() enabled=true
                    on activate=|a, _| Message::Exit/>
            <SimpleAction::new("about", None) enabled=true on activate=|_, _| Message::About />

            <Window default_width=800 default_height=600
                    border_width=20 on destroy=|_| Message::Exit>
                ...
            </Window>
        </Application>
    }
}

Notice that we add them as direct children of the application, alongside the Window. Notice, also, that we need to use the SimpleAction::new constructor to make a SimpleAction. Actions aren't GTK widgets, they live in GIO, which is a lower level component of the GTK stack than GTK itself. As a consequence of this, they can't be built using GTK's Buildable API like the rest of our widgets, hence the constructor. They are GLib objects, though, so they still have signals and properties. We're using one of each: an action must have enabled=true for the user to be able to trigger it, and we listen to the activate signal to find out when it's been triggered.

There's also a child property worth noting on the first action: Application::accels. It takes a list of keyboard shortcut descriptions, and lets you trigger the action using them. In our case, <Ctrl>q should be a way to trigger the "quit" action and exit the application.

Let's try that straight away. We have a slight problem first, though, because SimpleAction isn't in scope. We're going to have to extend our use statement for vgtk::lib::gio like this:

use vgtk::lib::gio::{ActionExt, ApplicationFlags, SimpleAction};

It should compile now, so go ahead and cargo run it and see if you can use <Ctrl>q as a keyboard command to exit the application.

Did it work? It did, didn't it? This is so exciting.

Building The Menu #

Now let's take these actions and make a menu out of them. There's no macro for building a menu (at least not yet), but there's a pretty straightforward builder for them. Let's try it out by building a menu inside our view function, just before the gtk! macro.

A menu can contain any number of sections and items. An item is the thing you pick from the menu, and it's made from a label and the name of the action it triggers. A section is a way to group items inside a menu: GTK will render visual separators between sections. These separators can be just whitespace, depending on your widget theme, but they're always shown as visually distinct parts of the menu. In other words, if you want a separator between two menu items, you put them in two different sections.

Let's make ours:

fn view(&self) -> VNode<Model> {
    let main_menu = vgtk::menu()
        .section(vgtk::menu().item("About...", "app.about"))
        .section(vgtk::menu().item("Quit", "app.quit"))
        .build();

    gtk! {
        ...
    }
}

Notice the name of the action: it takes a prefix telling GTK where to find it. Ours are both application level actions, because they're both attached to our Application object, so their prefix is app.. The prefix for window level actions is win., but we're not using that anywhere today.

The builder leaves us with a MenuModel from which we can build a Menu for any widget which will take it. GTK has a MenuButton widget for making menu dropdown buttons, and it can be added directly to our HeaderBar to make it appear alongside the standard buttons on the window frame. So let's turn our HeaderBar into a parent and add that MenuButton:

<HeaderBar title="Erlang: The Todo List" show_close_button=true>
    <MenuButton HeaderBar::pack_type=PackType::End @MenuButtonExt::direction=ArrowType::Down
                relief=ReliefStyle::None image="open-menu-symbolic">
        <Menu::new_from_model(&main_menu)/>
    </MenuButton>
</HeaderBar>

Note a few things here. We use our old friend pack_type=PackType::End again so that the button gets added on the right hand side of the header bar, just next to the window controls. We tell it not to draw a button border with relief=ReliefStyle::None, and we tell it to use the hamburger menu icon with image="open-menu-symbolic".

And there's a bit of new syntax here too. A menu button has a property direction which describes where the menu is going to pop up in relation to the button, which we want to set to ArrowType::Down. But there's a problem, because there's also a direction property on Widget which is about something else entirely (text direction). We therefore have to tell the gtk! macro which direction we need, using the @Type::property syntax. The relevant getter and setter methods come from the MenuButtonExt trait, so that's the type we need to give it.

OK, let's cargo run and try this out.

That's our menu! Look, the "quit" item even has the keyboard shortcut printed on it. You should be able to exit the app using that menu item now.

Let's Sit Down And Have A Dialog #

The "about" menu item doesn't work yet, though, because it's sending the Message::About message and we're just ignoring it. I think it's time we made ourselves an about dialog.

In fact, we've come so far already, I think it's time we treat ourselves. Let's make an about dialog with a dog in it.

Here is a dog.

Save this very good dog as dog.png in your project's src folder, alongside your main.rs file. Rust has an include_bytes! macro which we can use to embed the dog directly into our binary, and GTK has facilities for parsing the PNG file into something it can render into a window.

First, the dog. Put this anywhere at the top level of your code, but I prefer it in pride of place just beneath the use statements.

static DOG: &[u8] = include_bytes!("dog.png");

Now, the dialog. We're going to implement this as a separate component, and it follows the exact same pattern as any of your other components. It needs a struct for state, a Default implementation, and a Component implementation.

The only state we need is the dog, and it's not going to change after we've first created it, so there's no need for a message type.

pub struct AboutDialog {
    dog: Pixbuf,
}

You're going to need to pull that Pixbuf into scope, along with a few more things we're going to need to construct it. Find your use vgtk::lib::gio::... statement and replace it with this:

use vgtk::lib::gdk_pixbuf::Pixbuf;
use vgtk::lib::gio::{ActionExt, ApplicationFlags, Cancellable, MemoryInputStream, SimpleAction};
use vgtk::lib::glib::Bytes;

That pulls in the Pixbuf and everything we need to turn the PNG into one (as well as the things from gio we were already using).

We'll let the Default implementation do the work of parsing the PNG into a Pixbuf:

impl Default for AboutDialog {
    fn default() -> Self {
        let data_stream = MemoryInputStream::new_from_bytes(&Bytes::from_static(DOG));
        let dog = Pixbuf::new_from_stream(&data_stream, None as Option<&Cancellable>).unwrap();
        AboutDialog { dog }
    }
}

And now the Component implementation. It doesn't have any messages, and we're not going to embed it as a subcomponent, so the only method it needs to implement is view(). Its top level widget should be a Dialog or something which implements it. We're fine with regular Dialog here.

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

    fn view(&self) -> VNode<Self> {
        gtk! {
            <Dialog::new_with_buttons(
                Some("About The Todo List"),
                None as Option<&Window>,
                DialogFlags::MODAL,
                &[("Ok", ResponseType::Ok)]
            )>
                <Box spacing=10 orientation=Orientation::Vertical>
                    <Image pixbuf=Some(self.dog.clone())/>
                    <Label markup="<big><b>A Very Nice Todo List</b></big>"/>
                    <Label markup="made with <a href=\"http://vgtk.rs/\">vgtk</a> by me"/>
                </Box>
            </Dialog>
        }
    }
}

Let's take a look at this. First, we're using the new_with_buttons constructor to build a dialog with pre-made dialog buttons. We're asking it for just the one: an "Ok" button. We're also telling it this should be a modal dialog, which means it'll block the rest of the application until you either hit that "Ok" button or close its window. It takes a single content widget, just like a Window, and inside that we put our noble beast as an Image widget, along with some text labels. GTK is going to handle the button interaction for us, so that's all we need here.

Let's add a method to run the dialog.

impl AboutDialog {
    #[allow(unused_must_use)]
    fn run() {
        vgtk::run_dialog::<AboutDialog>(vgtk::current_window().as_ref());
    }
}

Wait, #[allow(unused_must_use)]? What's that about?

vgtk::run_dialog returns a Future which you can use if you need to wait for the user to respond to the dialog. We don't really care about this, though, so we just ignore the future. The dialog is still going to run. rustc, however, really doesn't like you ignoring futures, though, so it's going to flag this as a warning. We tell it to #[allow(unused_must_use)] explicitly so it'll stop complaining. Or you could just ignore the warning, but if you're anything like me, ignored compiler warnings will keep you up at night.

Note, also, that vgtk::run_dialog takes a parent window as an argument, to which it attaches the dialog, if that's meaningful on your choice of desktop environment. In order to get this, vgtk provides the current_window() function, which is usually the right choice here.

Finally, let's tell our top level update function to stop quietly ignoring the Message::About message and run the dialog instead.

fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
    match msg {
        ...
        Message::About => {
            AboutDialog::run();
            UpdateAction::None
        }
    }
}

It's still responding with UpdateAction::None, because running the dialog doesn't make the main window have to re-render itself, but now it's launching the dialog first.

So let's cargo run this and try it out!

Look at that amazing dog dialog!

We're never going to top this, so let's stop there, and be content with our labours.

If you're not ready to give up yet, there are some exercises below that you can try. You can also go back through the text and try the exercises scattered throughout, if you skipped them. You'll need to prepare yourself for getting used to reading GTK documentation, though, it's not always a task for the faint of heart.

I hope you've enjoyed this tutorial (and the dog, most of all), and good luck building your next big GTK app.

If you find anything missing (and there's bound to be a lot missing yet), you can file an issue or, even better, submit a pull request.

As a final bonus for getting this far, if you've noticed the other dog lurking behind the application window, and you really want her as your desktop wallpaper too, it is here, and it's an unlimited fount of inspiration.

  • So far, we've used a hardcoded list of tasks, discarding any edits on exit, which is a bit silly for a real app. Design a storage format for todo lists (or see if there's a standard format out there already) and implement load/save functionality so the app can persist its state. You can use standard Rust I/O for this, but it would be even better to use [GIO] async I/O.
  • If you can load and save files, it makes sense to add the standard file menu items to our hamburger menu: "Open..." and "Save as..." and all that. You'll need to figure out GTK file dialogs as well as add some window level actions.
  • Once you have a way to open and save files, make the window title show the name of the current file. For extra credits, make it also show a little star * after the file when it's dirty.

Footnotes #

1:

GTK properties aren't just defined by getters and setters, in reality: GTK objects actually keep a list of which properties exist and what sort of values they should accept, with a full API for manipulating them via string keys, which earlier iterations of vgtk took advantage of. The problem with this approach is that the API is untyped, or, rather, relies on casting the provided value into the expected value at runtime rather than at compile time. The reason for the current design is that we can now check the property values at compile time and throw the appropriate type errors, rather than have the application panic at runtime when you've made a boo-boo.