Let’s write a mail viewer with Rust and Qt. This is another blog about Rust Qt Binding Generator, the project that lets you add a Qt GUI to your Rust code, or if you will, add Rust to your Qt program.
Previous blogs about Rust Qt Binding Generator covered the initial announcement, building a simple clock, and making a todo list. Now, I’ll show a bit of how to make a more complex application: an email reader.
But first: Rust Qt Binding Generator takes an unusual approach to
bindings. It is not a complete binding from the Qt API to Rust because I
think that that is impossible: the C++ Qt API does not have all the
safety guarantees that Rust has. So the binding API would be full of
unsafe
negating a big advantage of using Rust.
Instead, Rust Qt Binding Generator generates a binding for just your code to make your Rust code available to a Qt GUI.
In the first tutorial, we showed a clock. The model shared between
Rust and Qt had three simple properties: hour
,
minute
and second
. The second blog was about a
todo application. There, the model was a list of todo items shared
between Rust and Qt.
This time, we move on to a more complex object: a tree.
We’re keeping with the theme of personal information management and
are writing an email viewer. It can read mail from MailDir folders and
IMAP servers. It is completely readonly and will not even change the
state of your messages from unread
to read
. I
feel comfortable using it on my own mails alongside my real mail
programs.
The code is available in my personal KDE space. It requires Rust, Cargo, Qt, CMake, OpenSSL, and ninja (or make). You can retrieve and compile it with
git clone https://anongit.kde.org/scratch/vandenoever/mailmodel
mkdir mailmodel/build
cd mailmodel/build
cmake -GNinja ..
ninja
The code is about 2200 lines of Rust and 550 lines of QML. Parsing mails and communicating over IMAP is done by three crates: mailparse, imap-proto and imap.
In an email application there are usually two prominent trees: one shows the email folders and the other shows the messages in the selected folder.
First we model the list of folders. Here is the JSON object from bindings.json
that does this.
{
"MailFolders": {
"type": "Tree",
"itemProperties": {
"name": {
"type": "QString"
},
"delimiter": {
"type": "QString"
}
}
}
}
The type of MailFolders
is Tree
. Each node
in the tree has two properties: name
and
delimiter
. Rust Qt Binding Generator generates Qt and Rust
code from this. The Qt code (Bindings.cpp
and Bindings.h
)
defines an implementation of QAbstractItemModel
.
This is the same base class as in the todo example. This time, it holds
a tree instead of a list.
There is also Rust code generated. The file interface.rs
is the binding to the Qt code. It defines a trait
MailFoldersTrait
that the developer needs to implement in a
struct called MailFolders
.
We’ll discuss some parts of the Rust implementation file.
The implementation should be backed by a structure. There are two
structures: MailFolder
which represents a node in the tree
and MailFolders
which contains all the nodes in a
Vec
and interfaces for communicating with other parts of
the program.
/// Nodes in the tree
struct MailFolder {
: String,
name: String,
delimiter/// position of the parent node in `folders` Vec
: Option<usize>,
parent/// position of child folders in the `folders` Vec
: Vec<usize>,
subfolders}
/// Rust-side implementation of QAbstractItemModel via MailFoldersTrait
pub struct MailFolders {
/// interface for emitting signals from any thread to the UI
: MailFoldersEmitter,
emit/// interface for emitting signals from the UI thread to the UI
: MailFoldersTree,
tree/// all nodes in the tree
: Vec<MailFolder>,
folders/// interface for sending new data to this object
: Arc<Mutex<MailFoldersData>>,
data}
In the tree, each node has a unique index. The index is used by Qt to find out information about the node, like how many children (rows) it has or to get out data like the name.
These functions correspond to the C++ virtual functions in QAbstractItemModel.
impl MailFoldersTrait for MailFolders {
/// Returns the name for a particular node
fn name(&self, index: usize) -> &str {
&self.folders[index].name
}
/// Returns the number of rows for a node with a given index.
/// To get the number of root nodes, call `row_count(None)`.
fn row_count(&self, index: Option<usize>) -> usize {
self.folders[index.unwrap_or(0)].subfolders.len()
}
/// Returns the index for n-th row in parent.
/// To get the index of the n-th root node, call `index(None, n)`.
fn index(&self, parent: Option<usize>, row: usize) -> usize {
self.folders[parent.unwrap_or(0)].subfolders[row]
}
/// Returns the index of the parent node or `None` if there is no parent.
fn parent(&self, index: usize) -> Option<usize> {
self.folders[index].parent
}
/// Returns the row number of a node with given index.
fn row(&self, index: usize) -> usize {
if let Some(parent) = self.folders[index].parent {
// iterate through the parents list of children to find index
self.folders[parent]
.subfolders
.iter()
.position(|i| *i == index)
.unwrap()
} else {
0
}
}
}
The user interface should stay responsive. So intense and slow work like reading and parsing email is done in a separate thread. The user interface starts a thread to do the hard work and sends commands to it via a channel.
When new data is available, the UI needs to update. This must be done by the UI thread. When the processing thread has new data it emits a signal to the UI thread. The UI thread then aquires accesses the data via a mutex that is shared between the two threads.
The Rust-implemented model is used from the QML. The connection
between the TreeView
and the model is made by the line
model: mailmodel.folders
. Each node is rendered according
to the Text
delegate. When the user selects a different
folder the model is notified of this by handling the
onCurrentIndexChanged
event.
TreeView {id: folders
model: mailmodel.folders
TableViewColumn {title: "Name"
role: "name"
width: folders.width - 20
delegate: Text {
text: icon(styleData.value) + " " + styleData.value
verticalAlignment: Text.AlignVCenter
}
}onCurrentIndexChanged: {
//...
.currentFolder = path;
mailmodel
}style: TreeViewStyle {
highlightedTextColor: palette.highlightedText
backgroundColor: palette.base
alternateBackgroundColor: palette.alternateBase
}headerVisible: false
frameVisible: false
}
The list of folders is one of five object defined in the model. The
others are the tree for the message threads in a folder (middle pane in
the screenshot above), the current email (right pane), the list of
attachments for the current email and an overall object that contains
all the other ones. The latter, MailModel
, is the initial
entry point for the user interface. The user-initiated commands are sent
to the processing thread from that overall object.
Create a configuration file for MailDir or IMAP.
{
"MailDir": {
"path": "/home/user/mail"
}
}
The path for the MailDir configuration is the folder that contains
.inbox.directory
.
{
"IMAP": {
"domain": "imap.electronmail.org",
"username": "username",
"password": "v3ry53cur3",
"port": 993,
"ssl": true
}
}
and run the code
./mailmodel configuration.json
This GUI is built with QML via Qt Quick Controls. One might as well write one with Qt Quick Controls 2, QWidgets or Kirigami. The majority of the code, the Rust code, could stay the same. With the appropriate abstractions one might even use a different GUI framework and still keep the core application logic. Just imagine: KDE and GNOME joined together by Rust.
Comments
Post a comment