Cross-platform GUIs and Nim macros
23rd March 2017 - WxWidgets , Graphical User Interfaces , Nim , Programming
A while ago I read John Novaks great rant on how hard and annoying it can be to do something as simple as extending a cross-platform application with the simplest of GUIs. Or as he puts it:
You must not under any circumstance try to open a window (on the computer, I mean), attempt to change the colour of a single pixel in it, or—god forbid!—fantasise about using native (or any kind of, for the matter) GUI controls in a cross-platform and non-hair loss inducing manner!
In his rant he uses a fairly simple real-life example for an application he's created and shows how all of the existing solutions for creating simple cross-platform user interfaces in Nim fails his expectations. And I have to agree, I've written a couple of simple programs and GUI is by far the most painful part to do cross platform. In his post he tried a couple different approaches but never really landed on any option which fulfilled all his criteria. IUP wasn't truly cross-platform since it lacks OSX support. Gtk works fine on Linux since most distributions has it installed. However on Windows it not only looks non-native, but the dependency is also quite large and annoying for users to instal since Windows lacks a proper package manager. While static linking is possible with his simple program this adds 20MB of bulk to his program. He then goes off trying to create his own GUI based off SVG rendering. This also fails because his approach of drawing with OpenGL would require him to either include a large font layouting engine or be stuck with sub-optimal looking fonts. Apart from the fact that this would leave you with a solution you would have to create and maintain yourself it also means that it looks non-native.
I found his post while searching for a solution to creating graphical user interfaces in Nim myself, so necessarily I was a bit concerned. Could it really be this bad? I headed over to the comments in hopes that someone would have put him straight and linked to a proper solution. One individual linked to something which seemed interesting, namely LibUI and Nims "official" bindings for it simply named UI. If Araq (the creator, and main contributor, to Nim) has deemed it good enough to simply be called UI it has to be good? Right?
Enter LibUI
The idea behind this library is to create a small but flexible "common ground" for user interfaces and then implement these common elements on each supported platform. This means that whichever platform you are on it will use your native graphical toolkit which is great since you will get both a native look, and good performance. Using LibUI in Nim is pretty straight forward, the bindings works like you would expect and looks pretty much like any other graphical toolkit you would find. So case closed right? Well, not so fast. It looks like any other graphical toolkit you would find, however the code from graphical toolkits normally looks horrible!
Exit LibUI
Don't get me wrong, LibUI has great promise. But it's not ready for any larger projects just yet. One problem seems to be that the focus of development have been around creating UIs, but not actually using them. I found that even the simplest of customizations was impossible and that some of the widgets outright didn't work (some are also disabled in the Nim wrapper as there is no way to get data out of them). However if all you need is a couple buttons and some text then it's a great alternative, and super light weight. One thing which annoys me slightly about it though is that the same capabilities could be implemented in Nim itself. There already exists bindings for the toolkits used by LibUI for Nim so converting the code base shouldn't even be that hard of a job. In the future I definitely hope this is the direction that the Nim community will lean when creating an "official" GUI toolkit. But that's a discussion for another day.
Enter wxWidgets
I'm not sure why wxWidgets isn't more talked about when these topics come up. It was created in 1992 and uses the platforms native toolkits to create a GUI. This is the same approach as LibUI but wxWidgets is a lot more mature and has bindings for many languages, including Nim. When it comes to backends it can compile to pretty much anything, including pure X11 and legacy Mac, so platform support is pretty far reaching. It does however show it's old age a bit, there are no mobile front-ends (no Android or IOS), and pretty much everything in it is wrapped up in it's own wx class including characters, strings, and even threads. This is to make the library truly cross-platform but is something that Nim and many other languages does by themselves which makes it seem a bit awkward to use. But since LibUI lacks in maturity and wxWidgets offers the same set of features and more it works as a good stand-in until something better might come along.
Nim macros and Domain Specific Languages
As you might know, Nim has a powerful macro system which allows it to rewrite it's AST on compile time. An AST or Abstract Syntax Tree, for those not particularly interested in compiler design, is basically what the compiler converts your code to before actually creating the proper output for it. So in Nim we are able to get the AST for a part of our code, and in Nim code write functions that converts it to something else. This is an extremely powerful tool and allows for the creation of Domain Specific Languages within Nim. One example of this is Nims JSON module which allows the user to append "%*" to any valid JSON object and have it automatically converted into a Nim representation. This means that handing back JSON data from for example a server is as easy as writing the JSON code itself. Another example would be the htmlgen macro in which a Nim-like hierarchical syntax can be used to create HTML. Graphical toolkits are much like HTML and this approach could definitely be extended to wxWidgets or any other toolkit.
A simple GUI DSL
Most GUI toolkits are quite horrible to use. The problem has a couple different aspects to it. First of GUIs are inherently quite adaptable to an OO workflow. This means creating objects for each of your elements and then adding them together somehow. The problem with this is that we need to create variable names for all our elements and to quote Phil Karlton:
There are only two hard things in Computer Science: cache invalidation and naming things.
The second issue is that the structure of graphical user interfaces are often lost when written as code. Many frameworks have gotten around this by creating their own formats such as Gtks XML based format but many still lack this feature. To show what I mean by these two problems let's have a look at some wxWidgets code:
var mainFrame = cnew constructWxFrame(title = "Hello World", parent = nil,
id = wxID_ANY)
var tmp217031 = cnew constructWxPanel(parent = mainFrame, id = wxID_ANY)
var tmp217032 = cnew constructWxBoxsizer(orient = wxHorizontal)
tmp217031.setSizer(tmp217032)
var tmp217033 = cnew constructWxStaticBox(label = "Basic controls", parent = tmp217031,
id = wxID_ANY)
var tmp217034 = cnew constructWxStaticBoxSizer(orient = wxVertical, box = tmp217033)
var tmp217035 = cnew constructWxButton(parent = tmp217033, id = wxID_ANY,
label = "Button")
tmp217034.add(tmp217035, border = 5, flag = wxExpand or wxAll)
var tmp217036 = cnew constructWxCheckBox(parent = tmp217033, id = wxID_ANY,
label = "Checkbox")
tmp217034.add(tmp217036, border = 5, flag = wxExpand or wxAll)
var tmp217037 = cnew constructWxTextCtrl(value = "Entry", parent = tmp217033,
id = wxID_ANY)
tmp217034.add(tmp217037, border = 5, flag = wxExpand or wxAll)
var tmp217038 = cnew constructWxStaticText(parent = tmp217033, id = wxID_ANY,
label = "Label")
tmp217034.add(tmp217038, border = 5, flag = wxExpand or wxAll)
tmp217032.add(tmp217034, proportion = 1, border = 5, flag = wxExpand or wxAll)
var tmp217039 = cnew constructWxPanel(parent = tmp217031, id = wxID_ANY)
var tmp217040 = cnew constructWxBoxsizer(orient = wxVertical)
tmp217039.setSizer(tmp217040)
var tmp217041 = cnew constructWxStaticBox(label = "Numbers", parent = tmp217039,
id = wxID_ANY)
var tmp217042 = cnew constructWxStaticBoxSizer(orient = wxVertical, box = tmp217041)
var spinner = cnew constructWxSpinCtrl(min = 0, max = 100, parent = tmp217041,
id = wxID_ANY)
spinner.bind(wxEVT_SPINCTRL, spinnerCallback)
tmp217042.add(spinner, border = 5, flag = wxExpand or wxAll)
var slider = cnew constructWxSlider(value = 0, minValue = 0, maxValue = 100,
parent = tmp217041, id = wxID_ANY)
slider.bind(wxEVT_SLIDER, sliderCallback)
tmp217042.add(slider, border = 5, flag = wxExpand or wxAll)
var gauge = cnew constructWxGauge(range = 100, parent = tmp217041, id = wxID_ANY)
tmp217042.add(gauge, border = 5, flag = wxExpand or wxAll)
tmp217040.add(tmp217042, border = 5, flag = wxExpand or wxAll)
var tmp217043 = cnew constructWxStaticBox(label = "Lists", parent = tmp217039,
id = wxID_ANY)
var tmp217044 = cnew constructWxStaticBoxSizer(orient = wxVertical, box = tmp217043)
var tmp217045 = cnew constructWxChoice(choices = cbChoices, pos = wxDefaultPosition,
size = wxDefaultSize, parent = tmp217043,
id = wxID_ANY)
tmp217044.add(tmp217045, border = 5, flag = wxExpand or wxAll)
var tmp217046 = cnew constructWxComboBox(choices = cbChoices, parent = tmp217043,
id = wxID_ANY)
tmp217044.add(tmp217046, border = 5, flag = wxExpand or wxAll)
var tmp217047 = cnew constructWxRadioButton(style = wxRB_GROUP, parent = tmp217043,
id = wxID_ANY, label = "RadioButton 1")
tmp217044.add(tmp217047, border = 5, flag = wxExpand or wxAll)
var tmp217048 = cnew constructWxRadioButton(parent = tmp217043, id = wxID_ANY,
label = "RadioButton 2")
tmp217044.add(tmp217048, border = 5, flag = wxExpand or wxAll)
var tmp217049 = cnew constructWxRadioButton(parent = tmp217043, id = wxID_ANY,
label = "RadioButton 3")
tmp217044.add(tmp217049, border = 5, flag = wxExpand or wxAll)
tmp217040.add(tmp217044, border = 5, flag = wxExpand or wxAll)
tmp217032.add(tmp217039, proportion = 2, border = 5, flag = wxExpand or wxAll)
As you might notice almost all the variables in this code are temporary, since again, naming is hard. But looking at this code would you be able to tell what the GUI would look like? I sure can't, and that's a problem. Trying to work with code like this can be pretty confusing and debugging it can be hard. The thing is that there are a couple of distinct patterns here. Many of the objects generated here are just created to serve as the parent for future objects. And the elements that can have children often have sizers which dictate the layout for the children elements. So let's define a pattern that creates this for us. My goal here was to create something which was quite transparent with wxWidgets, that way it would be easier for people who are used to wxWidgets to get started, and new users could use the wxWidgets documentation as a guide.
genui:
mainFrame % Frame(title = "Hello World"):
Panel | Boxsizer(orient = wxHorizontal):
StaticBox(label = "Basic controls")[proportion = 1] | StaticBoxSizer(orient = wxVertical):
Button: "Button"
CheckBox: "Checkbox"
TextCtrl(value = "Entry")
StaticText: "Label"
Panel[proportion = 2] | Boxsizer(orient = wxVertical):
StaticBox(label = "Numbers") | StaticBoxSizer(orient = wxVertical):
spinner % SpinCtrl(min = 0, max = 100) -> (wxEVT_SPINCTRL, spinnerCallback)
slider % Slider(value = 0, minValue = 0, maxValue = 100) -> (wxEVT_SLIDER, sliderCallback)
gauge % Gauge(range = 100)
StaticBox(label = "Lists") | StaticBoxSizer(orient = wxVertical):
Choice(choices = cbChoices, pos = wxDefaultPosition, size = wxDefaultSize)
ComboBox(choices = cbChoices)
RadioButton(style = wxRB_GROUP): "RadioButton 1"
RadioButton: "RadioButton 2"
RadioButton: "RadioButton 3"
Quite a bit easier right? This code creates the same code as above, in fact the above code is the output of this macro. We now have a hierarchical structure which quite clearly shows how the GUI is structured and as you can see we no longer need to name everything. The parent is automatically inferred from the hierarchy, and calls to add the new elements to the sizers are also automatically created. And pretty much the only thing this macro does behind the scenes is to rearrange the code we've written. We also have some special syntax for defining event handlers so along with your callbacks this is all the code your application needs to create and manage the UI. If you look over at the GitHub page for my wxnim fork the macro itself is explained in more detail and it includes examples showing how to have long running threads running in the background and passing events to wxWidgets so the UI doesn't freeze.
Size considerations
Another concern of John Novaks original post was the size of his project. Adding 20MB dependencies is not fun when your entire project is just a couple of kilobytes. And compiling with wxWidgets is going to add a little bit of bulk. Audacity which is a rather large cross-platform project that uses wxWidgets to create it's GUI comes in a zip file for Windows that is about 11MiB or 24MiB unpacked. The reason why I cite the Windows build is that on Windows you would typically static link your files or ship them with the DLLs it needs. On Linux Audacity is normally installed through a package manager with the proper dependencies listed. For example on my machine Audacity has an installed size of 18MiB but requires the wxgtk package which itself is a 29MiB install but is used by all my programs which require wxWidgets. So already Audacity is about the size of the 20MiB Windows dependency Novak mentions for Gtk in his post. But wxWidgets is still not the tiniest library around, as I mentioned it wraps up all sorts of things which makes an application written with it truly cross-platform. When I compile the controlgallery example on the GitHub page it comes down to about 4MiB when statically linked (but otherwise optimized for size). When dynamically linking it only takes about 130KiB. This tells us that any program written in Nim is likely to only need about 4MiB of static dependencies when using wxWidgets, way short of Gtks 20MiB plus you get native looks across platforms. It is also worth noting that this could possibly be further reduced by disabling components in the wxWidgets build that are not used, however I have not tested this.
Summing up
So now we have easy to use, cross-platform, and native looking UIs in Nim! And it isn't even that huge of a dependency. Please note that the wxWidgets bindings in Nim currently aren't 100% complete, but they are perfectly workable for everyday tasks and even some more advanced tasks. Notable mentions that are missing include the possibility to manually draw things to widgets, but it is in the works.