Lesson 2: Drawing Text with gtk3

In this lesson we build a simple digital clock application using GTK+GTK+ and the gtk3 packagegtk3 on Hackage which serves as a Haskell interface over it. Like FLTK which we saw in the previous lesson, GTK+ is also a fully-featured GUI toolkit with an extensive library of widgets to choose from. It doesn’t happen to come with a clock, though.

So we’re going to use a drawing area,GtkDrawingArea about which the GTK documentation has this to say:

The GtkDrawingArea widget is used for creating custom user interface elements. It’s essentially a blank widget; you can draw on it.

First, though, let’s start by writing a program that opens a window. Then we’ll start drawing in it.

A plain window

At a minimum, we need to do these things to get a program that shows a window:

main :: IO ()
main =
    _ <- Gtk.initGUI          -- 1
    window <- Gtk.windowNew   -- 2
    Gtk.widgetShowAll window  -- 3
    Gtk.mainGUI               -- 4
  1. initGUIDocumentation for GTK initialization and the main event loop: Graphics.UI.Gtk.General.General initializes GTK. This has to happen before any other GTK function is used.
  2. windowNewDocumentation for the GTK window widget: Graphics.UI.Gtk.Windows.Window creates the window. This gives us a value of type Window.
  3. Every widgetDocumentation for the GTK widget concept: Graphics.UI.Gtk.Abstract.Widget is either visible or hidden, and they’re initially hidden when you first create them. When you create a new widget, you need to show it before it can actually appear on screen. The widgetShow function tells GTK that we’re not just creating a window for future use that’s invisible for the moment; we do in fact want this window to appear on screen right now.
  4. mainGUI hands control over to GTK+ for the remainder of the program’s execution. This action blocks (and thus the program continues running) until something runs the mainQuit action to stop it (thus finally reaching the end of main and stopping the program).

Behold the fruit of our labor:

Knowing when to quit

I said that mainGUI runs until we stop it with mainQuit. Notice that we haven’t actually used mainQuit anywhere yet, and so we have a program but no way to halt it. Before we go on with the clock drawing, let’s fix that.

When the user closes a window (such as by pressing the “×” in the top-right corner of the title bar in the Gnome window manager), typically the program that launched the window should stop. GTK doesn’t give us that behavior automatically, because it’s not always what we want; sometimes a program might continue running “in the background” after the window is closed. But I want our window to exhibit the standard program-stopping behavior. We’re going to do this by adding a handler for the window’s “delete” event.

The documentation for the deleteEvent signal says:deleteEvent

The deleteEvent signal is emitted if a user requests that a toplevel window is closed.

The way we express “every time event X happens, react by taking action Y” is with the on function, which comes from System.Glib.SignalsSystem.Glib.Signals in the glib package (and is re-exported by Graphics.UI.Gtk):

on :: object
   -> Signal object callback
   -> callback
   -> IO (ConnectId object)

We use this any time we’re adding a handler for some event (or, to use an alternative phrasing, when we want to perform an action in response to some signal).

on returns a unique ConnectId that identifies this particular attachment, which you need to retain if you might need to subsequently use signalDisconnect to what we’re setting up here. Since we are unlikely to need that in this case, in my quitOnWindowClose function I’ve chosen to just discard the ConnectId and return () instead.

quitOnWindowClose :: Gtk.Window -> IO ()
quitOnWindowClose window =
    _connectId <- Gtk.on window Gtk.deleteEvent action
    return ()
    action =
        liftIO Gtk.mainQuit
        return True

The reaction that we take is “stop the application,” using Gtk.mainQuit.Gtk.mainQuit

True or False? – In the GTK documentation,GTK documentation for widget signals we see that the description of the return value for a lot of actions says:If you’re familiar with JavaScript, you may find this difficult to remember because True/False has the opposite meaning: because this is the opposite of how JavaScript events work. A JavaScript handler cancels event propagation by returning false, whereas a GTK+ handler cancels event propagation by returning true.

TRUE to stop other handlers from being invoked for the event. FALSE to propagate the event further.

The corresponding documentation in the Haskell packageHaskell gtk3 documentation for widget signals is somewhat more sparse. This is a good time to suggest a general tip: Whenever you’re using a library that exists mostly to provide foreign bindings for another library (like the Haskell gtk3 package wraps GTK+ which is written in C), anticipate that the wrapper probably doesn’t repeat all of the documentation from the underlying library; this means that for a fuller understanding you’ll probably have to consult the GTK+ Reference ManualGTK+ Reference Manual sometimes in addition to the documentation for the Haskell API.

The Haskell documentation does however replicate this useful bit of information from the GTK+ manual:

The default handler for this signal destroys the window.

Since I do want the window to be destroyed, I’m going to return False from the handler to indicate that I want the default behavior. It doesn’t really matter, because quitting the entire program will result in destroying the window anyway, but we might as well say what we mean.

Getting interrupted

There’s one other situation I want to make sure we handle: When you launch a program from the command line and press ctrl+C, you expect the program to quit. This is another thing that, although it is normal and expected, won’t just happen unless we make it happen.

What’s happening when you kill a program with ctrl+C? The shell is sending a signal to the process – specifically, INT, which stands for interrupt. A signal is a message you’re sending to a program; There are a lot of signals, and each means something different, but most of them are ways of telling the program to stop running. The one called “interrupt” triggered by ctrl+C is the most polite, non-urgent way of saying “please stop”.

This is a POSIX thing, not a GTK+ thing, so we need to go to a different package for this: unix.System.Posix.Signals in the unix package

import qualified System.Posix.Signals as Signals

quitOnInterrupt :: IO ()
quitOnInterrupt =
    _oldHandler <- Signals.installHandler Signals.sigINT quitHandler Nothing
    return ()
    quitHandler :: Signals.Handler
    quitHandler = Signals.Catch (liftIO (Gtk.postGUIAsync Gtk.mainQuit))

The installHandler function from unix expresses much the same idea as the on function from gtk3: “every time event X happens, react by taking action Y.” In this case, the event is “when the process receives an interrupt signal,” and the reaction again is “stop the application” (Gtk.mainQuit).

With these two actions inserted, main now looks like this:

main :: IO ()
main =
    quitOnInterrupt           -- 1
    _ <- Gtk.initGUI
    window <- Gtk.windowNew
    quitOnWindowClose window  -- 2
    Gtk.widgetShow window
  1. quitOnInterrupt is the very first thing that happens in main, so that the program can be able to properly respond to interrupts as soon as possible.
  2. quitOnWindowClose has to happen after we create the window, but it can come before we show the window.

The blank window still opens exactly as it did before, but now it closes upon request like a window ought to.

The main thread

There’s a function I used in the last section that calls for some explanation: postGUIAsync. First let’s back up and discuss how GTK+ uses threads.See thread concepts for some background on how Haskell threads work. We can get this from reading the documentation on the Graphics.UI.Gtk.General.General module.Documentation for the GTK main event loop: Graphics.UI.Gtk.General.General

First, the comment on initGUI gives us this warning (slightly paraphrased here):

In a multi-threaded application, it is your obligation to ensure that all calls to GTK happen in a single OS thread.

This means that once we run initGUI, the thread that ran it is now designated as the only thread that should be doing GTK stuff from then on. But sometimes events originating outside of GTK need to affect the GTK thread, like when our interrupt handler tells GTK to stop running. This will also come up again later when we start actually turning this application into a clock, because we’ll have a thread that’s responsible for updating the time once a second. Fortunately, there is a mechanism for inter-thread communication. The documentation goes on to say:

If you want to make calls to GTK functions from another thread, you will have to use postGUISync or postGUIAsync. These will execute their arguments from the main loop, that is, from the OS thread of GTK, thereby ensuring that any GTK and OS function can be called.

Gtk.postGUISync  :: IO a  -> IO a
Gtk.postGUIAsync :: IO () -> IO ()

This is why we wrote Gtk.postGUIAsync Gtk.mainQuit in the signal handler instead of just Gtk.mainQuit.

Notice that when we used mainQuit in quitOnWindowClose, we did not use a “post” function. That’s because that was a handler for a GTK signal, and so it’s already running in the GUI thread; there’s no inter-thread communication required.

Widget nesting

Now it’s time to start putting interesting things into the window. We’ll use two widgets:

  1. Most of the screen will be filled by a drawing areaDocumentation for the drawing area widget: Graphics.UI.Gtk.Misc.DrawingArea will serve a blank canvas onto which we shall paint a clock;
  2. We’ll put the drawing area inside a frameDocumentation for the frame widget: Graphics.UI.Gtk.Ornaments.Frame, a border that goes around the drawing and has a label.The documentation for GtkFrame includes a sample image demonstrating what a frame looks like.

import Graphics.UI.Gtk (AttrOp ((:=)))
import qualified Graphics.Rendering.Cairo as Cairo
import qualified Graphics.Rendering.Pango as Pango
main :: IO ()
main =
    _ <- Gtk.initGUI

    drawingArea <- Gtk.drawingAreaNew       -- 1
    Gtk.set drawingArea
      [ Gtk.widgetWidthRequest := 300
      , Gtk.widgetHeightRequest := 100
    _ <- Gtk.on drawingArea Gtk.draw render

    frame <- Gtk.frameNew                   -- 2
    Gtk.set frame
      [ Gtk.containerChild := drawingArea
      , Gtk.frameLabel := "What time is it"
      , Gtk.frameLabelXAlign := 0.5
      , Gtk.frameLabelYAlign := 1
      , Gtk.widgetMarginTop := 20
      , Gtk.widgetMarginRight := 20
      , Gtk.widgetMarginBottom := 20
      , Gtk.widgetMarginLeft := 20

    window <- Gtk.windowNew                 -- 3
    Gtk.set window
      [ Gtk.windowTitle := "Clock"
      , Gtk.containerChild := frame
      , Gtk.windowDefaultWidth := 500
      , Gtk.windowDefaultHeight := 250

    quitOnWindowClose window
    Gtk.widgetShowAll window

Each of the three widgets follows the same pattern: There’s a function called [...]New that constructs the widget, and then we use Gtk.set to set a handful of properties on it.Properties that apply to all widgets are documented in Graphics.UI.Gtk.Abstract.Widget. Others are documented in the modules for their respective widgets.

  1. For the DrawingArea, we set its requested width and height. The requested size of a widget is the size that it wants to be. If the widget were nested within a more complex layout with side-by-side widgets, GTK would take this into consideration when deciding what size everything should be. For this simple application, the requested size has the effect of limiting how much the user is able to resize the window. We set a minimum size of 300 × 100 pixels to make sure the drawing area never gets too small to fit the clock.

  2. The Frame widget specifies that it contains the drawing area. Documentation for widgets that contain other widgets (such as Window and Frame): Graphics.UI.Gtk.Abstract.Container Label alignment of 0.5 on the X axis means the text “What time is it” is centered horizontally, and alignment of 1 on the Y axis means the label text appears above the frame’s border. We also gave the frame a margin of 20 pixels on all sides; otherwise it would be directly touching the window edge and it would be difficult to see that it’s there.

  3. The window’s title (“Clock”) is what appears in the title bar (which is drawn by the window manager, not by our application). Just as the frame contains the drawing area, so too does the window contain the frame; or in other words, the frame is the window’s child. We set a default size for the window, which is the size of the window when it first opens.

Drawing some text

There’s one line above that we haven’t mentioned yet:

_ <- Gtk.on drawingArea Gtk.draw render

The draw signalGTK+ documentation for the draw signal indicates that a widget (or some portion of it) needs to be drawn to the screen:

Gtk.draw :: Signal Gtk.DrawingArea (Cairo.Render ())

This type tells us that the handler we need to specify for the draw signal has type Cairo.Render (). Cairo is the graphics library that you use with GTK, and Render is the monad in which you construct Cairo drawings. Whenever you’re trying to understand a new monad, it’s usually useful to look at the source code to see how it’s defined. If we look at the source code in the cairo package, we find this:

newtype Render m =
  Render { runRender :: ReaderT Cairo IO m }

This means that Render has two defining aspects:

  1. ReaderT Cairo – This tells us that all Render actions take place within some context, and you can expect that the library will provide us some actions in the Render monad that obtain information from that context.
  2. IO – This tells us that we can lift IO actions into the render monad. We didn’t have to look at the source code to know this; we also could have noticed by looking at the list of typeclass instances for Render that it has an instance of MonadIO.

For this application, we’re going to have the rendering consist of two steps: First draw a background (fill the area with a solid color), then draw some text on top of the background.

render :: Cairo.Render ()
render =
    renderBackground  -- 1
    renderText        -- 2

The way we draw with Cairo is probably familiar if you’ve used any other graphics libraries, but you might find the imperative style a little unusual if you mostly write Haskell. Drawing consists of a sequence of state mutations. For example, there’s a function to set the drawing color,Cairo documentation for setting the color: cairo_set_source_rgb which will color all subsequent drawing operations until the color is changed again.

renderBackground :: Cairo.Render ()
renderBackground =
    Cairo.setSourceRGB 1 0.9 1

Drawing text requires a whole other library called Pango, Pango documentation: Graphics.Rendering.Pango because rendering text turns out to be a much more complicated problem than it might seem! To draw text, we create what Pango calls a layout – a way in which the characters are arranged, or laid out, in a two-dimensional space.

renderText :: Cairo.Render ()
renderText =
    Cairo.setSourceRGB 0.2 0.1 0.2
    layout <- Pango.createLayout "I don't know yet!"
    Pango.showLayout layout

We now have an application that displays something!

Centering the text

We really want to center the text, both horizontally and vertically, within the drawing window. GTK does not provide us with an easy way to do this, so we have to do some math.

We’re going to replace Pango.showLayout with showPangoCenter, a function that we’re about to write. In order to know where the center of the drawing area is, we need to know the size of the drawing area, so we’re also adding a Gtk.DrawingArea parameter to the function.

renderText :: Gtk.DrawingArea -> Cairo.Render ()
renderText drawingArea =
    Cairo.setSourceRGB 0.2 0.1 0.2
    layout <- Pango.createLayout "I don't know yet!"
    showPangoCenter drawingArea layout

Since we will be doing arithmetic in two dimensions, we’re going to make it a little easier by using the linear package.The linear package contains a lot of math that we don’t know anything about. But we can still make basic use of the V2 type from the Linear.V2 module to represent points on the screen.

import Linear.V2

That provides this type:

data V2 a = V2 a a

You don’t really need to know anything about linear algebra to use this package.We’ll only need the standard arithmetic operations: addition, subtraction, multiplication, division. Think of V2 as a pair of numbers (representing a horizontal distance and a vertical distance), and any arithmetic operations we perform on them happen to both numbers.

λ> V2 4 5 * 2
V2 8 10
λ> V2 4 5 + V2 1 2
V2 5 7

This part took a lot of thinking and some trial-and-error to get right. Here’s the idea:

Before when we just used Pango.showLayout, the layout appeared with its top-left corner in the top-left corner of the drawing area. Now, we’re going to use Cairo.moveTo first, and so the layout will appear with its top-left corner in the position that we’ve moved to.

The movement, depicted in the figure above, is calculated as the distance between the center of the text and the center of the drawing area.

showPangoCenter :: Gtk.DrawingArea -> Gtk.PangoLayout -> Cairo.Render ()
showPangoCenter drawingArea layout =
    clipCenter <- getClipCenter
    textCenter <- getTextCenter
    let (V2 x y) = clipCenter - textCenter
    Cairo.moveTo x y
    Pango.showLayout layout
    getClipSize :: Cairo.Render (V2 Int) =
        Gtk.Rectangle _x _y w h <- liftIO (Gtk.widgetGetClip drawingArea)
        return (V2 w h)

    getClipCenter :: Cairo.Render (V2 Double) =
        size <- getClipSize
        return ((realToFrac <$> size) / 2)

    getTextCenter :: Cairo.Render (V2 Double) =
        (_, Gtk.PangoRectangle x y w h) <-
            liftIO (Pango.layoutGetExtents layout)
        return (V2 x y + (V2 w h / 2))

Now we are looking very well-aligned.

That was a lot of setup! We will conclude this lesson here and turn this application into a clock next time. Now that we have the mechanics in place, getting the time and changing the text shouldn’t be too much trouble.

Join Type Classes for courses and projects to get you started and make you an expert in FP with Haskell.