Golang GUI libraries
By Sean E. Russell on on Permalink.
So, interesting story about toolkits.
I mostly do terminal stuff, because that’s where I mostly live on the desktop. GUIs are fine for some stuff, like image manipulation (gimp, Inkscape), but I mostly don’t do much of that sort of stuff. However, mobile phones are all graphical, at least for me, because screen keyboards a barely sufficient work- around for any other kind of interface.
So a while ago I was using Fyne because it was a pretty decent GUI toolkit and it had great cross-platform support, including for Android. Except then the project got a bad case of Poetteringitis and started kitchen-sinking everything, and it got bloated and unpleasant to work with.
Chelydra using gioui
I next switched to gioui, which also has decent cross-
platform support (including Android), and I fell in love with immediate mode
GUIs. They make intuitive sense once you overcome any preconceived concern
about recreating a bunch of widgets every redraw. In practice, gioui is
blindingly fast and uses almost no CPU, which is great for mobile devices. On
the downside, the gioui interface is confusing and unintuitive, at least to me,
with weird mixtures function signatures that aren’t immediately obvious: you
have Dimensions, which are functions, and Widgets, which aren’t, and Material
things, and they all go together in what I’m sure is a logical yet still
byzantine way. I utterly depend on copy/paste from older programs to build
anything in gioui, which I really don’t like. C&P is just fine as a process, but
it shouldn’t be necessary, and for gioui for me it is. Another big issue with
gioui is that the libraries it depends on don’t cross compile architectures
cleanly, at least not without a large amount of work. I was stuck with having to
build assets in a container; this meant I couldn’t built assets with CI, and it
took forever, and it was hugely annoying.
Chelydra using go-shirei
Fast forward a year, and I’d come across a project called
go-shirei. It’s another intermediate-mode
library with a much more intuitive API than gioui, which is fussy at best. It’s
a young project and so any evaluation is prone to becoming obsolete, but at
first blush it’s simultaneously quite nice but has some issues. First of all,
the API is very well designed; it’s a pleasure to work with and is proof
immediate-mode doesn’t have to be confusing (which might be your impression if
you started with gioui). It also cross-compiles architectures with no issue, just
setting
GOARCH, which bas a huge relief. On the downside, whereas gioui
secretly sips CPU cycles and never appears in top, go-shirei crunches on CPU
while idle, and while it’s not high usage, every extra cycle burned on a
mobile device eats into the battery life. According to the
author @hasenj,
This may or may not be a defect. Think of it as a tradeoff. We take advantage of the fact that computers are fast enough that we can afford to do that.
Which is fine, except that burning CPU cycles does matter, in the aggregate,
but even if you don’t agree with the “if everyone did it” theory, it matters for
mobile devices, which is my sole target for GUI programs. I agree with hasenj:
whether it’s a defect is debatable, and absolutely it may not be a concern for
many people or the project. But it does factor into whether or not I’m going
to use shirei for my mobile projects. Ultimately, I was having trouble aligning
widgets and while @hasenj was
answering my question I came across nucular.
Chelydra using nucular
Chelydra
One of the issues I’ve been encountering on my Linux phone is that there aren’t the wealth of apps Android has. There are even more available for Linux, but only a percentage of those are GUI apps; and of those, a great many were written for landscape desktops with lots of pixels and a mouse, and are mostly unusable on a phone; and of the ones that are suitable for phones, some just don’t compile on arm64. So I’ve got all of these little projects waiting, one-offs, to provide a tool which isn’t available. Scratching the itch.
One of these projects was a way to start and stop snapclient. Usually, snapclient would be a background daemon, but I only want it running when I’m at home and even then, only sometimes when I’m somewhere in the house I haven’t put speakers yet. I named this Chelydra, after snapping turtles.
So I looked at gioui and felt the weight of the prospect of slogging through the boilerplate, and glanced around a bit to see if there were any newer alternatives, and found shirei. The first version of Chelydra was in shirei, and I encountered some issues (again, mostly related to how new the library is – there’s lots of time and room for improvement!), and since Chelydra is such a tiny little utility, and thought I’d give gioui a shot after all. Version 2 used gioui. To my surprise, I had more trouble with gioui; cross-compiling to arm64 was painfully slow, I couldn’t improve the layout as much as I wanted, I was missing some widgets, and most disturbingly, the app was crashing on Wayland on start.
I went digging around to see if I could find a drop-down menu widget for gioui that I could steal without importing an entire extra library, and somehow ended up on nucular. While I’d seen nucular before, for various reasons hadn’t gotten invested in it. I believe it doesn’t compile for Android, and when I was first messing around with Go+mobile development, I had an Android phone so nucular was out of the running for me. In any case, I tried it again, and to my surprise it ticked all of the boxes: the API is decent, it cross-compiles architectures, and it has a rich widget library and full-spectrum layouts. nucular is “mostly- immediate-mode”, and I’m not thrilled with the API; the pollution of non- immediate-mode widgets is off-putting and makes the API more complex than it needs to be. It’s far better POLS-wise than gioui, but not nearly as nice as go- shirei.
Evaluation
Here’s my subjective summary table. Note that some of these (e.g. “Low CPU”) are not generally impactful. What’s the difference between <1% and 2%? Not much, and 2% isn’t exactly “high CPU”. I haven’t really discussed the “Library vs Platform”, but developers to whom it matters will know what I’m talking about: Fyne is the only one of these where you can’t easily factor out business logic from UI logic, because Fyne touches every part of a program: storage, data structures, everything. There are advantages to platforms, so if that’s your bag Fynes a good choice.
| Library | GOARCH |
Widgets set | Layouts set | Android | Low CPU | ¬Platform |
|---|---|---|---|---|---|---|
| Fyne | ✓ | ✓ | ✓ | ✓ | ✓ | X |
| gioui | X | X | X | ✓ | ✓ | ✓ |
| go-shirei | ✓ | X | X | ✓ | X | ✓ |
| nucular | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Measurements
These were run on a Furilabs FLX1s, so the binaries are stripped Linux arm64; the build command was:
GOARCH=arm64 go build -ldflags "-X main.Version=$VERSION -w -s" .
The test program was chelydra, and the versions are pretty close in functionality. There are some differences, like how nucular and go-shirei versions have drop-downs for some widgets but gioui just has text boxes, and that’s because gioui doesn’t have drop-down selectors, and I didn’t put the effort into implementing them. Memory use was about the same for programs whether I was fidgeting with them or not, so I took the worst for each program.
| Library | Active CPU | Idle CPU | Memory | VSZ | RSS | Binary |
|---|---|---|---|---|---|---|
| gioui | 4.30 % | 0.00 % | 1.97% | 2,210,876 | 154,704 | 6,867,456 |
| go-shirei | 23.70 % | 0.68 % | 0.49% | 1,691,380 | 38,112 | 7,340,297 |
| nucular | 1.90 % | 0.45 % | 0.17% | 1,264,368 | 13,652 | 4,063,397 |
The “Active” stats were gathered with pidstat -p $(pidof chelydra) -ur 10 1 with me randomly
messing with the UI – focusing fields, typing text – and “Idle” stats with
pidstat -p $(pidof chelydra) 60 1 and me focusing away from the app (back to
Phosh) and blanking the screen. I was ssh’d into the phone and both ran the
programs and pidstat from there, so there was no other app switching going on,
and no other GUI apps or processes (besides Phosh, pidtsat, and *waves hand*
Linux) were running at the time of measurement.
gioui had the lowest idle CPU, but in all other metrics nucular beat the other toolkits. It’s a surprising result, given how comparatively rich the toolkit is. go-shirei did pretty well on the memory.
You can see the various versions with these links:
The go-shirei version is the first, and it was before I added all the
redo
build scripts, so it looks like there’s more in the other versions. There isn’t;
if you just look at the .go files, according to scc:
| Language | Files | Lines | Code | Complexity |
|---|---|---|---|---|
| gioui | 1 | 478 | 429 | 76 |
| shirei | 1 | 259 | 218 | 58 |
| nucular | 1 | 280 | 243 | 64 |
You can see the boilerplate in gioui; although the code complexity score doesn’t look outrageous, the number of lines of code is double for gioui than for the other libraries. This is a little unfair to nucular, because in the measured version I added a status output widget at the bottom and code to report errors in the GUI. Without that, nucular would be about on par with go-shirei.
Final comment
Honestly, I low-key like gioui the most: despite it being the most painful to actually code, it’s pure. Everything in gioui is immediate mode, and it’s shockingly fast and really doesn’t do anything when idle… there are no wasted CPU cycles. I have written a couple of GUIs apps with it, one of them containing a list with a few hundred items, and it scrolls faster than you can see with no lag or stutter. I haven’t tried the same test with the other toolkits, but gioui feels smoother and more responsive than any app I’ve seen on Android or FuriOS. I did have a common issue with the toolkit panic’ing under Wayland, so it doesn’t fill me with confidence. However, in the future I’ll probably reach for nucular simply because it’s easier to write and has a rich set of widgets and layouts. It looks pretty OK, and that memory usage is crazy – and it uses less CPU than gioui when active, and not that much more when idle. Pretty impressive.