Getting Started With Rust and GTK

The goal of this article is to help you hit the ground running with Rust and GTK (via gtk-rs) by setting up a project and build configuration to deal with:

  • Compiling bundling resources
  • Adding CSS and images
  • Automating bundling resources at compile time

I will be using VSCode as my development environment on Ubuntu 20.04 in this article. However most of this guide is editor agnostic.

The full source for this guide can be found here: https://github.com/gh123man/Rust-GTK-Resources-Example

Dependencies

If you have not already set up your dev environment, you will need to install the following:

Rust: https://www.rust-lang.org/tools/install

GTK and build essentials

sudo apt install build-essential 
sudo apt install libgtk-3-dev 

Glade is a dedicated graphical editor for GTK, it is optional but recommended

sudo apt install glade 

Structuring your project

First lets generate a new package (see here for more details)

$ cargo new rust_gtk_resources_example --bin

Lets also add a resources folder:

cd rust_gtk_resources_example
mkdir resources

Your folder structure should look like this:

$ tree .
.
├── Cargo.toml
├── resources
└── src
    └── main.rs

Try to run your project

$ cargo run

Adding a Glade resource

Lets open glade and create a simple UI:

This UI contains an image, a label (that we will add a background to via CSS) and a button. The application window ID is main_window - this is important later so we can reference it in rust. Save it to the resources folder we created earlier. I called it main_window.

Now create a new file called resources.xml alongside our main_window.glade file and add the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/example/Example">
    <file>main_window.glade</file>
  </gresource>
</gresources>

This XML file tells GTK where our resources are and how to find them. All of our resources will get compiled into the application binary when we build. But first we need to bundle the resources.

To generate the bundle you can run: glib-compile-resources resources.xmls from the resources folder, but we don't want to do this by hand every time we build, so lets automate it.

Automate compiling the resource bundle

Rust supports a build script that will run before every build. Lets create one:
In the root of the project, create  build.rs file

$ touch build.rs
$ tree .
.
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── resources
│   ├── main_window.glade
│   └── resources.xml
├── src
│   └── main.rs
└── target
    ...

Open your build.rs file and add the following:

use std::process::Command;

fn main() {
    let _ = Command::new("sh")
                        .args(&["-c", "cd resources && glib-compile-resources resources.xml"])
                        .output()
                        .expect("failed to execute process");

}

This will compile your resources and drop a resouces.gresource file in the resources folder before every build. See it in action:

$ cargo build
$ tree .
.
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── resources
│   ├── main_window.glade
│   ├── resources.gresource       <<< our compiled bundle
│   └── resources.xml
...

Loading resources

Now that we have a resource bundle that builds with our app, lets get our UI from the bundle and wire it up! Open cargo.toml and add the GTK Libraries:

[package]
name = "rust_gtk_resources_example"
version = "0.1.0"
authors = ["Your name"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
[dependencies.gtk]
version = "0.8.1"
features = ["v3_16"]

[dependencies.gdk]
version = "0.12.1"

[dependencies.glib]
version = "0.9.3"

[dependencies.gio]
version = ""
features = ["v2_44"]

Then open main.rs and updatet it to the following:

use gtk::*;
use gtk::prelude::*;
use gio::prelude::*;
use std::env::args;

fn main() {
    let application = gtk::Application::new(Some("com.example.example"), Default::default())
        .expect("Initialization failed...");

    application.connect_activate(|app| {
        
        // Load the compiled resource bundle
        let resources_bytes = include_bytes!("../resources/resources.gresource");
        let resource_data = glib::Bytes::from(&resources_bytes[..]);
        let res = gio::Resource::new_from_data(&resource_data).unwrap();
        gio::resources_register(&res);

        // Load the window UI
        let builder = Builder::new_from_resource("/org/example/Example/main_window.glade");

        // Get a reference to the window
        let window: ApplicationWindow = builder.get_object("main_window").expect("Couldn't get window");
        window.set_application(Some(app));

        // Show the UI
        window.show_all();
    });

    application.run(&args().collect::<Vec<_>>());
}

You should now be able to do a cargo run and see your UI!

Adding image resources

Now that we have the UI XML resource compiling and loading, lets add CSS and images.

First lets populate our image view with a picture of our friend tux:

Put tux.png in your resources folder, and add it to your resources.xml

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/example/Example">
    <file>main_window.glade</file>
    <file>tux.png</file>
  </gresource>
</gresources>

Go back to glade and add a reference to your image /org/example/Example/tux.png:

Now you can build and run $ cargo run

Adding CSS resources

You will often want to customize parts of GTK widgets that are not possible in Glade. Theam and styles are handled by CSS. CSS can also be loaded as a resource, so lets make the background green behind our label!

First add a style.css file in the resource folder containing:

.greenBg {
    background: #005826;
}

Add the CSS resource to the resources.xml

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/example/Example">
    <file>main_window.glade</file>
    <file>tux.png</file>
    <file>style.css</file>
  </gresource>
</gresources>

Add the class to the label in Glade

Finally we have to update our rust code to load the styles:

use gtk::*;
use gtk::prelude::*;
use gio::prelude::*;
use std::env::args;

fn main() {
    let application = gtk::Application::new(Some("com.example.example"), Default::default())
        .expect("Initialization failed...");

    application.connect_activate(|app| {

        // Load the compiled resource bundle
        let resources_bytes = include_bytes!("../resources/resources.gresource");
        let resource_data = glib::Bytes::from(&resources_bytes[..]);
        let res = gio::Resource::new_from_data(&resource_data).unwrap();
        gio::resources_register(&res);

        // Load the CSS
        let provider = gtk::CssProvider::new();
        provider.load_from_resource("/org/example/Example/style.css");
        StyleContext::add_provider_for_screen(
            &gdk::Screen::get_default().expect("Error initializing gtk css provider."),
            &provider,
            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
        );

        // Load the window UI
        let builder = Builder::new_from_resource("/org/example/Example/main_window.glade");

        // Get a reference to the window
        let window: ApplicationWindow = builder.get_object("main_window").expect("Couldn't get window");
        window.set_application(Some(app));

        // Show the UI
        window.show_all();
    });

    application.run(&args().collect::<Vec<_>>());
}

Finally you can see the green background!

Wrapping up

I hope you found this helpful - as I could not find a comprehensive guide when I first went though this. The full source for this guide can be found here: https://github.com/gh123man/Rust-GTK-Resources-Example