Sunday, November 13, 2005

I now have basic box support working in the layout engine on MacOS X.

The following XML illustrates a layout that is based on boxes:

<window name="main" title="&hello.title;" main="true" width="320" height="200" x="100" y="100" position="center, mouse">
<script type="text/javascript" src="resources/content/simple.js"/>
<box orient="vertical">
<text editable="true" selectable="true" string="&text1.value;" width="100" height="40"/>
<text editable="false" selectable="false" string="&text2.value;" width="100" height="30"/>
<statictext string="&statictext.value;" width="100" height="40"/>
<box orient="horizontal">
<button onclick="return Button1Click();" label="&button1.label;"/>
<button onclick="return Button2Click();" label="&button2.label;"/>

There are two boxes in this UI, one vertical, and one horizontal. Actually, if you count the window tag, there are three boxes (the window tag has layout semantics similar to a vertical box). The vertical box has 4 children, from top to bottom: an editable text field, a non-editable text field, a static text field, and a horizontal box. The horizontal box has two children, both which are buttons. The resulting UI looks like the following:

The basic strategy to implement boxes was to create a class named box that inherits from Container:

#if !defined(__CONTAINER__H__)
#define __CONTAINER__H__

#include "widget.h"

class Container : public Widget
virtual ~Container() {};
virtual PRStatus CreateChildren();
virtual PRStatus DrawChildren();
virtual PRStatus ShowChildren();
virtual PRStatus HideChildren();


#if !defined(__BOX__H__)
#define __BOX__H__

#include "container.h"
#include "boximpl.h"
#include "widgetfactory.h"

class Box : public Container

typedef enum {
} BoxOrientation;

virtual ~Box();
PRStatus Draw();
PRStatus Create();
PRStatus Show();
PRStatus Hide();
PRStatus ComputeLayout(const int &base,
const int &offsetx, const int &offsety,
int &x, int &y, int &width, int &height);
PRStatus GetGeometry(int &x, int &y, int &width, int &height);
PRStatus SetGeometry(const int &x, const int &y,
const int &width, const int &height, const char &mask);
ElementType GetType() {return TYPE_BOX;};
bool IsContainer() {return true;};
WidgetImpl *GetImpl() {return m_box;};

PRStatus GetOrientation(BoxOrientation &orient);
PRStatus SetOrientation(const BoxOrientation orient);

BoxImpl *m_box;
BoxOrientation m_orient;


Containers are what you might expect: in addition to being widgets, they have children (themselves widgets) that need to be created, shown, hidden, and drawn. In addition, boxes must compute a layout, and they have an orientation - vertical boxes arrange their children from top to bottom, while horizontal boxes arrange their children from left to right.

Almost every UI toolkit involves the following steps to create and show a UI:

-- create the widgets, assigning each a size and location within the window
-- make the widgets visible

With a static layout engine, the XML describing the layout specifies the x and y coordinates of each widget, and when the UI is created, the coordinates that were specified in markup for each widget are used to set their locations before they are made visible on the screen.

On the other hand, an engine supporting dynamic layout creates widgets without regard to their final locations; at the point of creation, all that is important is that the internal representation of the widget elements preserves 1) the parent/child relationships that were specified in markup and 2) the relative ordering of the children with containers (e.g., vertically and horizontally). Once the widgets are created, the layout engine then computes the layout of the widgets in the window in a top-down, somewhat recursive fashion. Once the layout is computed, the layout engine then instructs each widget to show itself (this also happens in a top-down, recursive fashion). It is the computation of the layout that is the most interesting aspect of the layout engine's work. Let's take a look at how it is done in the simplest of cases.

Each widget has a size requirement. A static text field, for example, must be large enough to display its label. The process of computing a layout includes querying each widget to determine how much real estate is needed in order for that widget to display itself completely. This computation is done in a topdown fashion for all widgets in the window, including containers. In the layout above, the window widget calls the outermost vertical box widget, asking it to compute its layout. The vertical box widget, in turn, asks each of its children to compute their layouts. In the case of the text widgets and the static text widget, computing a layout is done by determining its basic size (the height of its font determines the vertical space requirement, while the width of the font mulitplied by the number of characters in its label determines its horizontal space requirement). In the case of the horizontal box, its layout is computed by calling each of its button children to determine their size needs (which in turn are a function of the width and height of their labels), and then using this information to compute the size of the horizontal box, the height of which is the greater height of the two buttons, and the width of which is the sum of the widths that were computed by each of the buttons.

Once the above sizes are computed, widgets are then told by containers to reposition themselves based on the computed layout. When all the widgets in the window have been given their new sizes, the layout engine then iterates the widget hierarchy once more, and tells each widget to display itself.

Should the user resize a window containing a dynamic layout, the same process may repeat itself. Depending on the location of the window origin, recomputing a layout may be needed. Under Cocoa, the origin of a window is at the lower left hand corner of the window as viewed by the user. Because of this, changing the height of a window requires a recomputation of the location of each widget because the layout is specified in markup as though the origin was actually located at the upper left hand corner of the window. But even if that were not the case, other characteristics of a layout may require recomputation in both the horizontal and vertical directions. One example would be a container that is designed to give each child an equal (or homogeneous) amount of space. As the amount of space changes in a container, the amount of space available to each widget must also change, forcing a recomputation of the layout. At this point, the layout engine does not support this sort of mode; recomputation is done only to ensure that the widgets position relative to 0,0 is consistent when the height of the window changes.