Lesson 4: The widget hierarchy in FLTK

The previous lesson featured Chris’s clock built with the gtk3 package and discussed how to fetch the time and display it. This lesson, we switch back to Julie’s fltkhs clock from the first lesson. At the end of lesson 1, we had a working clock app, and our clock was pleasingly centered inside a window. But the window was pretty bare-bones. For example, the title bar only had buttons to minimize or close the window; the window wasn’t resizable. That’s what we’ll change today.

Also, we found it almost too easy to set up a clock, since FLTK has clock widgets built in that automatically get the system time for us. So, in the next lesson we will use some of what Chris taught us in lesson 3 to set our clock to use a time zone other than our system time. This week’s lesson will be a bit shorter than the next one, and it may seem a little trivial – just resizing a window! – but much of this lesson is about navigating the FLTK and fltkhs documentation and about the ways that fltkhs mimics the FLTK hierarchy.

The fltkhs hierarchy

Our ultimate goal in this lesson is to make the window resizable. It seems safe to assume that FLTK has some notions of resizing windows built into it, so we’ll start by looking in their documentationFLTK 1.4.0 Programming Manual for the appropriate functions, then we’ll find their analogs in the fltkhs package.

A bit of search-engine-ing leads us to the documentation for the resizable() function.FLTK documentation for resizable().

We’re going to assume that resizing works, by default, basically how we expect it to. Although exactly what you expect can depend on which OS, display manager, and window manager you use, we expect certain default behaviors. We expect that it will give us a resizing button in the title bar of the window, that clicking on that will make the window get bigger, and that double-clicking in the title bar will resize the window to full screen.

This documentation tells us that resizing is a behavior assigned to a Group.A group in FLTK is a widget that displays one or more other widgets within it. Windows inherit from Groups, so we should be able to use this to resize our window. This is important because that’s our cue for where we’ll find our relevant functions in fltkhs. The design of fltkhs is intended to rely on the FLTK documentation, so its organization and naming conventions are as similar as possible to the original – no easy feat considering the differences between C++ and Haskell!

We have two things right now: a window and a clock. Let’s look at their hierarchies in FLTK.

Both of those inherit from Widget, but Clock does not inherit from Group. That makes sense, because a clock isn’t a group; it doesn’t contain other widgets.

Navigating to the LowLevel.GroupList of available group functions in Graphics.UI.FLTK.LowLevel.Group. module of the fltkhs package, we find the list of Group functions. This list of functions has the useful type signatures for us. Interestingly, they are written to reflect the hierarchy of widgets from FLTK.In a future lesson, we may want to dive into the source and see how the author implemented some of this, but this for now, while writing this program, we mostly want to reference the useful types. The package author has written an API Guide that explains some of this and we recommend reading it. We say these are “the useful type signatures” because there is a whole lot of type weirdness going on under the surface to make this “inheritance” and the overloaded nature of some of this C++ work out in Haskell. So, the “real” type signatures of all these functions are generally ugly and difficult to understand. They are, however, available for reference, although often in a different module. For instance, you can find the “real” type of setResizable in the LowLevel.Hierarchy module documentation.

In this list, we find some functions called resize and resizeSuper. Both of those take Rectangle arguments, so they are analogous to the resize() functions in FLTK; these functions can set a size to resize to, but they don’t actually make the window or widget resizable in response to clicks, which is what we’re looking for. They just change the size of the thing they’re applied to and whatever other widgets in the group might be affected by that size change. So that’s not quite what we’re looking for. It seems to be setResizable that is analogous to FLTK’s resizable().

setResizable ::
    (Parent a Widget) => Ref Group -> ...

Group inherits from Widget and Window in turn inherits from Group. A DoubleWindow inherits from Window. That means that even though our window is of type Ref DoubleWindow, we can still pass it as the first argument to this function that needs a Ref Group. This was not obvious to me at first because, as a Haskeller, I am not used to passing things of the apparently wrong type to functions and having them just typecheck like everything is cool. But, here, it’s cool. This is wild.

It’s going to take our window as its first argument. And let’s look for a moment at that second argument before we add it to our ui function. .

setResizable ::
    (Parent a Widget) => Ref Group ->
        Maybe (Ref a) -> IO ()

It takes a Maybe (Ref a) as its second argument. We aren’t sure what that should be, so we’ll switch back to reading the FLTK documentation:

The resizable widget defines the resizing box for the group.

When the group is resized it calculates a new size and position for all of its children. Widgets that are horizontally or vertically inside the dimensions of the box are scaled to the new size.

We are going to hazard a hypothesis that passing a Nothing value there means that it doesn’t resize the child widgets, because the FLTK docs say:

If the resizable is NULL then all widgets remain a fixed size and distance from the top-left corner.

And it’s a common choice to translate NULL in C to Nothing in Haskell. Let’s try that out!

ui :: (?assets :: Assets) => IO ()
ui = do
 window <- doubleWindowNew
           (Size (Width 300) (Height 300))
           Nothing
           Nothing
 begin window
 clock <- clockNew
        (Rectangle (Position (X 50) (Y 50))
        (Size (Width 200) (Height 200)))
        (Just "Atlanta time")
 setLabelfont clock josefinSlabSemiBold
 setLabelsize clock (FontSize 24)
 setResizable window Nothing
 end window
 showWidget window

Oooh, that gives us a nasty-looking type error, that we need not repeat here. It’s complaining that the a is ambiguous. This highlights a difference between the type systems of C++ and Haskell: In C++, we could pass a value of NULL here, and NULL inhabits the type Fl_Widget *, so everything’s fine; we don’t have to care what specific type of widget we’re not providing. The Haskell function, however, has a type parameter, and all type parameters must be specified. For every expression we write, we have to be able to know exactly what its type is – we must know whether our Nothing is a Maybe (Ref Clock), a Maybe (Ref Widget), etc. – even if it doesn’t end up mattering. OK, so we can add a type annotation to the Nothing, like this:

 setResizable window (Nothing :: Maybe (Ref Widget))

It doesn’t matter what type we specify here, as long as it is Widget or one of its subtypes in the class hierarchy.

And, indeed, that typechecks. Now you should have a resizable window – note the appearance of the “maximize” button in the title bar – that is responsive to double-clicks in the title bar. But you should also notice that the window resizes but the clock does not.

So, let’s make that right. You can do it now, but the answer will appear in the body of the ui function the next time we show that.

Personally, we don’t particularly want the clock to resize to full screen, especially since it distorts the clock so badly, although that is the default and expected behavior of double-clicking the title bar. We’re going to try next to set a maximum size the window can expand to and also see if we can force the clock to maintain its aspect ratio to the window as it resizes.

Size range

Again, we start with some searching and find a promising name in the FLTK docs: size_range in the Window class of functions.FLTK documentation for size_range. The FLTK documentation for this function says it

Sets the allowable range the user can resize this window to. This only works for top-level windows.

And that’s exactly what we’re looking for. It also gives an overview of the different possible arguments to it, which will keep around for reference while writing our Haskell version. Let’s go now to the fltkhs documentation for the Window class of functions.FLTK.LowLevel.Window list of functions.

This package offers us both sizeRange, which takes a Window and a Size, and sizeRangeWithArgs which allows us to also pass in some OptionalSizeRangeArgs since that is the one that seems to mimic the FLTK original most closely. If you’re following along in your own browser, you can click directly on the OptionalSizeRangeArgs in that type and it will take you to its documentation. It’s the constructor that we’re interested in.

Constructors

OptionalSizeRangeArgs

maxw :: Maybe Int
maxh :: Maybe Int
dw :: Maybe Int
dh :: Maybe Int
aspect :: Maybe Bool

You might notice that in contrast to the FLTK function, this does not have constructors for the minimum width and height; those minimums are set by the Size argument instead. So we have maximum width and height, some size increment settings that we are going to ignore, and then an aspect constructor that may let us maintain the right aspect ratio so our clock doesn’t look stretched and funky when we expand the window. We say may because the FLTK docs seem a little negative on this:

This only works if both the maximum and minimum have the same aspect ratio (ignored on Windows and by many X window managers).

But we shall persist in our optimism.

To sizeRangeWithArgs, then, we need to pass

  • our window;
  • its minimum size, as a Size value; and
  • a value of type OptionalSizeRangeArgs which will include
    • a maximum width,
    • a maximum height,
    • no size increments,
    • and True for the aspect setting.

Putting it all together looks like this.

ui :: (?assets :: Assets) => IO ()
ui = do
 window <- doubleWindowNew
           (Size (Width 300) (Height 300))
           Nothing
           Nothing
 begin window
 clock <- clockNew
        (Rectangle (Position (X 50) (Y 50))
        (Size (Width 200) (Height 200)))
        (Just "theme clock")
 setLabelfont clock josefinSlabSemiBold
 setLabelsize clock (FontSize 24)
 setResizable window (Just clock)
 sizeRangeWithArgs window (Size (Width 200) (Height 200))
   (OptionalSizeRangeArgs (Just 400) (Just 400)
       Nothing Nothing (Just True))
 end window
 showWidget window

This should only allow the window to double its height and width. Let’s load this into the REPL and run main to try it out.

When we double-click on the title bar or use the provided “maximize” button, we find it still goes to fullscreen and the clock gets all stretched and wonky. However, if we use the resizing arrow that appears at the sides or bottom of the window when we move the mouse to that edge to manually resize the window, we find that it will not (depending on your window manager and all that) get bigger than our maximum size, it resizes proportionately, and it keeps its children in the correct aspect ratio. So … it sort of works. And we’re going to leave that as “good enough” for now.

Coming up

So far, this package is difficult for me (Julie) because Haskell is my first language. The fltkhs author does warn that you should not expect to write really idiomatic Haskell when using this package, and I do believe that’s true; this feels a lot different to me than any other Haskell I’ve written. If you are already familiar with FLTK, then I think this would be a very easy package to use; if you are familiar with C++ and/or any other GUI programming toolkit, then you may already at least be familiar with the relevant concepts and find this package a handy tool to add to your collection.

The next lesson will continue with fltkhs. We’re will be integrating some lessons from setting the time of the gtk3 clock into this clock app so that we can display times from other time zones, instead of relying on the default behavior of the fetching the system time.

Sign up for access to the full page, plus the complete archive and all the latest content.