September 04, 2018
This post introduces a declarative GTK+ architecture for Haskell which I’ve been working on during the journey with FastCut, a video editor specialized for my own screencast editing workflow. It outlines the motivation, introduces the packages and their uses, and highlights parts of the implementation.
When starting to work on FastCut, I wanted to use a GUI framework that would allow FastCut to be portable across Linux, macOS, and Windows. Furthermore, I was looking for a native GUI framework, as opposed to something based on web technology. GTK+ stood out as an established framework, with good bindings for Haskell in the haskell-gi package.
It didn’t take me very long before the imperative APIs of GTK+ became
problematic. Widgets are created, properties are set, callbacks are
attached, and methods are called, all in IO
. With the
imperative style, I see there being two distinct phases of a widget’s
life cycle, which need to be handled separately in your rendering
code:
This programming style makes testing and locally reasoning about your
code really hard, as it forces you to scatter your application logic and
state over IORef
s, MVar
s, and various
callbacks, mutating shared state in IO
. These symptoms are
not specific to GTK+ or its Haskell bindings, but are common in
object-oriented and imperative GUI frameworks. If you’re familiar with
JavaScript and jQuery UI programming, you have most likely experienced
similar problems.
While GTK+ has the Glade
“WYSIWYG” editor, and the Builder
class, they only make the
first phase declarative. You still need to find all widgets in the
instantiated GUI, attach event handlers, and start mutating state, to
handle the second phase.
After having experienced these pains and getting repeatedly stuck when building FastCut with the imperative GTK+ APIs, I started exploring what a declarative GTK+ programming model could look like.
The package I’ve been working on is called gi-gtk-declarative, and it aims to be a minimal declarative layer on top of the GTK+ bindings in haskell-gi.
Rendering becomes a pure function from state to a declarative widget, which is a data structure representation of the user interface. The library uses a patching mechanism to calculate the updates needed to be performed on the actual GTK+ widgets, based on differences in the declarative widgets, similar to a virtual DOM implementation.
Event handling is declarative, i.e. you declare which events are
emitted on particular signals, rather than mutating state in callbacks
or using concurrency primitives to communicate changes. Events of
widgets can be mapped to other data types using the Functor
instance, making widgets reusable and composable.
Finally, gi-gtk-declarative tries to be agnostic of the application architecture it’s used in. It should be possible to reuse it for different styles. As we shall see later there is a state reducer-based architecture available in gi-gtk-declarative-app-simple, and FastCut is using a custom architecture based on indexed monads and the Motor library.
By reusing type-level information provided by the
haskell-gi
framework, and by using the
OverloadedLabels
language extension in GHC,
gi-gtk-declarative can support many widgets automatically. Even though
some widgets need special support, specifically containers, it is a
massive benefit not having to redefine all GTK+ widgets.
A single widget, i.e. one that cannot contain children, is
constructed using the widget
smart constructor, taking a
GTK+ widget constructor and an attribute list:
= widget Button []
myButton
= widget CheckButton [] myCheckButton
Note that Button and CheckButton are constructors from gi-gtk. They are not defined by gi-gtk-declarative.
Bins, in GTK+ lingo, are widgets that contain a single
child. They are created using the bin
smart
constructor:
=
myScrollArea ScrolledWindow [] $
bin Button [] widget
Other examples of bins are Expander, Viewport, and SearchBar.
Containers are widgets that can contain zero or more child widgets,
and they are created using the container
smart
constructor:
=
myListBox ListBox [] $ do
container ListBoxRow [] $ widget Button []
bin ListBoxRow [] $ widget CheckButton [] bin
In regular GTK+, containers often accept any type of widget to be
added to the container, and if the container requires its children to be
of a specific type, it will automatically insert the in-between widget
implicitly. An example of such a container is ListBox
,
which automatically wraps added children in ListBoxRow
bins, if needed.
In gi-gtk-declarative, on the other hand, containers restrict the
type of their children to make these relationships explicit. Thus, as
seen above, to embed child widgets in a ListBox
they have
to be wrapped in ListBoxRow
bins.
Another example, although slightly different, is Box
.
While Box
doesn’t have a specific child widget type, you
can in regular GTK+ add children using the boxPackStart
and
boxPackEnd
methods. The arguments to those methods are
expand, fill, and padding, which control how
the child is rendered (packed) in the box. As
gi-gtk-declarative doesn’t support method calls, there is a helper
function and corresponding declarative widget boxChild
to
control Box
child rendering:
=
myBox Box [] $ do
container False False 0 $ widget Button []
boxChild True True 0 $ widget CheckButton [] boxChild
Note that we are using do
notation to construct adjacent
boxChild
markup values. There is a monadic
MarkupOf
builder in the library that the
container
smart constructor takes as its last argument.
Although we need the Monad
instance to be able to use do
notation, the return value of such expressions are rarely useful, and is
thus constrained to ()
by the library.
All declarative widgets can have attributes, and so far we’ve only seen empty attribute lists. Attributes on declarative widgets are not the same as GTK+ properties. They do include GTK+ properties, but also include CSS classes declarations and event handling.
One type of attribute is a property declaration. To declare
a property, use the (:=)
operator, which takes a property
name label, and a property value, much like (:=)
in haskell-gi:
=
myButtonWithLabel Button [#label := "Click Here"]
widget
=
myHorizontallyScrolledWindow ScrolledWindow [ #hscrollbarPolicy := PolicyTypeAutomatic ] $
bin
someSuperWideWidget
=
myContainerWithMultipleSelection ListBox [ #selectionMode := SelectionModeMultiple ] $
container children
To find out what properties are available, see the GI.Gtk.Objects
module, find the widget module you’re interested in, and see the
“Properties” section. As an example, you’d find the properties available
for Button
here.
Using the on
attribute, you can emit events on GTK+
signal emissions.
=
counterButton clickCount let msg = "I've been clicked "
<> Text.pack (show clickCount)
<> " times."
in widget
Button
#label := msg
[ #clicked ButtonClicked
, on ]
Some events need to be constructed with IO
actions, to
be able to query underlying GTK+ widgets for attributes. The
onM
attribute receives the widget as its first argument,
and returns an IO event
action. In the following example
getColorButtonRgba
has type
ColorButton -> IO (Maybe RGBA)
, and so we compose it
with an fmap
of the ColorChanged
constructor
to get an IO Event
.
data Event = ColorChanged (Maybe RGBA)
=
colorButton color
widgetColorButton
#title := "Selected color"
[ #rgba := color
, #colorSet (fmap ColorChanged . getColorButtonRgba)
, onM ]
You can think of onM
having the following signature,
even if it’s really a simplified version:
onM :: Gtk.SignalProxy widget
-> (widget -> IO event)
-> Attribute widget event
Finally, CSS classes can be declared for widgets in the attributes
list, using the classes
attribute:
=
myAnnoyingButton
widgetButton
"big-button"]
[ classes [#label := "CLICK ME"
, ]
In addition to the declarative widget library gi-gtk-declarative, there’s an application architecture for you to use, based on the state reducer design of Pux.
At the heart of this architecture is the App
:
data App state event =
App
update :: state -> event -> Transition state event
{ view :: state -> Widget event
, inputs :: [Producer event IO ()]
, initialState :: state
, }
The type parameters state
and event
will be
instantiated with our specific types used in our application. For
example, if we were writing a “Snake” clone, our state datatype would
describe the current playing field, the snake length and where it’s
been, the edible objects’ positions, etc. The event datatype would
likely include key press events, such as “arrow down” and “arrow
right”.
The App
datatype consists of:
update
function, that reduces the current state and
a new event to a Transition
, which decides the next state
to transition to,view
function, that renders a state value as a
Widget
, parameterized by the App
s event
type,To run an App
, you can use the convenient
run
function, that initializes GTK+ and sets up a window
for you:
run :: Typeable event
=> Text Window title
-> Maybe (Int32, Int32) -- ^ Optional window size
-> App state event -- ^ Application to run
-> IO ()
There’s also runInWindow
if you like to initialize GTK+
yourself, and set up your own window; something you need to do if you
want to use CSS, for instance.
The haskell-gi README includes an “Hello, world!” example, written in an imperative style:
main :: IO ()
= do
main Nothing
Gtk.init
<- new Gtk.Window [ #title := "Hi there" ]
win
#destroy Gtk.mainQuit
on win
<- new Gtk.Button [ #label := "Click me" ]
button
#clicked $
on button
set
button#sensitive := False
[ #label := "Thanks for clicking me"
,
]
#add win button
#showAll win
Gtk.main
It has two states; either the button has not been clicked yet, in which it shows a “sensitive” button, or the button has been clicked, in which it shows an “insensitive” button and a label thanking the user for clicking.
![]() |
![]() |
Let’s rewrite this application in a declarative style, using gi-gtk-declarative and gi-gtk-declarative-app-simple, and see how that works out! Our state and event datatypes describe what states the application can be in, and what events can be emitted, respectively:
data State = NotClicked | Clicked
data Event = ButtonClicked
Our view function, here defined as view'
, renders a
label according to what state the application is in:
view' :: State -> Widget Event
= \case
view' NotClicked ->
widgetButton
#label := "Click me"
[ #clicked ButtonClicked
, on
]Clicked ->
widgetButton
#sensitive := False
[ #label := "Thanks for clicking me"
, ]
The update
function reduces the current state and an
event to a Transition event state
, which can either be
Transition
or Exit
. Here we always transition
to the Clicked
state if the button has been clicked.
update' :: State -> Event -> Transition State Event
ButtonClicked = Transition Clicked (return Nothing) update' _
Note that the Transition
constructor not only takes the
next state, but also an IO (Maybe Event)
action. This makes
it possible to generate a new event in the update function.
Finally, we run the “Hello, world!” application using
run
.
main :: IO ()
=
main
run"Hi there"
Nothing
App
= view'
{ view = update'
, update = []
, inputs = NotClicked
, initialState }
Comparing with the imperative version, I like this style a lot
better. The rendering code is a pure function, and core application
logic can also be pure functions on data structures, instead of mutation
of shared state. Moreover, the small state machine that was hiding in
the original code is now explicit with the State
sum type
and the update'
function.
There are more examples in gi-gtk-declarative if you want to check them out.
Writing gi-gtk-declarative has been a journey full of insights for me, and I’d like to share some implementation notes that might be interesting and helpful if you want to understand how the library works.
At the core of the library lies the Patchable
type
class. The create
method creates a new GTK+ widget given a
declarative widget
. The patch
method
calculates a minimal patch given two declarative widgets; the old and
the new version:
class Patchable widget where
create :: widget e -> IO Gtk.Widget
patch :: widget e1 -> widget e2 -> Patch
A patch describes a modification of a GTK+ widget, specifies a replacement of a GTK+ widget, or says that the GTK+ widget should be kept as-is.
data Patch
= Modify (Gtk.Widget -> IO ())
| Replace (IO Gtk.Widget)
| Keep
Replacing a widget is necessary if the declarative widget changes
from one type of widget to another, say from Button
to
ListBox
. We can’t modify a Button
to become a
ListBox
in GTK+, so we have to create a new GTK+ widget and
replace the existing one.
Declarative widgets are often wrapped in the Widget
datatype, to support widgets of any type to be used as a child in a
heterogeneous container, and to be able to return any
declarative widget, as we did in the App
view function
previously. The Widget
datatype is a GADT:
data Widget event where
Widget
:: ( Typeable widget
Patchable widget
, Functor widget
, EventSource widget
,
)=> widget event
-> Widget event
If you look at the Widget
constructor’s type signature,
you can see that it hides the inner widget
type, and that
it carries all the constraints needed to write instances for patching
and event handling.
We can define a Patchable
instance for
Widget
as the inner widget is constrained to have an
instance of Patchable
. As the widget
is also
constrained with Typeable
, we can use eqT
to
compare the types of two Widget
s. If their inner
declarative widget types are equal, we can calculate a patch from the
declarative widgets. If not, we replace the old GTK+ widget with a new
one created from the new declarative widget.
instance Patchable Widget where
Widget w) = create w
create (Widget (w1 :: t1 e1)) (Widget (w2 :: t2 e2)) =
patch (case eqT @t1 @t2 of
Just Refl -> patch w1 w2
-> Replace (create w2) _
Similar to the case with Patchable
, as we’ve constrained
the inner widget type in the GADT, we can define instances for
Functor
and EventSource
.
At first, it might seem unintuitive to use dynamic typing in Haskell, but I think this case is very motivating, and it’s central to the implementation of gi-gtk-declarative.
All smart constructors — widget
, bin
, and
container
— can return either a Widget
value,
such that you can use it in a context where the inner widget type needs
to be hidden, or a MarkupOf
with a type specifically needed
in the contexts in which the widget is used, for example, a bin or
container with a requirement on what child widget it can contain.
Here are some possible specializations of smart constructor return types:
Button [] :: Widget MyEvent
widget Button [] :: MarkupOf (SingleWidget Button) MyEvent ()
widget
ScrolledWindow [] _ :: Widget MyEvent
bin ScrolledWindow [] _ :: MarkupOf (Bin ScrolledWindow Widget) MyEvent ()
bin
Box [] _ :: Widget MyEvent
container Box [] _ :: MarkupOf (Container Box (Children BoxChild)) MyEvent () container
As a small example, consider the helper textRow
that
constructs a ListBoxRow
to be contained in a
ListBox
:
myList :: Widget Event
=
myList ListBox [] $
container mapM textRow ["Foo", "Bar", "Baz"]
textRow :: Text -> MarkupOf (Bin ListBoxRow Widget) Event ()
=
textRow t ListBoxRow [] $
bin Label [ #label := t ] widget
As the type signature above shows, the textRow
function
returns a MarkupOf
value parameterized by a specific child
type: Bin ListBoxRow Widget
. You can read the whole type as
“markup containing bins of list box rows, where the list box rows can
contain any type of widget, and where they all emit events of type
Event
.” I know, it’s a mouthful, but as you probably won’t
split your markup-building function up so heavily, and as GHC will be
able to infer these types, it’s not an issue.
The return type polymorphism of the smart constructors should not affect type inference badly. If you find a case where it does, please submit an issue on GitHub.
Callback-centric GUI programming is hard. I prefer using data
structures and pure functions for core application code, and keep it
decoupled from the GUI code by making rendering as simple as a function
State -> Widget
. This is the ideal I’m striving for, and
what motivated the creation of these packages.
I have just released gi-gtk-declarative and gi-gtk-declarative-app-simple on Hackage. They are both to be regarded as experimental packages, but I hope for them to be useful and stable some day. Please try them out, and post issues on the GitHub tracker if you find anything weird, and give me shout if you have any questions.
The gi-gtk-declarative library is used heavily in FastCut, with great success. Unfortunately that project is not open source yet, so I can’t point you to any code examples right now. Hopefully, I’ll have it open-sourced soon, and I’m planning on blogging more about its implementation.
Until then, happy native and declarative GUI hacking!