Abstract
I've recently started using GTK from Rust. I'm new to this, and here are some of the patterns that I've found so far to stick things together.
Graphical User Interfaces
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: >k::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.