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:
$ tree
.
├── Cargo.toml
├── README.md
├── res
│ ├── img
│ │ ├── logo.svg
│ └── style
│ └── main.css
└── src
├── app.rs
├── macros.rs
├── main.rs
├── res
│ ├── icons.rs
│ └── mod.rs
├── ui
│ ├── about.rs
│ ├── app_menu.rs
│ ├── header_bar.rs
│ ├── mod.rs
│ ├── notifier.rs
│ └── sidebar.rs
├── widget
│ ├── jid_entry.rs
│ └── mod.rs
└── window.rs
7 directories, 18 files
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::<>k::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: >k::AboutDialog) {
match about.get_window() {
Some(_) => {
about.present();
}
None => {
about.run();
}
}
}
/// A callback to invoke when the app is activated.
fn on_activate(app: >k::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: >k::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) -> >k::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:
- Applications create one or more windows per file or project we want to open
- Windows handle loading their own data from one or more file or projects
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: >k::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.