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
A bit of search-engine-ing leads us to the documentation for the
resizable() function.FLTK documentation for
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
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
setResizable :: Parent a Widget) => Ref Group -> ... (
Group inherits from
Window in turn inherits from
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:
Nothing :: Maybe (Ref Widget)) setResizable window (
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.
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
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.
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.
sizeRangeWithArgs, then, we need to pass
- our window;
- its minimum size, as a
- a value of type
OptionalSizeRangeArgswhich will include
- a maximum width,
- a maximum height,
- no size increments,
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.
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.