Saturday, December 03, 2005

After a couple of weeks of (part-time) work, spacers have been added as a widget and incorporated into the layout engine. The following screen shots show examples of a complex layout with spacers after various resizes (note, the vertical orientation of these windows is inverted, due to Cocoa -- window origins are at the lower left corner, not the upper left corner as in many other toolkits. This is something the layout engine needs to deal with and I had yanked support for it while debugging to simplify things):






The widget hierarchy for this window is:

root
window
button
spacer
vertical box
spacer
text
spacer
text
spacer
text
spacer
horizontal box
spacer
button
spacer
button
spacer
vertical box
spacer
button
spacer
button
spacer
spacer
spacer
spacer
vertical box
spacer
text
spacer
text
spacer
text
spacer
horizontal box
spacer
button
spacer
button
spacer
vertical box
spacer
button
spacer
button
spacer
spacer
spacer
spacer
button


In short, the window contains two vertical box children, each which contains an identical set of child widgets. These widgets include a few text fields, and a horizontal box. the horizontal box contains three children of its own: two buttons, and a vertical box which itself contains two child buttons.

Between all these widgets are spacers.

As the window is resized (including when it is first displayed), an event is triggered in the concrete widget's window class. This event is being listened to by an abstract window class. The abstract window class then invokes a function that it implements named ComputeLayout(), passing to it the width and height of the window. This size, plus the layout of the document below the window, and the size needs of the widgets it contains, will determine the ultimate layout of the window. This layout is computed, recursively, by calls to ComputeLayout() (for container widgets), and to GetGeometryRequest(), which is implemented by each widget that exists in the widget hierarchy of the window.

Leaf widgets (e.g., those that do not contain other children), simply report their absolute size requirement from within GetGeometryRequest(). For example, the abstract text widget's GetGeometryRequest() function invokes the concrete text widget's implementation of GetGeometryRequest(), which will use the size of the font, and the size of the text being displayed, to determine the size request that is returned.

It is in the ComputeLayout() functions implemented by Box:: and Window:: that all of the important work is done. Let's take a closer look at these functions, starting with the simpler of the two, Window::ComputeLayout(), since it only lays its children out vertically. The Box::ComputeLayout() function must layout vertically or horizontally, depending upon the orientation of the box.

Here is the source for Window::ComputeLayout():


PRStatus Window::ComputeLayout(const int &x, const int &y, const int &width,
const int &height)
{

list::iterator itr;
int widthacc = 0, heightacc = 0;

int childx, childy;
int childwidth, childheight, childwidth2, childheight2;
int spacerdenom = 0;
int winx, winy, winwidth, winheight;
int vboxcount;

GetGeometry(winx, winy, winwidth, winheight);

// figure out what size the children want to be

GetGeometryRequest(childx, childy, childwidth, childheight, true, false);

// figure out the spacer relative sizes

for (itr = m_children.begin(); itr != m_children.end(); ++itr) {
Widget *w = reinterpret_cast(*itr);
if (w) {
if (w->IsSpacer()) {
Spacer *spacer = reinterpret_cast(w);
spacerdenom += spacer->GetSpacerValue();
}
}
}

// allocate the spacers, dividing up the remaining width and height

int remainingwidth = winwidth - childwidth;
int remainingheight = winheight - childheight;

if (remainingheight < 0)
remainingheight = 0;
if (remainingwidth < 0)
remainingwidth = 0;

for (itr = m_children.begin(); itr != m_children.end(); ++itr) {
Widget *w = reinterpret_cast(*itr);
if (w) {
if (w->IsSpacer()) {
Spacer *spacer = reinterpret_cast(w);
int spacerval = spacer->GetSpacerValue();
int spacerwidth = (int)(remainingwidth * ((double)spacerval/spacerdenom));
int spacerheight = (int)(remainingheight * ((double)spacerval/spacerdenom));
w->SetGeometry(0, 0, spacerwidth, spacerheight, GEOM_ALL);
}
}
}

GetGeometryRequest(childx, childy, childwidth2, childheight2, false, true);
GetSiblingVboxCount(vboxcount); // share equally among all vertical children

int remainingwidth2 = (winwidth - childwidth2) / vboxcount;
int remainingheight2 = (winheight - childheight2) / vboxcount;

if (remainingheight2 < 0)
remainingheight2 = 0;
if (remainingwidth2 < 0)
remainingwidth2 = 0;

for (itr = m_children.begin(); itr != m_children.end(); ++itr) {
Widget *w = reinterpret_cast(*itr);
if (w) {
int tmpx, tmpy;
w->GetGeometryRequest(childx, childy, childwidth, childheight, true, false);
if (w->IsContainer()) {
tmpx = 0;
tmpy = heightacc;

Container *c = reinterpret_cast(w);
bool isBox = (w->GetType() == TYPE_BOX);
bool isVbox = false;
if (isBox) {
Box::BoxOrientation orient;
Box *box;
box = reinterpret_cast(c);
if (box) {
box->GetOrientation(orient);
if (orient == Box::Vertical)
isVbox = true;
}
}
c->ComputeLayout(tmpx, tmpy, winwidth, remainingheight2);
w->GetGeometry(childx, childy, childwidth, childheight);
if (childwidth > widthacc)
widthacc = childwidth;
heightacc += childheight;
}
else {

w->SetGeometry(0, heightacc,
childwidth, childheight, GEOM_ALL);
if (childwidth > widthacc)
widthacc = childwidth;
heightacc += childheight;
}
}
}

return PR_SUCCESS;
}


Breaking this down a bit, the function does the following:

-- Get the size of the window. This was stored in the Window class instance when the window was created (or when it was resized, depending on what triggered ComputeLayout() to be invoked. This is done with a call to GetGeometry(), which every widget (container or not) implements.

-- Determine the intrinsic size requirements of the window's children, including any embedded boxes it contains. This computation omits the size requirements of spacers, which are not known at this time (and are computed later).

-- For all spacers that are immediate children of the window (if there are any), compute a weight relative to the other spacers that are also immediate children. This weight begins, in markup, as an integer valued attribute of the spacer. The sum of these attributes is computed. Then, a percentage is computed for each spacer by dividing the value of its weight attribute by this sum. An example will help understand this better. Assume that the window contains two button, and three spacers, arranged as follows:

spacer weight = "1"
button
spacer weight = "2"
button
spacer weight = "3"

The sum of the spacer's weights is 6. The percentage amount of vertical space allocated to each spacer is:

spacer 0.167
button
spacer 0.333
button
spacer 0.5

The first spacer this gets 1/6th of the whatever vertical space is not used by the buttons in the window, the second spacer gets a third of this space, and finally, the last spacer gets half of the remaining space.

-- The remaining width and remaining height values are computed by subtracting the child widget size requests from the width and height of the window (which were passed as arguments to ComputeLayout()).

-- The spacers are assigned their vertical heights.

-- Now that the spacers have been assigned their sizes, and now that we know the size requirements of the window's other children, we can iterate the entire list of children (spacers, child containers, buttons -- whatever) and generate a layout.

If the child widget is not a container, then this is relatively straightforward: call the widget's GetGeometryRequest() function to get its desired width and height, and then call the widget's SetGeometry() member. Each widget is given its requested width and height, and its x coordinate is set to zero. The y coordinate is set to the accumulated height of all children that came before it in the window.

If, however, the child is a container (e.g., a box), we call its ComputeLayout() function, which performs the same algorithm being described here to layout its children. We pass to ComputeLayout() zero as the x coordinate, the accumulated height as the y coordinate, and restrict its width to the width of the window, and its height to a subset of the vertical space remaining in the window. Once ComputeLayout() returns, we query the geometry of the container by calling its GetGeometry() member function, and add its height to the accumulated y coordinate we are maintaining.

Once all the children have been layed out, the window's geometry is set with a call to its SetGeometry() member function.

To help make better sense of all of this, here is the basic algorithm:

heightacc = 0;
widthacc = 0;
while (there are children) {
GetGeometryRequest(x, y, widthrequest, heightrequest);
if (child is a container) {
ComputeLayout(0, heightacc, windowwidth, remainingheight);
GetGeometry(x, y, width, height);
heightacc += height;
widthacc += width;
} else {
SetGeometry(0, heightacc, widthrequest, heightrequest);
heightacc += heightrequest;
widthacc += widthrequest;
}
}

// x and y in the following call are the original coordinates for the window

SetGeometry(x, y, widthacc, heightacc);

One attribute of above algorithm is that the first vertically oriented container widget (window or vertical box) in the window's widget hierarchy that contains a spacer will allocate all of the vertical space that is not allocated to widgets. This is an attribute of the topdown nature by which calls to ComputeLayout() are made to containers in the window. A bottom up visitation would, naturally enough, reverse this, with the lowest-level container containing spacers using all the remaining space in the window. There is probably some modification to either approach that might distribute the remaining space more equitably among containers at all levels of the widget hierarchy of the window. However, this is an exercise for the reader :-)