SamWhited|blog

GTK3 Patterns in Rust: Structure

README.md

Writing GTK applications in Rust requires finding a balance between writing idiomatic Rust, and idiomatic GTK. In many cases, it is possible to do complete tasks “the Rust way” or “the GTK way”, and it can be hard to choose between the two. For example, a callback on a button can be set by registering a glib virtual function using an implementation of gio::Action, or by passing its dependencies to a callback attached to the button’s clicked signal.

In this post, I’ll discuss the structure of a GTK application in Rust including files and modules, registering actions, and bundling widgets together into UI elements that have some defined behavior. The directory structure I use looks something like this, and will also serve as an out-of-order table of contents for this post:

Cargo.toml

Before we dive in, it should be noted that any examples in this post will be using the following constraints on the various gtk-rs dependencies, which may be long out of date by the time you see this (but the general patterns will hopefully still be relevant):

[dependencies]
cairo-rs = "0.5"
chrono = "0.4"
gdk = "0.9"
gdk-pixbuf = "0.5"
gio = "0.5"
glib = "0.6"

[dependencies.gtk]
version = "0.5"
features = ["v3_22_20"]

res/

The res/ tree is one of the things I borrow from GTK. It gets created at the root of the project and contains any non-code resources that we might need including stylesheets (under the style/ tree), images (img/), etc. We then load these resources into our code and use them from the res module. There’s not much more to say other than “put resources here”, so let’s move on to using them.

src/res/

In a traditional GTK application, resource files are installed to $DATADIR/appname by our package manager, and then loaded by the application into a gio::Resource at runtime. This means we only discover that we left one of these files out at runtime, and gives packagers more work to do. GTK sometimes recommends using glib-compile-resources(1) to build your resources into an XML or C source file at compile time, but this still makes using them from Rust awkward (we need to link with C) and requires an extra step in our Makefile. Instead, I prefer to create a module (src/res/mod.rs) that includes each of my resources using Rust’s std::include_bytes macro. This let’s our normal build handle adding each resource as a &'static [u8; N], which the compiler will likely stick in the data segment of the binary. I generally start putting everything directly in the src/res/mod.rs file, but use the directory style module in case the application grows later and I need to split the file out into separate modules for each resource I want to load (eg. src/res/img.rs, src/res/style.rs, etc.).

I also include other common strings such as the application’s DBUS ID, icon names, etc. in the res module. Here’s a small example src/res/mod.rs file:

//! Static resources and constants that are used throughout the project.

use gdk_pixbuf;
use gdk_pixbuf::PixbufLoaderExt;

// Republish a child module containing icon names.
mod icons;
pub use icons::*;

/// The name of the application.
pub const APP_NAME: &str = "My App";

/// The GTK application ID.
pub const APP_ID: &str = "net.example.myapp";

/// The version of the app.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

/// The logo to be shown in the about dialog.
/// See [`load_logo`] for an example of its use.
///
/// [`load_logo`]: ./fn.load_logo.html
const IMG_LOGO: &[u8] = include_bytes!("../res/img/logo.svg");

/// Loads the logo included in the binary into a pixbuf.
pub fn load_logo() -> Result<Option<gdk_pixbuf::Pixbuf>, gdk_pixbuf::Error> {
    Ok({
        let logoloader = gdk_pixbuf::PixbufLoader::new();
        logoloader.write(IMG_LOGO)?;
        logoloader.close()?;

        logoloader.get_pixbuf()
    })
}

macros.rs

In general, I try to avoid writing macros whenever possible. As useful as they are, macros make code more difficult to read, debug, and breaks some tooling like rustfmt. However, the gtk-rs project recommends including a macro to make cloning refcounted pointers into closures easier, and I tend to think the trade off is worth it here. I also personally like to include a macro (feel free to copy/paste or otherwise use it however you want) that acts like std::format but escapes arguments for use in The Pango Markup Language.

/// Makes it easier to clone GTK objects before moving them into closures.
/// Borrowed from the gtk-rs examples.
///
/// ```rust,ignore
/// let about = gtk::AboutDialog::new();
/// if let Some(settings) = about.get_settings() {
///     settings.connect_property_gtk_application_prefer_dark_theme_notify(
///         clone!( about, logo_dark, logo_light => move |settings| {
///             if settings.get_property_gtk_application_prefer_dark_theme() {
///                 about.set_logo(&logo_dark);
///             } else {
///                 about.set_logo(&logo_light);
///             }
///         }),
///     ),
/// }
/// ```
macro_rules! clone {
    (@param _) => ( _ );
    (@param $x:ident) => ( $x );
    ($($n:ident),+ => move || $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move || $body
        }
    );
    ($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move |$(clone!(@param $p),)+| $body
        }
    );
}

/// Like format! but only takes &str arguments which are escaped for use in
/// [The Pango Markup Language].
///
/// [The Pango Markup Language]: https://developer.gnome.org/pygtk/stable/pango-markup-language.html
macro_rules! markup {
    ($fmt:expr, $($arg:expr),*$(,)?) => ({
        use glib;
        std::fmt::format(format_args!($fmt, $(
            glib::markup_escape_text($arg)
        ),*
        ))
    })
}

Having these in a single macros.rs file isn’t actually very nice; it can be too tempted to turn this into a dumping ground for unrelated code, consider putting any macros you define in a more relevant module. For example, I also sometimes use a macro for constructing menus and UIs from Glade XML files at compile time instead of using a gtk::Builder, but that’s large enough that I include it in its own separate crate (and large enough that it will be a whole separate blog post if I ever decide to polish it up and release it).

main.rs

Just like writing a GTK application in C, our entry point should be kept simple. Your main.rs file should import modules, register your application, and handle operating system level features like passing args through to your app for parsing, signal handling, etc. It should also be the final place where errors are handled; if a result when creating your App can’t be handled some other way such as logging or an in-app notification, it can be returned up to main where you can print it and exit the process. This is mostly obvious, so here’s a simple example:

mod yourapp;
// mod …

use std::env::args;
use std::process;

// This is the main entrypoint for the program.
// It should handle anything that needs to happen before GTK takes over,
// for instance, OS signals, dealing with command line args, and exiting the
// process on errors (nothing else should ever call process::exit).
fn main() {
    gtk::init().expect("Failed to start GTK. Please install GTK3.");
    match yourapp::App::new() {
        Err(e) => {
            eprintln!("Error while registering GTK3 application: {:?}", e);
            process::exit(1);
        }
        Ok(app) => {
            app.run(&args().collect::<Vec<_>>());
        }
    }
}

The App struct wraps a gtk::Application and let’s us do our own arg parsing and window management. Let’s look at it next.

app.rs

The App struct is something I always make to encapsulate the gtk::Application, register the standard handlers, manage windows, etc. It also is generally pretty simple; it contains any top level dialogs that will be shared across all windows used by our application, and has a constructor that registers signals that aren’t window specific (such as opening any app dialogs, opening files in new windows, etc.).

The struct itself is generally pretty simple:

/// The app handles creating the UI and reacting to GIO signals.
#[derive(Debug, Clone)]
pub struct App {
    app: gtk::Application,
}

It’s possible that you’d need to store some state on the app struct, but I try to avoid storing any sort of mutable state where possible. Most of the work actually happens in the constructor, which might look something like this for a multi-window GTK application that handles the open file signal:

impl App {
    /// Creates the main application and reacts to the activation signal to
    /// populate a window with a header bar and a view area where the various
    /// application panes can be rendered.
    pub fn new() -> err::Result<Self> {
        // Create a GTK application that handles opening files.
        let app = gtk::Application::new(Some(res::APP_ID), gio::ApplicationFlags::HANDLES_OPEN)?;
        let new_app = Self { app };

After we create the struct, we can call its methods such as a convenient shortcut for creating new app scoped actions, which might register a callback that delegates to an associated function:

        // About action
        new_app.new_action("about", {
            // Load resources, create the dialog, etc.
            let logobuf = res::load_logo().unwrap_or(None);
            let about = ui::new_about_dialog(None::<&gtk::Window>, logobuf);
            move |_, _| Self::on_about(&about)
        });

We also do setup here like asking for a specific theme, loading stylesheets, etc.

        // If our application prefers a specific theme, ask for it.
        // Unfortunately, this won't always work and there's no way to check if
        // it did.
        if let Some(settings) = gtk::Settings::get_default() {
            settings.set_property_gtk_application_prefer_dark_theme(true);
        }

        // If we have any custom application-wide stylesheets, load them from
        // the included CSS file here.
        if let Some(display) = gdk::Display::get_default() {
            let screen = display.get_default_screen();
            let style_provider = gtk::CssProvider::new();
            style_provider.load_from_data(res::STYLE_MAIN)?;
            gtk::StyleContext::add_provider_for_screen(
                &screen,
                &style_provider,
                gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
            );
        }

Finally, we actually start the application by registering it with GTK and connecting to the startup and activate signals. If we created the application with the gio::ApplicationFlags::HANDLES_OPEN flag, we might also need to handle the open signal.

        // Register required signal handlers.
        // If we didn't include the HANDLES_OPEN flag above, we don't need to
        // register for the open signal.
        //
        // For the difference between startup, activate, and open, see:
        // https://wiki.gnome.org/HowDoI/GtkApplication
        new_app.app.connect_startup(|_| {});
        new_app.app.connect_activate(Self::on_activate);
        new_app.app.connect_open(Self::on_open);

        new_app.app.register(None)?;
        Ok(new_app)
    }

Personally, I always like to use methods or associated functions for all my callbacks. Especially on callbacks that could be very long, such as activate, this keeps nesting to a minimum and means you don’t struggle as much scrolling up and trying to figure out if you’re in the activate signal or out in the new function. Readable code is maintainable code.

    /// A callback to invoke when the about action is triggered.
    fn on_about(about: &gtk::AboutDialog) {
        match about.get_window() {
            Some(_) => {
                about.present();
            }
            None => {
                about.run();
            }
        }
    }

    /// A callback to invoke when the app is activated.
    fn on_activate(app: &gtk::Application) {
        let window = window::Window::new(app);

        // Load whatever data we need into the window, and if everything was
        // successful add the window to the application.
        // TODO: you probably want to match here and log any errors, display a
        // dialog, open an in-app notification, etc. to let the user know if
        // something went wrong.
        if window.open_last_project().is_ok() {
            app.add_window(window.as_ref());
        }
    }

    /// A callback to invoke when files should be opened.
    fn on_open(app: &gtk::Application, files: &[gio::File], _hint: &str) {
      // If we're being asked to open new files, create a window for each one.
      // If we use tabs, we'd want to store the primary window that was added to
      // the app in the activate signal and call its open method (or similar)
      // instead.
        for file in files {
            let window = window::Window::new(app);
            if let Some(path) = file.get_path() {
                if window.open_project_file(&path).is_ok() {
                    app.add_window(window.as_ref());
                }
            }
        }
    }

    /// Run the application.
    pub fn run(&self, argv: &[String]) -> i32 {
        self.app.run(argv)
    }

    /// Creates a simple action and connects the given handler to its activate signal.
    pub fn new_action<F: Fn(&gio::SimpleAction, &Option<glib::Variant>) + 'static>(
        &self,
        name: &str,
        f: F,
    ) {
        let action = gio::SimpleAction::new(name, None);
        action.connect_activate(f);
        self.add_action(&action);
    }
}

impl AsRef<gtk::Application> for App {
    #[inline]
    fn as_ref(&self) -> &gtk::Application {
        &self.app
    }
}

// Normally I don't implement GTK traits for my own custom types because they
// contain a lot of methods and Rust doesn't yet have a good way to delegate
// impls to another type, but `ActionMapExt` is small and I use it a lot.
impl ActionMapExt for App {
    fn add_action<P: gtk::IsA<gio::Action>>(&self, action: &P) {
        self.app.add_action(action);
    }
    fn lookup_action(&self, action_name: &str) -> Option<gio::Action> {
        self.app.lookup_action(action_name)
    }
    fn remove_action(&self, action_name: &str) {
        self.app.remove_action(action_name);
    }
}

Also, notice the AsRef implementation? I implement this for most types that I create which wrap a GTK type. It makes it easy to use my type as if it were the primary underlying GTK type (in this case, the application), by calling myapp.as_ref() instead of taking a reference the normal way. You can see a similar AsRef impl being used when adding a window in the example above, so let’s talk about windows next.

window.rs

In src/window.rs I encapsulate a gtk::ApplicationWindow into a custom type that can handle window related logic. Even in a single-window application, it is helpful to have a Window struct that can contain header bars, your main application view, a widget for in-app notifications, etc. It may be tempting to put these on the App struct for simple, single-window applications, but since the scope of projects tends to grow I like to save myself some refactoring time later and always keep window-level concerns separate from application-level concerns. I also like to keep window and application action registration separate, so giving the Window type a separate new_action function (like the example for the Application type above) is a good way to keep those separate and make it obvious where we’re registering actions. This is also a good place to handle opening and closing projects or files, since that will likely only affect the appearance of a single window. As a general rule of thumb:

This means that our Window type isn’t strictly concerned with the UI. Something like “Project” may be a better name for it to avoid confusion, but I’ve chosen to stick with the name of the thing the type is wrapping here. Windows should not register themselves on the application; the application should handle that if creating and setting up the window did not result in an error. Creating a Window can of course setup the underlying window with some defaults, and possibly build a UI or load a UI from a Glade XML file (though I prefer to create my UIs programmatically to find more errors at compile time):

/// A window that our application can open that contains the main project view.
#[derive(Debug, Clone)]
pub struct Window {
    window: gtk::ApplicationWindow,
    notify: ui::Notifier,
    container: gtk::Box,
    header_bar: ui::SplitHeaderBar,
}

impl Window {
    /// Create a new window and assign it to the given application.
    pub fn new(app: &gtk::Application) -> Self {
        let menu = ui::app_menu();
        let header_bar = ui::SplitHeaderBar::new(&menu);
        let hbox = header_bar.get_paned();

        let notify = ui::Notifier::new();
        let window = gtk::ApplicationWindow::new(app);
        window.set_default_size(1280, 720);
        window.set_position(gtk::WindowPosition::Center);
        window.set_title(res::APP_NAME);
        window.set_titlebar(hbox);

        let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
        let container = gtk::Box::new(gtk::Orientation::Vertical, 0);

        main_box.add(notify.as_ref());
        main_box.add(&container);
        window.add(&main_box);

        window.show_all();

        Self {
            window,
            notify,
            container,
            header_bar,
        }

        // TODO: add open method, register actions, etc. as suits your application…
    }
}

We use the ui module a lot in this example, so let’s look at that next.

ui/

The ui module is where I like to put any code that is strictly concerned with the UI (no behavior, loading data, etc.), and will never be reusable in another project. For example, a custom sidebar or a split header bar full of buttons might go here. Each separate widget or UI type is contained in its own file, and then reexported from mod.rs as usual. Your Window type should be constructing its views by creating types from this module.

For example, ui might be where you keep an about dialog type or a function for creating an about dialog with strings specific to your application:

use crate::res;

use gdk;
use gdk_pixbuf;

use gtk;
use gtk::AboutDialogExt;
use gtk::DialogExt;
use gtk::GtkWindowExt;
use gtk::HeaderBarExt;
use gtk::SettingsExt;
use gtk::WidgetExt;

/// Create an about dialog specific to this application that should be re-used
/// each time the user asks to open it.
///
/// If this is a single window app, set parent to the main window, if not, use
/// `None`.
pub fn new_about_dialog<'a, P: gtk::IsA<gtk::Window> + 'a, Q: Into<Option<&'a P>>>(
    parent: Q,
    logobuf_light: Option<gdk_pixbuf::Pixbuf>,
    logobuf_dark: Option<gdk_pixbuf::Pixbuf>,
) -> gtk::AboutDialog {
    let p = gtk::AboutDialog::new();
    p.set_authors(&["Your Name"]);
    p.set_copyright("Copyright © 2019 Your Name.\nAll rights reserved.");
    p.set_destroy_with_parent(true);
    p.set_license_type(gtk::License::Bsd);
    p.set_program_name(res::APP_NAME);
    p.set_skip_pager_hint(true);
    p.set_skip_taskbar_hint(true);
    // I sometimes use a macro to determine language and perform i18n; maybe
    // that's a topic for a future post.
    let title = translate!("About");
    p.set_title(&title);
    p.set_transient_for(parent);
    // Make sure tiling window managers know that this window should probably be
    // floating.
    p.set_type_hint(gdk::WindowTypeHint::Splashscreen);
    p.set_version(res::VERSION);
    p.set_website("https://example.net");
    p.set_website_label("example.net");
    p.add_credit_section(
        translate!("Open Source"),
        &[
            "Gtk-rs http://gtk-rs.org/ (MIT)",
            "Rust https://www.rust-lang.org/ (MIT/Apache-2.0)",
        ],
    );

    // We expect to reuse this dialog across multiple windows, so don't destroy
    // it when the window is closed.
    p.connect_delete_event(|p, _| gtk::Inhibit(p.hide_on_delete()));
    p.connect_response(|p, _| p.hide());

    // If the window manager doesn't use a header bar, slap one on there.
    // I just think this makes the about dialog look nicer on eg. MATE.
    if p.get_header_bar().is_none() {
        let hbar = gtk::HeaderBar::new();
        hbar.set_title(
            p.get_title()
                .unwrap_or_else(move || title)
                .as_str(),
        );
        hbar.show_all();
        p.set_titlebar(&hbar);
    }

    // This isn't strictly necessary, but on about dialogs I like to swap the
    // logo back and forth if the theme changes. This isn't guaranteed to work.
    if let Some(logo_dark) = logobuf_dark {
        if let Some(logo_light) = logobuf_light {
            match p.get_settings() {
                Some(settings) => Some(
                    settings.connect_property_gtk_application_prefer_dark_theme_notify(
                        clone!( p, logo_dark, logo_light => move |settings| {
                            if settings.get_property_gtk_application_prefer_dark_theme() {
                                p.set_logo(&logo_dark);
                            } else {
                                p.set_logo(&logo_light);
                            }
                        }),
                    ),
                ),
                None => None,
            };
        }
    }

    p
}

widget/

The widget module is similar to the ui module in that it contains types which are purely concerned with UIs, but I tend to put smaller, self-contained types that might be reused in other projects here. This is a fine distinction, and it’s not really necessary to make, but it makes it slightly easier to pull them out into their own crates later if I ever decide to reuse a widge, and since UI must import them from this crate it makes me sure that I won’t accidentally tie a standalone widget to functionality that is only useful in my application.

Here’s an example of something that belongs in widget, a quick way of creating a gtk::Entry that validates XMPP addresses (JIDs) or emails using a naive regular expression. The widget is simple, and self-contained.

use gtk;
use gtk::EditableSignals;
use gtk::EntryExt;

use lazy_static::lazy_static;
use regex::Regex;

/// Returns a `gtk::Entry` that validates that it contains an XMPP address.
pub fn jid_entry<F: Fn(&str, bool) + 'static>(on_change: F) -> gtk::Entry {
    let jid_entry = gtk::Entry::new();
    jid_entry.set_placeholder_text(format!("{}@example.com", translate!("user")).as_str());
    jid_entry.set_input_purpose(gtk::InputPurpose::Email);
    jid_entry.set_activates_default(true);

    // This is probably good enough for simple front-end validation, but we
    // could also use the xmpp-addr crate if we want to do better validation
    // (it also works pretty well for emails if you stripe off the
    // resourcepart of the JID).
    //
    // See: https://docs.rs/xmpp-addr
    lazy_static! {
        static ref RE: Regex = Regex::new(r"[^@/]+@[^@/]+(/.+)?").unwrap();
    }

    // Setup basic (non-binding) validation of the JID entry field.
    jid_entry.connect_changed(move |entry| {
        match entry.get_text() {
            None => {
                entry.set_icon_from_icon_name(
                    gtk::EntryIconPosition::Secondary,
                    "dialog-warning-symbolic",
                );
                on_change("", false);
            }
            Some(text) => {
                if RE.is_match(&text) {
                    entry.set_icon_from_icon_name(gtk::EntryIconPosition::Secondary, None);
                    on_change(&text, true);
                } else {
                    entry.set_icon_from_icon_name(
                        gtk::EntryIconPosition::Secondary,
                        Some("dialog-warning-symbolic"),
                    );
                    on_change(&text, false);
                }
            }
        };
    });

    jid_entry
}

Conclusion

This post mostly covered the basics of structuring an application. Of course, there are much more important concerns when writing a GTK3 application, which I may write about in future posts. If that’s something you’d like to see, feel free to drop me a line with your suggestions.