Being the naive person that I am, I tried to do it with 'brew'. What could possibly go wrong?
Okay, gtk+ installed.
Now we need the Haskell bindings to it.
gi-gtk is great, mostly auto-generated and well-maintained.
The problem is due to version incompatibility.
Either an older 'gtk+' or a newer 'haskell-gi' would work.
The system library (GTK+) and the haskell-gi library must be compatible.
Better use a package manager that lets you specify the exact version.
You might think that we could start by creating a window, but GTK+ will swiftly remind you that it's a C library, and greet you with a segfault.
To make it work, we start by creating a Gtk.Application. It handles GTK initialization, application uniqueness, session management, desktop shell integration, and so on.
In our small application, we will use it just for initialization. As input, it takes a string that identifies your application.
Next we attach an activation handler. This code will be run when GTK+ initialization is complete, so, this time, no segfaults.
Then we create a window and, quite importantly, show it – GTK+ widgets are created hidden by default.
We can set the window title via the Gtk.setWindowTitle method.
Now let's create another widget – an input field, called "entry" in GTK+.
We create it with Gtk.entryNew, add it to the window with Gtk.containerAdd, and then make it visible with Gtk.widgetShow
Since we want to create a converter, let's have to input fields rather than one.
Factor out its creation into a helper function 'addEntry', and call it twice.
Unfortunately, this doesn't quite cut it: we still see only one input field,
and also a warning in stderr.
To position the entries within the window, we create a box with vertical orientation.
We also set the space between its elements and its padding to 10, so it doesn't look cluttered.
And then we add the entries to the vertical box, instead of adding them to the window directly.
Here's a problem: we don't want this window to be resizable. Its layout supports no meaningful way of filling additional vertical space. Easy to fix.
Set the width, disable resizing.
Now let's add labels to the input fields. Firstly, we will need to pass the label text to 'addEntry'
Then we will create a box to lay out the entry and the label horizontally, and the label widget.
Use Gtk.containerAdd now: the entry and the label go into the hbox,
and the hbox goes into the outer container (which is vbox in our case).
That gets us the result shown on the left.
With two more lines, we can configure the alignment/expansion and get the result on the right.
Recall that we have a window and a vertical box in it.
With a few more lines, we can add a button there.
Here we have it: two entries, two labels, and a button, laid out using horizontal and vertical boxes.
When the user types into the first field, we want to react to that
and set the value of the second field.
So we connect to the the "editable-changed" signal of the first field.
In the handler, we take the text of the field, and (just for example) reverse it and put into the other field.
To implement the actual application logic instead of using Text.reverse, let's discuss a few helper functions.
– c_to_f and f_to_c do temperature conversions
– parseDouble and renderDouble handle conversions between floating-point numbers and text
– getWeather could make a network request to some weather API, and that could take a long time to complete; but since this is a demo, it just waits 3 seconds and returns a random temperature.
Now let's modify our event handler to use the conversion functions.
Parsing the input as a number gives us a Maybe.
In the Nothing case, we simply don't update the other field.
In the Just case, we do the conversion.
Now, to make the conversion in the opposite direction also possible,
(when the user modifies the second field),
let's factor out this logic into a helper and call it for both fields.
However, this results in a strange behavior.
The user enters a digit, and the system appends ".00" to it.
9 degrees Fahrenheit turns into 8.42
The reason for this is a feedback loop. User input gets converted in one direction,then it gets converted back, and so on, until it reaches a fixed point.
To break the loop, let's not modify an entry if the user is typing into it.
We can check it using Gtk.widgetHasFocus
To handle the button click, you could try calling getWeather right there, in the handler.
But this will make your app unresponsive during this blocking computation.
Better do it in another thread.
If you think you can just insert forkIO to do offload the work to another thread,
GTK+ will swiftly remind you that it's a C library, and crash.
Let's also make the button unclickable while it's getting the weather for us,
so that the user doesn't click it multiple times in a row.
And this basically gives us an app with the desired user experience.
The work is offloaded to another thread, but the UI remains responsive.
There's one minor bug though, finding it is left as an exercise for the reader (or should I say 'viewer').
There's a repository available with the application from this talk.
The commit history roughly follows the slides.
There's a comprehensive references for the C library.
haskell-gi is mostly auto-generated, so you can expect to find many of the same functions.
You can find buildable examples of varying complexity in the haskell-gi repository
And of course there's Haddock documentation for the bindings, where the index is of particular interest.