11 Specifying layout

When combining fudgets for GUI elements, there are two considerations:When developing fudget programs, it is not necessary to be concerned with the actual layout of the GUI fudgets. For example, the fudget
shellF "Buttons"
  (buttonF "A Button" >+< buttonF "Another Button")
will get some default layout which might look like Figure 15.

Figure 15. When no layout is specified in the program, the automatic layout system chooses one.

But sooner or later, we will want to have control over the layout. The GUI library lets us do this two different ways:

Before describing these, we will present the layout combinators that both of them use.

11.1 Boxes, placers and spacers

Layout is done hierarchically. Each GUI fudget will reside in a box, which will have a certain size and position when the layout is complete. A list of boxes can be put inside a single box by a placer. A placer defines how the boxes should be placed in relation to each other inside the larger box. This enclosing box can be subject to further placement, but the enclosed boxes are hidden by the placer in the sense that they cannot be manipulated individually any more. The effects of some placers are illustrated in Figure 16.

Figure 16. Different placers.

The parameter to matrixP specifies the number of columns the matrix should have. The types of the placers are

horizontalP :: Placer
verticalP :: Placer
matrixP :: Int -> Placer
revP :: Placer -> Placer
The placer revP reverses the list of boxes it is applied to. Another higher order placer is flipP, which transforms a placer into a mirror symmetric placer, with respect to the line x = y (that is, it flips the x and y coordinates):
flipP :: Placer -> Placer
Hence, we can define verticalP as
verticalP = flipP horizontalP
Placers can be applied to fudgets by means of placerF:
placerF :: Placer -> F a b -> F a b
It applies the placer to all boxes in the argument fudget. The order of the boxes is left to right, with respect to the combinators listF, >==< and >+<, etc.

As an example, suppose we want to specify that the two buttons in Figure 15 should have vertical layout. We could then write

shellF "Buttons" (placerF verticalP (buttonF "A Button" >+<
                                     buttonF "Another Button"))
The result can be seen in Figure 17. In a similar way, the first button could be placed below, to the right of, or to the left of the second button, by using the placers revP verticalP, horizontalP or revP horizontalP, respectively.

Figure 17. The same GUI elements as in Figure 15, but the program explicitly specifies vertical layout.

Abstract fudgets do not have a corresponding box in the layout. This means that the presence of mapstateF in the definition of counterF in Figure 18, does not leave a hole in the layout of verticalCounterF.

verticalCounterF = placerF verticalP counterF

counterF = intDispF >==< mapstateF count 0 >==<
           (buttonF "Up" >+< buttonF "Down")

Figure 18. An up/down counter with vertical layout. Abstract fudgets do not have a corresponding box in the layout.

What if we want the display to appear between the two buttons? With the placers we have seen, the two buttons will appear together in the layout, since they appear together in the program structure. One solution is to use a placer operator that allows the order of the boxes to be permuted:

permuteP :: [Int] -> Placer -> Placer
We can then replace verticalP with

permuteP [2,1,3] verticalP
to get the display in the middle. This kind of solution works, but it will soon become quite complicated to write and understand. A more general solution is to use name layout (Section 11.2).

Placers are used to specify the layout of a group of boxes. In contrast, spacers are used to wrap a box around a single box. Spacers can be used to determine how a box should be aligned if it is given too much space, or to add extra space around a box. Examples of spacers that deal with alignment can be seen in Figure 19.

Figure 19. Spacers for alignment.

The topmost box (placed with horizontalP) must fill up all the available space. The lower three boxes have been placed inside a box which consumes the extra space. The spacers used are derived from the spacer hAlignS, whose argument states the ratio between the space to the left of the box and the total available extra space:

hAlignS :: Alignment -> Spacer
leftS = hAlignS 0
hCenterS = hAlignS 0.5
rightS = hAlignS 1
There is a corresponding spacer to flipP, namely flipS. It too flips the x and y coordinates, and lets us define some useful vertical spacers:
flipS :: Spacer -> Spacer
vAlignS a = flipS (hAlignS a)
topS = flipS leftS
vCenterS = flipS hCenterS
bottomS = flipS rightS
With compS, we can compose spacers, and define a spacer that centers both horizontally and vertically:
compS :: Spacer -> Spacer -> Spacer
centerS = vCenterS `compS` hCenterS
To add extra space to the left and right of a box, we use hMarginS left right, where
hMarginS :: Distance -> Distance -> Spacer
type Distance = Int
Distances are given in number of pixels.(Footnote: This is easy to implement, but makes programs somewhat device dependent.) From hMarginS, we can derive marginS, which adds an equal amount of space on all sides of a box:
vMarginS above below = flipS (hMarginS above below)
marginS s = vMarginS s s `compS` hMarginS s s
Spacers can be applied to fudgets by means of spacerF:
spacerF :: Spacer -> F a b -> F a b
The fudget spacerF s f will apply the spacer s to all boxes in f which are not enclosed in other boxes. We can also modify a placer by wrapping a spacer around the box that the placer assembles:
spacerP :: Spacer -> Placer -> Placer
For example, spacerP leftS horizontalP gives a horizontal placer which will left adjust its boxes.

11.2 Name layout

To separate layout from fudget structure, we put unique names on each box (usually corresponding to a simple GUI fudget) whose layout we want to control, by using nameF:
type LName = String
nameF :: LName -> F a b -> F a b
The layout of the boxes that have been named in this way, is specified using the type NameLayout. Here are the basic functions for constructing NameLayout values:
leafNL :: LName -> NameLayout
placeNL :: Placer -> [NameLayout] -> NameLayout
spaceNL :: Spacer -> NameLayout -> NameLayout
To apply the layout to named boxes, we use nameLayoutF:
nameLayoutF :: NameLayout -> F a b -> F a b
As an application of name layout, we show how the vertical counter in Figure 18 can be changed, so that the display appears between the up and down buttons (Figure 20):
nlCounterF = nameLayoutF layout counterF

counterF = nameF dispN intDispF
           >==< mapstateF count 0
           >==< (nameF upN    (buttonF filledTriangleUp) >+<
                 nameF downN  (buttonF filledTriangleDown))

-- only layout below
layout = placeNL verticalP (map leafNL [upN, dispN, downN])
upN = "up"
downN = "down"
dispN = "disp"

Figure 20. With name layout, the order of the GUI elements in the window does not have to correspond to their order in the program text.

Now, we can control the layout of the two buttons and the display, without changing the rest of the program.

The actual strings used for names are unimportant, as long as they are unique within the part of the fudget structure where they are in scope. So instead we can write

(upN:downN:dispN:_) = map show [1..]

11.3 Pros and cons of the different layout methods.

When it comes to specifying the layout of user interfaces, the Fudget library provides at least three solutions that differ in expressiveness and safety:
  1. The don't care solution: ignore the problem. The programmer can compose a number of GUI fudgets using plumbing combinators, without specifying the layout. The system will automatically pick some layout. This is perfectly safe, but it obviously does not give the programmer any control over layout.
  2. The combinator-based approach: the programmer inserts placerF applied to some placer at some selected points in the fudget hierarchy. This gives the programmer more control over layout and is still perfectly safe, but there is a coupling between how the fudgets have been composed and how they appear on the screen. This is not necessarily bad, but it limits the freedom in the choice of layout.
  3. Name layout: the boxes of the GUI elements are labelled with unique names. On the top level of the program, the programmer inserts nameF applied to a layout specification which, by referring to the names, can achieve a layout of the GUI elements completely unrelated to how they were composed.

    In this solution it is possible to make mistakes, however. For the layout specification to work properly, the name of every named box should occur exactly once in the layout specification. If you forget to mention a box, or if you mention it twice, or if you name a box that does not exist, the layout will not work properly. These mistakes are not detected at compile time, but give rise run-time errors or a weird layout.

The Fudget library thus offers safe solutions with limited freedom, and unsafe solutions with full freedom. With respect to safety and expressiveness, the solutions used in some other functional GUI toolkits, for example Haggis [FP96] and Gadgets [Nob95], are equivalent to name layout.

The problem with the name layout solution is that it requires a certain consistency between two different parts of the program. Maintaining this consistency during program development is of course an extra burden on the programmer.

Can a type system be used to make name layout safe? It would perhaps be possible to include layout information in some form in the types of GUI fudgets and catch some mistakes with the ordinary Haskell type system. However, the requirement that each name occurs exactly once in the layout specification suggests that you would need a type system with linear types [Hol88] to catch all mistakes.