Rust and GTK from a React perspective
January 15, 2020Recently, after a few failed attempts at using other frameworks to make an application that was both easy to use and easy to install, I embraced native software development with Rust and GTK.
Though I have made short forays in the past, GTK was a change for me. Before this, most of my user interface experience came from building React applications. The transition from React to GTK posed some challenges. Most came from differences in widget philosophy. GTK in Rust, though, is particularly hard because of the extra rules Rust enforces to protect against memory management errors and against operations that are unsafe to do in a threaded context.
In this article, I will talk primarily how I adapted the philosophies from React into GTK, and I will highlight some of the extra tricks that are necessary to make GTK conform to Rust’s rules. Rust enforces some tricky rules that will be unfamiliar to most developers, primarily in terms of how values can be shared, but also with strong restrictions on mutability. I’ll point out these rules as they come up throughout this article.
All of the examples in this article come from FitnessTrax, a privacy-first fitness tracking application. Users are able to collect fitness and biometric data in a single place on their personal computers, without depending on companies that may not appropriately protect their user’s data in the long term.
I apologize for the appearance of the application, because as of the 0.4 release I have not taken the time to learn much about how GTK handles styling. I promise that I will improve the UI significantly soon.
Some differences in framework philosophy
The developer of Conrod, a graphical toolkit for Rust that experiments with applying Functional Reactive Programming techniques to graphics programming, describes two significantly different modes for managing graphical components. In “Retained Mode”, which is the common mode for most native graphical programming, any given screen component is created, then updated repeatedly throughout its lifetime. In “Immediate Mode”, components will have a draw method in which they freshly instantiate all of their children. The framework then compares this tree to the previous tree to determine how to update the screen.
React operates entirely in Immediate mode, while GTK operates entirely in Retained mode. In web developement, D3, a popular data visualization library, also works in retained mode, and in 2018 I wrote an article about interfacing between React and D3.
React paired with Redux or Apollo-GraphQL implements some of the concepts of Functional Reactive Programming in that it automatically handles propogating data changes to components. My introduction to FRP came from Elise Huard’s book “Game Programming in Haskell”. This book may be getting out of date by this point, but it does serve as a good introduction to the concept in the context of a particular FRP library in Haskell. Unfortunately, FRP has not seen wide adoption outside of React. Though there is at least one FRP library available for Rust, at the time of this writing it feels a bit too immature for me to adopt. As such, with a bit of creativity and my experience with React, I have designed mechanisms that approximate the FRP paradigm.
A note on terminology:
- A widget is a GTK object which represents something on screen. This could be a Window, a Button, a Label, or a Layout container. GTK widgets can only have other GTK widgets as their children.
- A component is any logical abstraction of a section of the screen. In simple cases, this will be a GTK widget returned from a function. In more complex cases, it may be a structure which contains one or more widgets. Components cannot necessarily be passed into GTK functions. Structure components always provide a public
widget
field which represents the root widget for this component.
An immutable value display
The simplest of all components is, much like a React component, a small collection of widgets that gets created and then never gets updated. This can be implemented simply as a function that returns a GTK widget.
pub fn date_c(date: &chrono::Date<chrono_tz::Tz>) -> gtk::Label {
gtk::Label::new(Some(&format!("{}", date.format("%B %e, %Y"))))
}
This pattern works when a component really is meant to be a visual component that rarely, or even never, updates. In my application, date labels are subcomponents of larger displays, and thus are the kind of thing that never change.
A component with internal widget state
Components with internal widget state only can be significantly more complex, yet can still be implemented as a function which returns a GTK widget. The the caller could read the data directly out of the returned GTK widget, this pattern arguably works best when the caller supplies a callback, and the component encodes rules for when to call the callback.
I have a validated text entry field. It is a regular gtk::Entry, but the interface abstracts the text handling behind render
, parse
, and on_update
functions.
pub fn validated_text_entry_c<A: 'static + Clone>(
value: A,
render: Box<dyn Fn(&A) -> String>,
parse: Box<dyn Fn(&str) -> Result<A, Error>>,
on_update: Box<dyn Fn(A)>,
) -> gtk::Entry {
let widget = gtk::Entry::new();
widget.set_text(&render(&value));
let w = widget.clone();
widget.connect_changed(move |v| match v.get_text() {
Some(ref s) => match parse(s.as_str()) {
...
},
None => (),
});
widget
}
The caller must provide an initial value, a render
function, a parse
function, and an on_update
function. In my implementation, the validated text entry will attempt to parse the string within the box after each change, and will call the on_update
function only if parsing succeeds. The caller is thus responsible for saving the data, but does not have to worry about the mechanics around parsing or verifying that it has valid data.
I find this pattern particularly useful in forms where I opt to store all of the values of a form together in one place. Storing all of the data together lets me notify the user immediately of errors, lets me detect errors that occur as a result of invalid combinations of data, and lets me easily disable the Submit button when there are errors present.
Components with internal state
2020-01-31: it turns out that I have made some big mistakes in the code in this section. I will need to revise it pretty significantly to handle more efficient component updates, and changing component state in a GTK callback.
While I build my application out of simple components like those above, I put them together into more sophisticated components that have multiple pieces of data that logically belong together but mechanically get edited within the various subcomponents. For this, I set up an internal state independently of the state of the subcomponents.
Fortunately, I can still usually implement this as a function.
Take the instance of a bike ride, which I have abstracted to a “time/distance” record. A time/distance event has a start time, an activity type (bike ride, walk, run, kayak trip…), a distance, and a duration. My user interface binds all of these together into a single component that worcks on the entire record at once.
pub time_distance_record_edit_c(
record: TimeDistanceRecord,
on_update: Box<dyn Fn(TimeDistanceRecord)>,
) -> gtk::Box {
}
Here we start to run into the rules that Rust enforces to guarantee safe memory management.
Every value has exactly one owner. While you can get a borrowed reference to that value, those references must go out of scope before that value’s owner goes out of scope. Additionally, you can only get a mutable reference if there are no other references of any kind. The Rust Book talks in detail about these rules and provides a significant number of examples and scenarios.
Fortunately, all of the parts are already here. I need a way to share the record across multiple callback functions, and I need a way to ensure safe multithreaded access to the record. We solve the sharing problem with an Arc. This is a thread-safe reference counted container. Any value passed to the Arc’s initializer becomes owned by the Arc. Cloning an Arc increments the reference count and creates a second reference that points to the shared value.
Arcs do not allow mutable access to the values they contain, so we need to also include an RwLock. As expected, an RwLock allows many readers but only a single writer, and no readers are allowed when there is a writer. So, here is how we safely mutate the record:
pub time_distance_record_edit_c(
record: TimeDistanceRecord,
...) -> gtk::Box {
let record_ref = Arc::new(RwLock::new(record));
{
let mut rec = record_ref.write().unwrap();
ref.activity = Cycling
}
Within the sub-block of code, rec
becomes a mutable reference to the record data. RwLock
governs read/write access to the data, while Arc
allows the data to be shared across functions or even threads.
Putting it all together, our code looks like this:
pub time_distance_record_edit_c(
record: TimeDistanceRecord,
...
on_update: Box<dyn Fn(TimeDistanceRecord)>,
) -> gtk::Box {
let on_update = Arc::new(on_update);
let record = Arc::new(RwLock::new(record));
let duration_entry = {
let record = record.clone();
let on_update = on_update.clone();
let duration = record.read().unwrap().duration.clone();
duration_edit_c(
&duration,
Box::new(move |res| match res {
Some(val) => {
let mut r = record.write().unwrap();
r.duration = Some(val);
on_update(r.clone());
}
None => (),
}),
)
};
}
(note: functions are always read-only and so require only the Arc
for sharing)
To recap, in the above function, we have a block of code which clones the Arc
containing the record. That clone gets moved into the callback function for duration_edit_c
(meaning that the callback function now owns that particular clone). Within the callback funuction, the record will be borrowed mutably, updated, the data cloned and passed to on_update
, and then the write lock will be automatically dropped at the end of the block.
This is a lot to absorb all at once. If you are not familiar with Rust, I definitely recommend reading about the ownership and borrow system, which is the magic that takes memory management away from the developer without incurring the cost of a garbage collector.
Updating from system state changes
Finally, the fourth pattern covers all components that need to respond to system changes. In React terms, this means property changes, possibly from Redux.
At a high level, we need a struct
which keeps track of all of the visual components that may be updated given new data, and a render
function which will handle those updates and return the root level widget.
For this example, I provide my History component.
struct HistoryComponent {
widget: gtk::Box,
history_box: gtk::Box,
}
pub struct History {
component: Option<HistoryComponent>,
ctx: Arc<RwLock<AppContext>>,
}
impl History {
pub fn new(ctx: Arc<RwLock<AppContext>>) -> History { ... }
pub fn render(
&mut self,
range: DateRange,
records: Vec<Record<TraxRecord>>,
) -> >k::Box { ... }
The constructor here is actually quite simple, doing nothing more than creating the abstract History
component. It doesn’t even create the widget at this point, as it has no data to populate into the widget. This is rather convenient because at construction time components may require data that is not available yet.
The bulk of the work appears in render
:
pub fn render(
&mut self,
range: DateRange,
records: Vec<Record<TraxRecord>>,
) -> >k::Box {
match self.component {
None => {
let widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);
/* create and show all of the widgets */
self.component = Some(HistoryComponent {
widget,
history_box,
});
self.render(prefs, range, records)
}
Some(HistoryComponent {...}) => {
..
}
}
}
If this is the first call to render, the visual components will not exist yet. Render will create all of the components, and then call itself again to populate them with the data.
pub fn render(
&mut self,
range: DateRange,
records: Vec<Record<TraxRecord>>,
) -> >k::Box {
match self.component {
None => {
...
}
Some(HistoryComponent {
ref widget,
ref history_box,
...
}) => {
history_box.foreach(|child| child.destroy());
records.iter().for_each(|record| {
let ctx = self.ctx.clone();
let day = Day::new(
record.clone(),
ctx,
);
day.show();
history_box.pack_start(&day.widget, true, true, 25);
});
&widget
}
}
}
On subsequent calls, render will handle updating the widgets. The details of how to populate the new data will vary pretty significantly by component. In this case I destroy all of the existing subcomponents and create new ones based on the data that I have. This is a pretty naive strategy, but sometimes it works.
Conclusion
And so, here they are. Four high level patterns that I discovered through weeks of learning how to program GTK. I doubt that this will be their final form.
Even over the course of writing this article I significantly modified, refactored, and simplified my components. I imagine that these four patterns will take me very far in this application, while also expecting to learn much more as I proceed.