Rust GUI: Introduction, a.k.a. the state of Rust GUI libraries (As of January 2021)

davidedelpapa

Davide Del Papa

Posted on January 18, 2021

Rust GUI: Introduction, a.k.a. the state of Rust GUI libraries (As of January 2021)

[Photo by Kelly Sikkema on Unsplash, modified (cropped)]

This article follows up on a discussion. Please share your experience there, I'd gladly hear about it!

Introduction

Some months ago I was surveying a little the state of GUI in Rust. Let me tell you, the quest has been horrible.

Starting off some searches on internet, apart some questions on Reddit and other sites, you get some (very old) articles.

Reddit Logo What’s the state of Rust GUIs?

Rust has always seemed super interesting to me since learning of its existence a few months ago. However one thing that turned me off at the time was the lack of (structured) GUI bindings for cross platform devices. But fast forward a few months, is there a good GUI library to use that can make “complex” applications? Or maybe a…

Old and stale.

The only guide you get at the end of the day (referenced by all) is the site Are We GUI Yet which is not helpful at all.

GitHub logo areweguiyet / areweguiyet

A website built for the Rust community

Readme

Want to find crates for GUI in Rust? Then you've come to the right place!

Status

This site is maintained as best as possible. The next steps likely involve replacing it with an automated system more like lib.rs.

CI runs weekly to update crate information from crates.io, if it is not overriden by the crate's configuration in this repo.

What is this?

Companion website to arewegameyet arewewebyet, and arewelearningyet.

Contributing

To make it easy for people (hey, that's you! 😊) to contribute, AreWeGuiYet uses a custom Rust CLI. It's currently a work in progress and is a little rough around the edges.

The workflow:

  • fork AreWeGuiYet
  • clone your fork
  • cd to the CLI directory (cli) (it currently uses relative paths 😬)
  • Build and run the CLI
    • Usage: cargo run -- [--clean] <command> [flags]
    • Help: cargo run -- help
  • When you're done…

It seems that AreWeGUIYet has not been updated since 2019, and the interface does not help in the least to search for a suitable library. The only tags it has is on the kind of implementations the crates have. Not many articles, not a useful tutorial, no understanding about the status of the crates that are showcased there.

Some featured crates seem pretty rough, some more advanced... but there's no way to know it other than trying each and every one.

Oh.
My.

And I did install some, but the results were a disaster, because at least the ones I tried sucked a lot.

Then, over the summer I found an article by boringcactus. They had much my same experience, but I realized that I actually managed to make many more things to work than they did...

Flash forward to early November. Here I was, trying again the old way. I was serious with my intention to try each and every crate in the list.

This time around I got even luckier. At least I did not only succeeded in make some libraries to work, I actually got material to write some tutorials to help others.

Here I'll wet your appetite: there's more to come!

Disclaimers

  • I only tried the following on my Linux laptop: sorry, I do not own a Windows or a Mac!
  • I usually employ cargo add from cargo edit (to install it: cargo install cargo-edit), but you can add the dependency straight to Cargo.toml
  • I tried to make the crates work in every possible way, and I meant it. I even patched them locally, and even submitted some issues or even a PR. I wanted to honestly do my homework

Summary Table

Below a summary of the crates I tried, with the status I found them in:

Status Name Notes
Working Druid Review It relies on piet 2D graphic library; on Linux that means GTK/Cairo 2D primitives
FLTK Review FLTK for Rust
GTK+ Review Official GTK+ Rust support
Iced Review Elm Inspired
IUI Review Based on libui
OrbTK Review Even if it is designed for Redox, it still works on other systems
QMetaObject Review QT based: it relies heavily on QML
Relm Review Elm-inspired, GTK improvement
Sciter Review Bindings to Sciter, a commercial Library
Tauri Review This is a different beast altogether: it transforms a web app to a desktop app, while adding the possibility to interface with Rust
VGTK Review Built on top of GTK, and Elm-inspired
Other/Could not try Conrod Review The examples work. How to build a GUI is still a mystery though.
ImGui Review Same thing as Conrod, no way to understand how it works, besides the examples
Core-Foundation Review Could not try: I do not have a Mac
LittlevGL Review Could not try: it runs on embedded systems
kas Review Could not try: I don't have Vulkan hardware, and the GTK backend is not working at all
Not Working Azul Review Based on WebRender. I tried many times over to make it work... It refuses!
QT_Widgets Review There's a small issue, but it does not work until it is solved

There are also two entries that I excluded completely, because I do not think they belong to Rust GUIs: WebRender and Rust-QT-Binding-Generator. Tauri almost got kicked out as well, but it stayed in, since it is another beast altogether (because it is a bridge between technologies, more than between languages as Rust-QT-Binding-Generator is).


Working

Druid

One of the crates I could not make to work the first time around (I don't remember why), but in reality Druid works like a charm now.
It relies on piet which is a 2D graphic library. On Linux it wraps GTK3 and its cairo 2D primitives, so check out all the requirements needed on your distro.
It has also a web based render engine, and it uses wasm and wasm-pack.

Let's give it a try.

cargo new rust-ui-druid
cd rust-ui-druid
cargo add druid
Enter fullscreen mode Exit fullscreen mode

Then we can set up our first app in src/main.rs:

use druid::widget::{Button, Flex, Label};
use druid::{AppLauncher, LocalizedString, PlatformError, Widget, WidgetExt, WindowDesc, Data};

#[derive(Clone, Data)]
struct Counter(i32);

fn main() -> Result<(), PlatformError> {
    // Window builder. We set title and size
    let main_window = WindowDesc::new(ui_builder)
        .title("Hello, Druid!")
        .window_size((200.0, 100.0));

    // Data to be used in the app (=state)
    let data: Counter = Counter(0);

    // Run the app
    AppLauncher::with_window(main_window)
        .use_simple_logger() // Neat!
        .launch(data)
}

fn ui_builder() -> impl Widget<Counter> {
    // The label text will be computed dynamically based on the current locale and count
    let text = LocalizedString::new("hello-counter")
        .with_arg("count", |data: &Counter, _env| (*data).0.into());
    let label = Label::new(text).padding(5.0).center();

    // Two buttons with on_click callback
    let button_plus = Button::new("+1")
        .on_click(|_ctx, data: &mut Counter, _env| (*data).0 += 1)
        .padding(5.0);
    let button_minus = Button::new("-1")
        .on_click(|_ctx, data: &mut Counter, _env| (*data).0 -= 1)
        .padding(5.0);

    // Container for the two buttons
    let flex = Flex::row()
        .with_child(button_plus)
        .with_spacer(1.0)
        .with_child(button_minus);

    // Container for the whole UI
    Flex::column()
        .with_child(label)
        .with_child(flex)
}
Enter fullscreen mode Exit fullscreen mode

Druid is pretty data-driven, so you see that widgets implement data-types: they are not a type unto themselves. That explains why a Widget type has to be declared as Widget<T>. We notice also that Druid employs the constructor pattern a lot.

Code notes:

#[derive(Clone, Data)]
struct Counter(i32);
Enter fullscreen mode Exit fullscreen mode

We create a custom data structure to employ as our app's state. It has to derive druid::Data.

fn main() has to return a Result<(), PlatformError>, because the AppLauncher might fail.

Inside main() we create a new main widow descriptor WindowDesc out of a widget we'll create with the function ui_builder(). We set title and state of the main window.

We init the custom state (Data in druid's jargon), and we pass the window and the state to the AppLauncher, which will manage the app's cycle for us.

Notice that Druid implements a simple logger to the stderr, which I think it's a neat function for debugging.

Inside the fn ui_builder() we first create a localized string (LocalizedString), which renders a translatable text. Also this is a neat function, to have localization embedded in the regular flow of development. We assign the "hello-counter" key to it; since this is bundled as a default (for example's purpose) the translation will be automatically created for us, according to the locale (by now only English, French and German). we assign this localized string to a label (we set the center position and a padding to it).

The we create two buttons, one to increase and the other to decrease our state counter, with a callback on the on_click() event. The state will be increased (decreased), and Druid will take care to update the interface to reflect the state for us (no need to signal to the label to update its text, it's Druid's duty... Very nice!)

We create a Flex which is a container, and we stack the two button horizontally, with a space in between.

Then we return another Flex where we stack vertically the label and the Flex that contains the buttons. This is the UI that our window descriptor takes as argument.

We can now run our first Druid application.

Druid example app

Example Repo:

Back to Summary

FLTK

Next in the list is FLTK. The first time around, I admit that I didn't even consider it: having a little C/C++ background I have actually used FLTK sometime before and, let me tell you, it's quite "complicated", and with poor results. It is not feature filled as wxWidgets or QT (it's much more bare-bones actually), but still it manages to have a way in which you have to code that... ugh! Well, I didn't even consider it because of my negative experiences in the past. However, now I'm told that FLTK has come a long way since the times I was using it in C++, so this should teach me something about having prejudices.

In fact, this time I was going to try each crate in the list, and when the FLTK bindigs turn came, I was fully blown away by this crate as soon as I gave it a try: I fell in love with it! Somehow the author managed to take away the complexity of writing a FLTK interface, and made it Rust-y (read it with a sex-y voice please!). I mean, it feels as if it was only Rust, no bindings whatsoever involved. It clearly does not feel as a wrapper, around a FFI wrapper, around a C library (too many chinese boxes I reckon), as many crates that bind C libraries (try one of the QT crates and then come beck here imploring!).
Instead, it feels like true Rust native. In order to do so, sometimes it wraps around widgets in a different way than the underlying FLTK libs. But this is really nice, I found out.

Like Druid, FLTK employs the constructor pattern a lot.

As first thing, let's check (and in case, fix), FLTK's dependencies:

sudo apt-get install libx11-dev libxext-dev libxft-dev libxinerama-dev libxcursor-dev libxrender-dev libxfixes-dev libpango1.0-dev libgl1-mesa-dev libglu1-mesa-dev
Enter fullscreen mode Exit fullscreen mode

Then, we can start a new project and declare FLTK dependency:

cargo new rust-ui-fltk
cd rust-ui-fltk
cargo add fltk
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the src/main.rs:

use fltk::{app::*, button::*, frame::*, window::*};

fn main() {
    let app = App::default();
    let mut wind = Window::new(100, 100, 400, 300, "Hello, FLTK!");
    let mut frame = Frame::new(0, 0, 400, 200, "Boring label");
    let mut but = Button::new(160, 210, 80, 40, "Click me!");

    wind.end();
    wind.show();

    // Remember: Callbacks after initializing the interface
    but.set_callback(move || frame.set_label("Hello, World!"));

    app.run().unwrap();
}
Enter fullscreen mode Exit fullscreen mode

We set the App with it's defaults, then in order to start the Window, we specify X,Y position and width,height dimensions, plus the title; then we create a Frame (equivalent of FLTK Box), again with position and dimensions, plus a label; finally a button (pos., dim., label as well).

We did not employ the constructor pattern here, but we could have done the following with the button, for example:

let mut but = Button::default()
    .with_pos(160, 210)
    .with_size(80, 40)
    .with_label("Click me!");
Enter fullscreen mode Exit fullscreen mode

Which is equivalent to the above Button::new(160, 210, 80, 40, "Click me!") initializing method.

Once the UI is done, we finalize the UI (with .end()), then we show the window(with .show()).

Callbacks are to be set once the UI is finalized; so we set a callback to the button (it gets called on the click event), and in its closure we change the label of the frame.

At the end we just run the app.

FLTK sample app

Maybe you didn't realize, but this is also maybe the simplest of the example here present, yet it is fully functional!

This crate is very promising: the author even manged to use it on the Android system.

Example Repo:

Back to Summary

GTK

Official GTK bindings, this should be the go to crate for "serious" projects. That said, please be prepared for a non ergonomic experience. You know, this is a full featured crate, binding to one of the 3 most humongous GUI libraries out there, so it is totally fitting that it has some complexity, as the full control that it brings is much bigger than any "simplistic" other project.

In fact, there are other crates that bind to GTK, and they all try to simplify it, but at the expense of full control. It's just a trade off.

That said, I have to say, the Rust bindings to GTK are much more ergonomic than the C/C++/c#. I think there's something in Rust that makes many implementations much more ergonomic than their C counterpart. Don't expect the treats fltk provides though; let's say it's a gracious compromise.

The project gives also the possibility to use Glade and its XML based interface descriptor.

Time to start off:

cargo new rust-ui-gtk
cd rust-ui-gtk
cargo add gtk --features=v3_16
cargo add gio --features=v2_44
Enter fullscreen mode Exit fullscreen mode

Now, let's write our src/main.rs

use gtk::prelude::*;
use gio::prelude::*;
use gtk::{Application, ApplicationWindow, Box, Button, Label};

fn main() {
    let application = Application::new(
        Some("com.github.rust-ui-rundown.rust-ui-gtk"),
        Default::default(),
    ).expect("failed to initialize GTK application");

    application.connect_activate(|app| {
        let window = ApplicationWindow::new(app);
        window.set_title("Hello, GTK+!");
        window.set_default_size(350, 70);

        let container = Box::new(gtk::Orientation::Vertical, 10);

        let label = Label::new(None);
        let button = Button::with_label("Click me!");

        container.add(&label);
        container.add(&button);
        window.add(&container);

        button.connect_clicked(move |_| {
            &label.set_label("Hello, World!");
        });

        window.show_all();
    });

    application.run(&[]);
}
Enter fullscreen mode Exit fullscreen mode

We create first an Application. We have to pass to it an identifier (an Option wrapped str) and the application's flags. We are passing to it the default ones.

Next we apply the method connect_activate which creates the UI in a closure. In this closure we declare a window for the app (ApplicationWindow), setting title and default dimensions. Then we create a Box container with vertical orientation and spacing.

We set a label, with no text (with None) and a button. We add these to the container; then we add the container itself to the window.

Next, we declare a callback for our button, with a closure that sets the label text, once the button is clicked.

We end the closure of connect_activate() by showing all widgets. Finally, we run the application.

The program works as intended, and at this stage is not even much too complex to create simple GUIs like this.

GTK sample app

Example Repo:

Back to Summary

Iced

A cross-platform, elm inspired framework, Iced is a good small solution. The Elm like architecture has been used in many Rust projects already (think Yew or Seed). Like Druid, Iced is data centered.

The first time around I could not make it run, because I do not have Vulkan (and I made a mess with some tricks trying to install it: I do not have a compatible hardware). Since then, though, Iced has got a new OpenGL back-end through glow; the problem is, it is not yet on the crates.io version, so I needed to add it thorugh the GitHub repo:

cargo new rust-ui-iced
cd rust-ui-iced
cargo add iced --git "https://github.com/hecrj/iced.git" --features glow
Enter fullscreen mode Exit fullscreen mode

And now, let's head to the src/main.rs:

use iced::{Element, Sandbox, Settings, Text};

struct MyApp;

impl Sandbox for MyApp {
    type Message = ();

    fn new() -> Self {
        MyApp
    }

    fn title(&self) -> String {
        String::from("Hello, Iced")
    }

    fn update(&mut self, _message: Self::Message) {}

    fn view(&mut self) -> Element<Self::Message> {
        Text::new("Hello, world!").into()
    }
}

pub fn main() -> iced::Result {
    MyApp::run(Settings::default())
}
Enter fullscreen mode Exit fullscreen mode

This example is a bit simplified. We need to declare a struct that we need to impl as an app. In this case it is a Sandboxed type of app, that is more simplified than a regular app (and with less customizing available).

The app has to impl several functions that will be called during the application cycle:

  • new() to init the app
  • title() to assign its title (it can be dynamic: neat!)
  • update() to check and react to the app messages (given by the Message type, but in this case its an empty object ()). Here we have nothing to react to.
  • view() to render the app graphics part (the UI), in this case a simple text that does nothing but stand still.

We init the App, running it on main() with the default options.

This is how it looks like when run.

Iced sample app

Example Repo:

Back to Summary

IUI

IUI is a cross platform wrapper for libui, which in turns uses native frameworks: the usual GTK+, cocoa, and Win32API.

It is a simple yet very usable framework, at least for small projects. Let's see it with an example.

cargo new rust-ui-iui
cd rust-ui-iui
cargo add iui
Enter fullscreen mode Exit fullscreen mode

In the src/main.rs:

use iui::prelude::*;
use iui::controls::{Label, Button, VerticalBox};
use std::rc::Rc;
use std::cell::RefCell;

struct State {
    label: String,
}

fn main() {
    // Initialize the UI and State
    let ui = UI::init().expect("Couldn't initialize UI library");
    let state = Rc::new(RefCell::new(State { label: "".into() }));

    // Create a window into which controls can be placed
    let mut win = Window::new(&ui, "Hello, IUI!", 200, 100, WindowType::NoMenubar);

    // Create a vertical layout to hold the controls
    let mut vbox = VerticalBox::new(&ui);
    vbox.set_padded(&ui, true);

    // Create a new label.
    let label = Label::new(&ui, "");

    // Create a button and its callback
    let mut button = Button::new(&ui, "Click me!");
    button.on_clicked(&ui, {
        let state = state.clone();
        move |_| {
            state.borrow_mut().label = "Hello, world".to_owned();
        }
    });

    // Adding controls to the box, and box to window
    vbox.append(&ui, label.clone(), LayoutStrategy::Stretchy);
    vbox.append(&ui, button.clone(), LayoutStrategy::Compact);

    win.set_child(&ui, vbox);

    // Show the window
    win.show(&ui);

    // Run the application
    let mut event_loop = ui.event_loop();
    event_loop.on_tick(&ui, {
        let ui = ui.clone();
        let mut ui_label = label.clone();
        move || {
            let state = state.borrow();

            // Update all the labels
            ui_label.set_text(&ui, &format!("{}", state.label));
        }
    });
    event_loop.run(&ui);
}
Enter fullscreen mode Exit fullscreen mode

We first define a struct to hold the global state, then initialize it as a RefCell, to clone safely.

We initialize a window, with some settings, and a vertical box. We then initialize two controls: a label and a button, with its callback. Once clicked, the button changes the global state, in order to update the text label.

We then add the controls to the layout, and the layout to the window. Up to this point it works mostly as any other UI framework

When running the app, there's a little difference: we have to manage the refreshes with the on_tick() callback in the event loop.
It is conceptually similar to the MVC pattern, however, we insert our logic inside the event loop itself. This means we have to centralize all the updating of the app in one function.

This means that in order to decouple the refresh from the cycle we would need to create a function for each component, then call it in sequence in the event loop on_tick.
This is not a problem per se, but it must be considered when architecting the apps.

IUI sample APP

On the general side, I find IUI quite simple, yet usable. It is a pity that its development seems halted. EDIT: Good news! As you can see in the comments, the development on this library has not halted at all, it was just slowed down by the underlying C lib.

Example Repo:

Back to Summary

OrbTK

orbtk is the base of the Redox Operative System widgets. It can be used also on other systems (even Android is on the plate, but the support is not yet completed).
I do not know if it's my configuration, but sometimes it feels slow. Compilation too takes a long time, even for small changes, and it seems it recompiles quite few things every time.

Remember to install xkb and wayland as dependencies:

sudo apt install libxkbcommon-dev libwayland-dev
Enter fullscreen mode Exit fullscreen mode

Let's get down to business.

cargo new rust-ui-orbtk
cd rust-ui-orbtk
cargo add orbtk --git="https://github.com/redox-os/orbtk.git" --branch develop
Enter fullscreen mode Exit fullscreen mode

As for the src/main.rs:

use orbtk::prelude::*;

fn main() {
    orbtk::initialize();

    Application::new()
    .window(|ctx| { 
        Window::new()
            .title("Hello, OrbTk!")
            .position((100.0, 100.0))
            .size(200.0, 100.0)
            .child(
                TextBlock::new()
                    .text("Hello, World!")
                    .v_align("center")
                    .h_align("center")
                    .build(ctx)
            )
            .build(ctx)
    })
    .run();
}
Enter fullscreen mode Exit fullscreen mode

It is a very small example:

  • first we initialize the orbtk
  • then we create a new Application, and we build its window;
  • the window method gets a closure to which the context gets passed on. Inside it we create a Window, again with a builder pattern.
  • for the new window we set some properties, like title, size, and position, and we declare a TextBlock as its child
  • the TextBlock is build with a builder pattern as well, setting the alignment (vertical and horizontal) inside the container, and its text
  • both TextBlock and Window take the context as parameter of the build() method, that init the widget.
  • the Application, which is the top-level "container", and provides the context for the contained widgets, uses a run() method, instead, in order to build and run.

OrbTK sample app

Example Repo:

Back to Summary

QMetaObject

QMetaObject relies on the QT Widgets, and it is written in order to support directly the QML interface language, that is a JSON like declarative language, with some JavaScript like constructs.

Needless to say, QT Widgets must be installed. But first, we need some libraries installed as well (if you do not have them already):

sudo apt install libclang-dev libsqlite3-dev
apt-get install libllvm-11-ocaml-dev libllvm11 llvm-11 llvm-11-dev llvm-11-doc llvm-11-examples llvm-11-runtime
Enter fullscreen mode Exit fullscreen mode

Now we need also QT:

Download the open source versions from the QT Download Page: in the next page scroll down and Download the QT Online Installer.

Once downloaded you have to execute the script:

./qt-unified-linux-x64-4.0.1-online.run
Enter fullscreen mode Exit fullscreen mode

Please install a Desktop setting.

After this, we need to export the paths:

export PATH="~/Qt/5.15.2/gcc_64/bin:$PATH"
export LD_LIBRARY_PATH="~/Qt/5.15.2/gcc_64/lib:$LD_LIBRARY_PATH"
Enter fullscreen mode Exit fullscreen mode

Substitute the 5.15.2 with your actual version.

Now we can pass to the Rust side.

cargo new rust-ui-qmetaobject
cd rust-ui-qmetaobject
cargo add qmetaobject
Enter fullscreen mode Exit fullscreen mode

We can now see an example main.rs adapted from the repo's README (adapted to use standard CStr actually):

use std::ffi::CStr;
use qmetaobject::*;

#[allow(non_snake_case)]
#[derive(Default, QObject)]
struct Greeter {
    base : qt_base_class!(trait QObject),
    name : qt_property!(QString; NOTIFY name_changed),
    name_changed : qt_signal!(),
    compute_greetings : qt_method!(fn compute_greetings(&self, verb : String) -> QString {
        return (verb + " " + &self.name.to_string()).into()
    })
}

fn main() {
    qml_register_type::<Greeter>(
        CStr::from_bytes_with_nul(b"Greeter\0").unwrap(),
        1,
        0,
        CStr::from_bytes_with_nul(b"Greeter\0").unwrap()
    );
    let mut engine = QmlEngine::new();
    engine.load_data(r#"
        import QtQuick 2.6; import QtQuick.Window 2.0;
        import Greeter 1.0
        Window {
            visible: true;
            title: "Hello, QMetaObject";
            Greeter { id: greeter; name: 'World'; }
            Text { anchors.centerIn: parent; text: greeter.compute_greetings('hello'); }            
        }
    "#.into());
    engine.exec();
}
Enter fullscreen mode Exit fullscreen mode

In the above we create a custom class to "implement" a C++ QObject. Of course a Rust struct cannot implement a C++ object, so in order for it to work, QMetaObject needs you to derive QObject, and then specify in the base field the C++ class to inherit from, with the macro qt_base_class!(trait QObject).
In the struct you can then add any custom field, besides a name that is the means by which the class can be render visible to the underlying C++.
In this case we added a compute_greetings which is rendered as a QT Method with the macro qt_method!() which takes a closure.

In main() we need to register our custom struct with qml_register_type which takes a uri, a version major and minor (in this case 1.0 therefore 1,0), and the name of the object (the version is needed in QML). Both uri and name have to be passed as C strings, in this case using std::ffi::CStr and the method from_bytes_with_nul; the null terminating the string (totally FFI C++ style) is rendered with \0.

Now the object is available in QML, and we can refer to it by the selected name and version (Greeter 1.0 in this case).
We can create a QmlEngine and load the QML either as a string or as a file (in our case a Rust literal string with #" ... "#). As you can see we use our Greeter and its method compute_greetings inside the QML.

QMetaObject sample app

Example Repo:

GitHub logo davidedelpapa / rust-ui-qmetaobject

Rust UI Rundown: QMetaObject

Back to Summary

Relm

A framework that aims to simplify GTK and give it a behaviour like Elm. Check gtk-rs requirements/dependencies.

In reality it does not simplify GTK, it just renders it elm-like. In fact, you need to know how to design UIs with gtk in order to use Relm. A nice thing is that Relm can use the Glade interface and descriptor as well. Another good perk is that in the examples it shows clearly how to test the interface, not a thing to underestimate!

cargo new rust-ui-relm
cd rust-ui-relm
cargo add relm relm-derive gtk
Enter fullscreen mode Exit fullscreen mode

Onto src/main.rs now:

use gtk::{ Button, ButtonExt, ContainerExt, Inhibit, Label, LabelExt, Orientation, WidgetExt, Window, WindowType};
use relm::{connect, Relm, Update, Widget};
use relm_derive::Msg;

struct Model {}

#[derive(Msg)]
enum Msg {
    Hello,
    Quit,
}

#[derive(Clone)]
struct Widgets {
    lbl: Label,
    but: Button,
    window: Window,
}

#[allow(dead_code)]
struct Win {
    model: Model,
    widgets: Widgets,
}

impl Update for Win {
    type Model = Model;
    type ModelParam = ();
    type Msg = Msg;

    fn model(_: &Relm<Self>, _: ()) -> Model {
        Model {}
    }

    fn update(&mut self, event: Msg) {
        let label = &self.widgets.lbl;
        match event {
            Msg::Hello => { label.set_text("Hello, World!"); },
            Msg::Quit => gtk::main_quit(),
        }
    }
}

impl Widget for Win {
    type Root = Window;

    fn root(&self) -> Self::Root {
        self.widgets.window.clone()
    }

    fn view(relm: &Relm<Self>, model: Self::Model) -> Self {
        // Create the view using the normal GTK+ method calls.
        let window = Window::new(WindowType::Toplevel);

        let lbl = Label::new(None);
        let but = Button::with_label("Click Me");

        let vbox = gtk::Box::new(Orientation::Vertical, 0);
        vbox.add(&lbl);
        vbox.add(&but);

        window.add(&vbox);
        window.show_all();

        connect!(relm, but, connect_clicked(_), Msg::Hello);
        connect!(relm, window, connect_delete_event(_, _), return (Some(Msg::Quit), Inhibit(false)));

        Win {
            model,
            widgets: Widgets {
                lbl,
                but,
                window,
            },
        }
    }
}

fn main() {
    Win::run(()).expect("Win::run failed");
}
Enter fullscreen mode Exit fullscreen mode

As you can see from the use section, gtk is not really hidden behind Relm with wrappers, but is forefront.

We need four data structures:

  • a struct for the Model, that holds the App's state (none in this case)
  • a enum Msg for the messages (callbacks), for which we derive relm_derive::Msg
  • a struct Widgts to hold the widgets list (we need to allow dead code, because we are not using the Model, as it has no state)
  • a struct Win for the main window (it holds the Model and the widgets)

For Win we impl the Update cycle, setting the model() function to create the model, and the update() function to set the behavior for each message (we match over the two messages, assigning the gtk::main_quit() when the message to Quit is fired, and setting the label's text when the Hello message is fired).

We need to impl also the Widget for our Win, setting the type of the root window (in this case single window gtk::Window). The root() function is needed to init the root window, while the view() function draws the UI, using the gtk widgets and conventions.

fn view() has to return the Widget struct initialized. We start declaring a gtk::WindowType::Toplevel window. Then we declare a label (with text = None) and a button.
We create a container and place the label and button in it, and then we place the container as the root widget of the window.
After showing the window (window.show_all()), we can set the callbacks, using the interesting Relm macro connect!. We connect the quit message of the window to the Msg::Quit, and the click of the button to the Msg::Hello.

After running the app, we have our window showing.

Relm sample app

You can notice that the interface is quite similar to that of the base GTK app.

Example Repo:

Back to Summary

Sciter

Sciter is a C-API commercial library (by Terra Informatica) used to create UIs based on web technologies. It has (amongst many others) a Rust binding, sciter-rs. There is a freeware version that we can use, and it seems that it is possible to use it also in commercial software. The only clause is to include somewhere, maybe in an about section, the following:

This Application (or Component) uses Sciter Engine (http://sciter.com/), copyright Terra Informatica Software, Inc.
Enter fullscreen mode Exit fullscreen mode

The other caveat is that the executable may send diagnostic data to one of Terra Informatica 's servers. They are very clear that it may, and maybe (for what I read) it's just diagnostic (it should not infringe the EU protection laws in matters of personal data...). Anyway, the important thing is that you are aware.

By the way, between September and Octobr 2020 the author has campaigned on Kikstarter in order to raise funds to open-source the project. The campaign ended and the goal was not reached, but the author started to disclose some part of the code replacing the Javascript engine, with a BSD 3-clause license, found on github.com/c-smile/sciter-js-sdk. It seems all very promising.

Let's get down to business...

First we need to download the sdk from the sdk download page and extract it. Let's copy in a folder accessible in your PATH the libsciter-gtk.so found in bin.lnx/x64.

If it's not installed we need to install the GTK development files as well

sudo apt-get install libgtk-3-dev
Enter fullscreen mode Exit fullscreen mode

And now, for the Rust part.

cargo new rust-ui-sciter
cd rust-ui-sciter
cargo add sciter-rs
Enter fullscreen mode Exit fullscreen mode

Now for our src/main.rs:

use std::fs;
use std::path::PathBuf;

use sciter;

fn main() {
    let mut frame = sciter::Window::new();
    let html_file_path = PathBuf::from("./app.html");
    let html_file = fs::canonicalize(&html_file_path).unwrap();

    frame.load_file(html_file.to_str().unwrap());
    frame.run_app();
}
Enter fullscreen mode Exit fullscreen mode

Notes:

  • We create a new Window frame with sciter::Window::new();
  • in order to use the .load_file() we have to pass to it the absolute path of the file .html to load (or a URL of an online resource). That is why we went to some lenghts to get a PathBuf and canonicalize it.
  • At the end we just run the app with run_app()

Let's see the content of app.html (to be put at the root of the project):

<html>
  <head>
    <title>Hello, Sciter</title>

    <script type="text/tiscript">
      $(button#click).on("click", function() {
        $(h1#label).$append("Hello, world");
      });
    </script>
  </head>
  <body>

    <h1 #label></h1>
    <button #click>Click me!</button>

  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

It is easy enough. By the way, we did not explore the interoperability between Rust and the HTML, and the TIscript (the Javascript used in Sciter), but of course it is possible (just a little more complex)

Sciter sample app

Example Repo:

Back to Summary

Tauri

Getting into some heavy bindings (hevier maybe than even Sciter). Tauri lets you give a Desktop GUI to a web based project (HTML, CSS, and Javascript). What is it doing in this list, then? Legitimate question, and in fact I was tempted to kick it out, just like Rust-QT-Binding-Generator. However, there are two considerations in order here:

  1. Tauri bridges the web world to the desktop using Rust, and in fact it allows to use the JavaScript just to give the Graphical side of GUI (exactly as Rust-QT-Binding-Generator uses QML, and this one stayed in the list)
  2. Tauri bridges two technologies, web and desktop, with Rust as the man-in-the-middle (no security theats involved... maybe!). And since Rust has a good permeation inside the web world with WASM, indeed you start to see how Tauri can be beneficial in its own right as a GUI option (Tauri works well also with Yew, and the likes of it, meaning there can be more than one level of Rust integration)

We'll use Tauri in conjunction with Vue, so you have to have a node/npm/yarn environment first.
EDIT: nodejs is not required per se for the project to be ported (it could be just HTML/CSS/JS), but for now it is needed for the CLI, and in future release there's a plan to provide also a pure Rust CLI.

If you do not have already the Vue cli installed:

yarn global add @vue/cli @vue/cli-service-global
Enter fullscreen mode Exit fullscreen mode

Now let's install also the Rust counterpart:

cargo install tauri-bundler
Enter fullscreen mode Exit fullscreen mode

And finally let's create our project (with Vue this time!!!)

vue create rust-ui-tauri
cd rust-ui-tauri
vue add tauri
Enter fullscreen mode Exit fullscreen mode

With vue add tauri we take care of the Rust part of the project. Wee have to answer some questions like app name (you can leave the default), and Window title.

Then we can test everything with:

yarn tauri:serve
Enter fullscreen mode Exit fullscreen mode

We will see the default Vue homepage, inside a desktop window!

VUE in your desktop!

Now let's write some code, shall we?

Vue Side

src/ is taken by Vue, while src-tauri/ is the Rust root.

In src/components/ we add another .vue file besides HelloWorld.vue which is already there. We call it RustButton.vue:

<template>
    <div>
        <h1>{{ msg }}</h1>
        <button @click="call_rust">
            Click Me!
        </button>
    </div>
</template>
<script>
import { promisified } from 'tauri/api/tauri'
export default {
  name: 'RustButton',
  methods: {
    call_rust() {
        promisified({
            cmd: 'getResponse',
            payload: {
                state: 1
            }
        }).then(response => {
            // do something with the Ok() response
            const { message } = response;
            this.msg_set = message;
        }).catch(error => {
            // do something with the Err() string
            console.log(error);
        })
        console.log('Rust invoked with Promisified!');
    }
  },
  data: function () {
    return {
      msg: ''
    }
  },
  computed: {
    msg_set: {
        set: function (newValue) {
        this.msg = newValue
        },
        get: function () {
            return this.msg
        },
    }
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

There could be an optional <style> part, but we do not care for this example.

The <template> contains the rendered part, the <script> the logic.

In the <template> we set an <h1> that will display a variable's value (double brackets as handlebars). We have also a button with the @click property that will cull a custom function.

In the <script> part we import promisified from tauri to use the interface with Rust, and we export a default object with the custom method call_rust() which is called by the button. This method in turn calls a Rust function getResponse with a payload object (it is there just to show the interaction between Rust and JS). This is achieved with promisified which returns a JS promise that we can consume with .then() (setting the msg data to the response of Rust with a custom computed setter). This will be called when the Rust answer comes, only if the answer is a Result::Ok; if it is a Result::Err we can catch the error with .catch().

As you can see this is a one-to-one match of a Rust function that returns a Result<T, E>. T must be serialized, while Tauri takes care of the error with the crate anyhow (for which we will implement a custom error, just because...)

There's also another JS function that can call Rust, invoke, but is more suitable for a one-way communication with Rust.

As for the rest of Vue, we just need to modify src/App.vue to call our custom component:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <br />
    <RustButton msg=""/>
  </div>
</template>

<script>
import RustButton from './components/RustButton.vue'

export default {
  name: 'App',
  components: {
    RustButton
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The <style> part we can leave as is.

In practice in the <template>, instead of calling <HelloWorld> we call our custom component <RustButton> with an empty msg property. We need to import it in the </script>, of course, and export it as a custom component in the default object.

And with this we took care of the Vue side.

Rust Side

The Rust side of the force is very strong, as usual... sorry, I mean: it's easy to set everything since Tauri already did some scaffolding for us.

Inside src-tauri we have the usual Rust crate root, with Cargo.toml, the .lock, even a rustfmt.toml (I set the indentation to 4 spaces instead of the default 2, :-) )
There's also a configuration file for Tauri in json.

What we are interested in is the src/cmd.rs. Here we need to put some custom data structures to interface with JS, namely the Cmd itself (which is an enum), that declares the data for our custom function getResponse that we set in JS, with payload, callback, and error. We have also to declare the struct Response to be sent to JS, and a struct representing the payload, which has to mimick the data we get from JS.

We'll use serde, that is already set as dependency in the Cargo.toml

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct GetResponsePayload {
    pub state: u64,
}

#[derive(Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd {
    GetResponse {
        payload: GetResponsePayload,
        callback: String,
        error: String,
    },
}

#[derive(Serialize)]
pub struct Response<'a> {
    pub message: &'a str,
}
Enter fullscreen mode Exit fullscreen mode

In main.rs we define a custom Error type (that's just boilerplate). We are more interested in the main() function:

fn main() {
    tauri::AppBuilder::new()
        .invoke_handler(|_webview, arg| {
            use cmd::Cmd::*;
            match serde_json::from_str(arg) {
                Err(e) => Err(e.to_string()),
                Ok(command) => {
                    match command {
                        // definitions for your custom commands from Cmd here
                        GetResponse {
                            payload,
                            callback,
                            error,
                        } => tauri::execute_promise(
                            _webview,
                            move || {
                                if payload.state != 0 {
                                    let response = cmd::Response {
                                        message: "Hello, World!",
                                    };
                                    Ok(response)
                                } else {
                                    Err(CommandError::new("State not OK").into())
                                }
                            },
                            callback,
                            error,
                        ),
                    }
                    Ok(())
                }
            }
        })
        .build()
        .run();
}
Enter fullscreen mode Exit fullscreen mode

We build a Tauri App, with the builder pattern, then we use a closure to manage the interface with JS, using invoke_handler(), before build() and run().

Inside the closure we convert the json object handler and match over it: if it is Ok we match the command to see which one we got (there can be more than one invoked by the JS of course).

If it is our GetResponse, we execute the promise, that is, we send back the answer. I put an if guard on the state that we got from JS, just to show the interaction...

Now everything is set, we can proudly run our app:

tauri sample app

Example Repo:

Back to Summary

VGTK

VGTK: built on GTK and gtk-rs, above in the list, inspired by Elm, and its inspired Rust counterparts, with a virtualDOM of sorts, like React, and with a sort of JSX for Rust interfaces... Lots to take in at a first date... Ok let's go in to this blind date, and see if it's a love at first glance or not... or maybe if it will become a steady friendhip, why not?

We need, as for the other GTK based frameworks, a working GTK environment.

cargo new rust-ui-vgtk
cd rust-ui-vgtk
cargo add vgtk --git=https://github.com/bodil/vgtk
Enter fullscreen mode Exit fullscreen mode

If we tried now to build a project from crates.io version, we would have some weird compilation error. The point is, something strange happened with gdk-pixbuf version "0.9.0", which is referenced to by vgtk "0.3.0" in crates.io. Instead, upon compilation from GitHub, those problems disappear. The only thing is that the compilations times are larger... It seems to be something connected to compiling by default also the docs, but I didn't have time to check it out better.

And now, let's take a look at our src/main.rs:

use vgtk::ext::*;
use vgtk::lib::gio::ApplicationFlags;
use vgtk::lib::gtk::*;
use vgtk::{gtk, run, Component, UpdateAction, VNode};

#[derive(Clone, Debug, Default)]
struct Model {
    msg: String,
}

#[derive(Clone, Debug)]
enum Message {
    Exit,
    UpdateLabel,
}

impl Component for Model {
    type Message = Message;
    type Properties = ();

    fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> {
        match msg {
            Message::Exit => {
                vgtk::quit();
                UpdateAction::None
            },
            Message::UpdateLabel => {
                self.msg = "Hello, world!".to_owned();
                UpdateAction::Render
            }
        }
    }

    fn view(&self) -> VNode<Model> {
        gtk! {
            <Application::new_unwrap(Some("com.example.rustuivgtk"), ApplicationFlags::empty())>
                <Window border_width=20 on destroy=|_| Message::Exit>
                    <HeaderBar title="Hello VGTK!" show_close_button=true />
                        <Box spacing=10 orientation=Orientation::Vertical >
                            <Label label=self.msg.clone() />
                            <Button label="Click Me!" on clicked=|_| Message::UpdateLabel />
                        </Box>
                </Window>
            </Application>
        }
    }
}

fn main() {
    std::process::exit(run::<Model>());
}
Enter fullscreen mode Exit fullscreen mode

To be honest, according to me it reminds of a fusion between VUE and Yew..., VUE for the code organization, and Yew (inspired by Elm in this) for the pretty recognizable MVC pattern (which is not a classic MVC, there's something peculiar to this that I can't put my finger on... yet).

As for the code, it is simple enough:

We have to build a model and a message system, as usual in these Elm-inspired frameworks; then we need to implement a model with an update() function to handle the messages, and a view() function to render the interface.

The model derives Default, so there's no need for initialization (which would get much more verbose).
The view() employs the macro gtk! which is like JSX, but for GTK interfaces.

The result, for you:

VGTK sample app

All in all it's not bad, the structure is clean enough, and the handling of the gtk! is neat: it seems really to be in a web based environment, and the limited testing I have done show that the framework is powerful enough. I will definitely go deeper with this framework.

Example Repo:

Back to Summary


Not working at all!

Azul

Azul: there was a broken dependencies. This crates is being developed, slowly so the bug might me fixed any time soon. Still, right now it is very promising, but unusable.

Back to Summary

QT_Widgets

QT_Widgets is a Rust Qt binder from Ritual, a C++ to Rust wrapping project. Not too well documented, it is a series of unsafe wrappers for the various C++ QT libraries.

I tried to make it work in a few ways, I even submitted an issue and a pull request for a bug, but as is the crate is not working. Maybe the situation will change, and I'll inform you soon.

Back to Summary


Other Issues

In fairness I have collected here some other crates that I didn't even try or that I tried but have some other issues:

Don't belong to the list according to me

Maybe I am a little too opinionated, but still:

Rust-QT-Binding-Generator

Rust-QT-Binding-Generator: it creates bindings for Rust and QT so that you can call Rust code from inside a QT interface.

The homepage says "it helps to create a Qt based GUI on top of Rust code". In reality it is not the case that you can use QT from a Rust program, but instead you have to write a QT program (in C++ and/or QML) and import the Rust code.

No way I am considering this as a Rust GUI project. It is useful in its own right, but not as a Rust GUI.

Back to Summary

WebRender

The folks at AreWeGuiYet put in the list also WebRender.

This does not belong to a GUI framework list, sorry: it is very low level! At this point we should add also the bindings for GDK, Cairo, etc...

In this I disagree: it is true you can render a GUI with WebRender (as you could with the bare cairo bindings, why not?), but this does not mean WebRender is a GUI framework.

Back to Summary

Immediate Mode "GUIs"

I have found a lot of immediate mode GUIs, which I purposely took out of the list: immediate mode GUIs belong to the gaming world, not to the Desktop Applications world.

Unless you have done a GUI in Blender's Style, that is useful also outside the realm of Graphics... at this point, let's discuss about it!

Here I present the two most used (and that are present in AreWeGUIYet, but shouldn't):

Conrod

Conrod is tied to the Piston Game Engine. Indeed it is its conception of User Interface.

There's often some dependency issue with XCB, needed by the xcb crate. In that case, you might need some or all of the following:

sudo apt-get install xcb xcb-proto libxcb-xkb-dev x11-xkb-utils libx11-xcb-dev libxcb-xrm-dev libxcb1-dev libxkbcommon-dev  libxcb-keysyms1-dev libxcb-xrm0 libxcb-shape0-dev\
libxcb-util0-dev libxcb-cursor-dev  libxkbcommon-x11-dev libxcb-xfixes0-dev   
Enter fullscreen mode Exit fullscreen mode

Just check what the linker complains about (remember to take off the initial "l"; so if it's "-lxcb-shape: missing", you know you need xcb-shape, found in libxcb-shape0-dev, etc...)

What works: the examples

git clone https://github.com/PistonDevelopers/conrod.git
cd conrod
cargo run --release --example all_winit_glium
Enter fullscreen mode Exit fullscreen mode

The examples are found in conrod/backends/conrod_glium/examples take a look there if you are interested.

What does not work: making a simple UI by yourself.

Now, I could not reproduce any of the examples and modify it: all the examples depend on some other module that is in there, with no explanation.

There's also an unfinished official guide, but I found myself at loss nevertheless.

There is not a single/simple way of understanding what is going on, what we are required to do, and most of all, why.

What can I say? Maybe if you are in the know about Piston you swim easily in Conrod, but otherwise it seems just mumbo-jumbo to me.

Back to Summary

ImGui

imgui is a Rust binding for Dear ImGui... Oh, dear! This is the same blessed way of doing things as Conrod: a lot of boilerplate inside a folder in the examples, that even if you copy it to your own project doesn't work!

Why, I say, why don't they make all the support boilerplate inside of the project? why a stupid folder in the examples? Is it too much to ask for a working minimal example when you are just learning about a new library?

Anyway, the working examples are as follows:

git clone https://github.com/Gekkio/imgui-rs
cd imgui-rs
git submodule update --init --recursive
cd imgui-examples
cargo test
cargo run --example hello_world
cargo run --example test_window
Enter fullscreen mode Exit fullscreen mode

The state of these projects is a pity, but I wonder: there are 70 contributors to this package, and the GitHub repo has 1.1k stars.

Can anybody make a stupid example, a tutorial, something for the rest of us? Please! Searching online gets results dated 2018 at best, on stuff I could not try, or even some 404 errors!!!

I would expect something, even minimal, to grasp at, from a project with 1.1k stars!

Back to Summary

Could Not Try: Core-Foundation

Maybe it's the best tool around in the Mac world, I don't know, but I could not try Core-Foundation, because I do not own a Mac.

Back to Summary

Could Not Try: LittlevGL

This one is different: LittlevGL runs on embedded systems. I could not try it, but I look forward to get a Raspberry o something similar, and play a little with Rust in embedded systems.

Back to Summary

Could Not Try: KAS

I tried with the GTK back-end, which is not working at all, but the main KAS crate employs Vulkan, for which I do not have any compatible hardware...
I could not try it out, sorry.

Currently, KAS's only drawing method is WebGPU which requires DirectX 11/12, Vulkan or Metal. In the future, there may be support for OpenGL and software rendering.

I do not have hardware support for vulkan. I tryed the GTK backend enabling Rust nightly (rustup override set nightly in the project root folder). Sadly it didn't work

Compiling kas-gtk v0.0.2
error[E0635]: unknown feature `const_vec_new`
 --> /home/<*>/.cargo/registry/src/github.com-1ecc6299db9ec823/kas-gtk-0.0.2/src/lib.rs:8:12
  |
8 | #![feature(const_vec_new)]
  |            ^^^^^^^^^^^^^

error: aborting due to previous error
Enter fullscreen mode Exit fullscreen mode

I hope to see improvements on the GTK back-end, so that at least I can try it on my machine (or to see an improvement in my salary in order to start owning better machines... but I do not think that is coming around any time soon).

Back to Summary


Conclusions

I tried over the past three months a lot of different crates. Right before publishing I even made sure they are all working properly.

Some crates do work, some crated do not. In my opinion, those who work should be merit-signalled, those who don't should be helped to get to a minimal usable product.

It is a shame that in the Rust ecosystem the GUI world is so mistreated. Granted, even in other languages the situation is not simple at all, but if Rust wants to take a spot out of the niche, as a mainstream language, curating the GUI state would be largely profitable.

Hope this little work of mine can help you getting your hands dirty with Rust GUI programming.

'Till next time!

💖 💪 🙅 🚩
davidedelpapa
Davide Del Papa

Posted on January 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related