My first GUI with GTK and Rust
Figuring out how to do the most basic of things

23 October 2016

I have been working on projects with a web front end for a long time now. I know my way around HTML, CSS and JavaScript fairly well. But when I’m working on side projects at home, I want to branch out and see how it would work if I use completely different tools. I think doing things deliberately outside of your experience is important for growth as a developer.

This article is an explanation on how I solved a problem I was having. If you know of a better way, please share it with me in the comments, since I am new to this particular combination of tools.

Some Background

The side project I’ve started is a small real-time audio signal processing thing. None of the audio or signal processing are in place yet, except for reading a list of microphones to allow user selection.

I’ve written before about Rust as a language that I want to get to know well. For a user interface, I’ve decided that I want to learn to use GTK, an open source and cross platform GUI toolkit. I’m using a Rust binding for GTK so that I don’t need to figure out calling C code from Rust just yet.

First Attempts

My first attempt involved a hierarchy of functions to set up the UI. On the top you’d create a window. The window would call a function to make the drop down, and any other controls I needed. It looks something like this:

use gtk;
use gtk::prelude::*;

pub fn start_gui() -> Result<(), String> {
    //Initializes the audio API, and gets the list of microphones
    let pa = try!(::audio::init().map_err(|e| e.to_string()));
    let microphones = try!(::audio::get_device_list(&pa).map_err(|e| e.to_string()));

    //Does all of the config for the UI
    try!(gtk::init().map_err(|_| "Failed to initialize GTK."));
    let window = create_window(microphones);

    //Let GTK take over the thread. It will loop until we quit.
    gtk::main();

    //Once GTK exits, we return from the function. It's also the end
    //of the program.
    Ok(())
}

fn create_window(microphones: Vec<(u32, String)>) -> gtk::Window {
    let window = gtk::Window::new(gtk::WindowType::Toplevel);
    window.set_title("Musician Training");

    let dropdown = create_device_dropdown(microphones);
    window.add(&dropdown);
    window.set_default_size(300, 300);
    window.show_all();
    //Functions that start with connect_ are, by and large, connecting
    //events. This is the one for clicking the "close" button on the
    //window.
    window.connect_delete_event(|_, _| {
        gtk::main_quit();
        Inhibit(false)
    });
    window
}

fn create_device_dropdown(microphones: Vec<(u32, String)>) -> gtk::ComboBoxText {
    let dropdown = gtk::ComboBoxText::new();
    for (index, name) in microphones {
        dropdown.append(Some(format!("{}", index).as_ref()), name.as_ref());
    }
    dropdown.connect_changed(|ref dropdown| {
        println!("{}", dropdown.get_active_id().unwrap());
        //Problem: somehow, this needs to change state in a much
        //higher scope, so we start listening on a difference
        //microphone.
    });
    dropdown
}

This idea of having each component sitting completely isolated in its own function seemed like a good idea, until I needed to do something meaningful with the change event on my drop down. Ideally, managing the state for which microphone I’m listening to wouldn’t by global, nor would it be live directly with the drop down. I need to be able to connect that event up in the start_gui function, where I’m managing things like the list of microphones already.

How do I get a reference to that drop down?

Given the size of the code as it stands, the easiest answer would be to just return the drop down as well from the create_window function. This approach will probably work well at first, but end up being difficult to maintain as the amount of things on the window grows. Let’s think of other alternatives.

Looking at the API docs, it is possible to get to the drop down by going through the window. The function would let me get all of the children, and I’d need to navigate through whatever tree of components to find the drop down, and then cast it to the appropriate type. This would break very easily if I make visual changes, like changing the order of components, so I want to avoid this as well.

There is another way in the API, that involves changing how I’ve done everything related to the GUI. You write some XML to describe your interface and pass the file to GTK. It will read the file and build up the components for your interface. You can then get direct access to any component that you’ve given an ID in the XML. This feels like the best solution so far, since it isn’t coupling the layout, order and nesting of components to the logic for connecting events to them.

XML

After I’ve rewritten the code to use XML for the structure of the interface, it looks more like this. I included the XML as a constant string in the source code since at this point it’s a very simple interface. If it becomes more complicated, it can be read directly from a file instead of a string.

use gtk;
use gtk::prelude::*;

const GUI_XML: &'static str = r#"
<interface>
  <object class="GtkWindow" id="window">
    <property name="title">Rusty Microphone</property>
    <child>
      <object class="GtkComboBoxText" id="dropdown">
      </object>
    </child>
  </object>
</interface>
"#;

pub fn start_gui() -> Result<(), String> {
    let pa = try!(::audio::init().map_err(|e| e.to_string()));
    let microphones = try!(::audio::get_device_list(&pa).map_err(|e| e.to_string()));
  
    try!(gtk::init().map_err(|_| "Failed to initialize GTK."));

    let gtk_builder = try!(create_window(microphones));
  
    let dropdown: gtk::ComboBoxText = try!(
        gtk_builder.get_object("dropdown")
                   .ok_or("GUI does not contain an object with id 'dropdown'")
    );
    dropdown.connect_changed(|ref dropdown| {
          println!("{}", dropdown.get_active_id().unwrap());
          //I now have the dropdown on the same level as where I want
          //my logic to be called.
      });

    gtk::main();
    Ok(())
}

fn create_window(microphones: Vec<(u32, String)>) -> Result<gtk::Builder, String> {
    let gtk_builder = gtk::Builder::new_from_string(GUI_XML);
    let window: gtk::Window = try!(
        gtk_builder.get_object("window")
                   .ok_or("GUI does not contain an object with id 'window'")
    );
    //some properties aren't immediately obvious how to put into XML
    window.set_default_size(300, 300);
    window.connect_delete_event(|_, _| {
        gtk::main_quit();
        Inhibit(false)
    });
    window.show_all();

    let dropdown: gtk::ComboBoxText = try!(
        gtk_builder.get_object("dropdown")
                   .ok_or("GUI does not contain an object with id 'dropdown'")
    );
    //set the list of microphones as before
    for (index, name) in microphones {
        dropdown.append(Some(format!("{}", index).as_ref()), name.as_ref());
    }

    Ok(gtk_builder)
}

So this very nicely makes the drop down, and any other component with an ID, accessible from anywhere. However, there’s one more challenge to actually having this drop down affect the selected microphone.

What the mutable state?

The next problem I ran into is that I can’t mutate external state from inside the callback. This is because the interface for the callback that connect_changed accepts is Fn, and not FnMut. Luckily, Rust has a mechanism to move your mutable borrow checking up to run time to handle such a situation. The short version is to use the RefCell or Cell structs from the standard library. The long version can be found in the Rust documentation.

use gtk;
use gtk::prelude::*;
use std::cell::Cell;

pub fn start_gui() -> Result<(), String> {
    // ... set up everything else as before
    let selected_mic: Cell<Option<u32>> = Cell::new(None);
    
    dropdown.connect_changed(move |dropdown: &gtk::ComboBoxText| {
        selected_mic.set(dropdown.get_active_id().and_then(|id| id.parse().ok()));        
        println!("{}", selected_mic.get().unwrap());
    });
}

And now, since I can do things that mutate shared state outside of the callback, I should be able to change the selected microphone. That is, once I get to implementing listening on any microphone at all…

Update (25 April 2017)

I’ve written a new post on my thoughts around this problem, having spent some more time on it now. You can find the new post here.