Friday, January 13, 2012

Some Edgy Reification

Ever had that experience where you're working on something, and it just feels like something's missing? You muddle on for a while. You begin to suspect there's an object whispering in the digital ether to come into being. When it works out (it doesn't always), I've found it to be one of the more rewarding experiences along the Zen Path of All Objects All The Time.

Working with GUIs and widgets and layout, an object I get to know real well is Rectangle. It's a pretty classic object that you find in many a class library, Smalltalk or otherwise. When working with widget trees and layout, rectangles make a lot of sense, because they store (directly or indirectly) the data you need to approximate most widget's layouts (e.g. top, left, bottom, and right). But when it comes to actually working with layout, my experience has been that often just Rectangles are sub optimal.

Generally, the types of behavior Rectangle supports are methods that work with all or most of the 4 implied values of a rectangle. But much of layout involves dealing with just one side of a rectangle. When we're left aligning some rectangles, we care about their left first, and then possibly all of their rights. So we often see code that looks like

newBox := bounds left + someDelta @ bounds top extent: bounds extent.

We didn't want to have to care about tops or extents.

We could do
aBox left: aBox left + someDelta

but I see less of this pattern actually.

A more involved example (no code) involves determining the bottomRight corner of a rectangle for a widget that may or may not have scrollbars around it. First we check if we need to move the right in for a scrollbar, and do so by the thickness if necessary. Then we use that width to determine if we need to show a horizontal scrollbar now. And now if we adjusted for that, we have to check one more time to see if we didn't need a vertical scrollbar before but do now. We care about first the right side, then the bottom side, then the right side.

Another pattern I've seen is that there are many types of widgets and layouts that can be either vertical or horizontal. A row layout container and column layout container are very similar, but one works along the x axis and the other the y axis. Consider a chunk of code that given a list of rectangles, stretch their widths from the leftmost to the rightmost, proportionately by their current widths.

| sumWidth scalar lastRight |
sumWidth := aBoxList inject: 0 into: [:sum :each | sum + each width].
scalar := sumWidth / (aBoxList last right - aBoxList first left).
lastRight := aBoxList first left.aBoxList do:
[:each |
each left: lastRight.
each right: (lastRight := each width * scalar + each left)]

If I want to reuse that same algorithm to work in a vertical direction, I get to copy/paste it and modify as such

| sumHeight scalar lastRight |
sumHeight := aBoxList inject: 0 into: [:sum :each | sum + each height].
scalar := sumHeight / (aBoxList last bottom - aBoxList first top).
lastBottom := aBoxList first top.aBoxList do:
[:each |
each top: lastBottom.
each bottom: (lasBottom := each height * scalar + each top)]

It gets even funner when you're working with points and have to transpose all of the constant x values for constant y values.

The Free Dictionary defines reification as
To regard or treat (an abstraction) as if it had concrete or material existence.

What I found working with these types of problems was that there was an object for a Rectangle's edge that really wanted to come into being. RectangleEdge is an abstract class defining the API, and polymorphic subclasses for TopEdge, LeftEdge, RightEdge, and BottomEdge take care of the particulars. Having an object which reifies the edge of a rectangle allows us to talk about rectangle manipulation at a higher level for many problems.

What follows is an overview of the classes' use and APIs. Followed by a revisit of the above examples, but using edges.


A particular edge can created by sending leftEdge, rightEdge, bottomEdge, or topEdge to an instance a Rectangle.

aRectangle bottomEdge

Or by sending the message of: to a given EdgeClass
BottomEdge of: aRectangle

A selection of some of the handier high level messages that allow us to talk about rectangle edges more directly

"How far apart are I and my complement (opposite) edge?"
"The placement value for this edge along either the x or y axis as appropriate."
position: aNumber
"Modify my target rectangle such that the edge I reify is now at aNumber."

-= aDelta
"Modify my position so that it is my current position minus aDelta."
+= aDelta
"Modify my position so that it is the sum of aDelta and my current position."
lineUpWith: anEdge
"Change my position so that I line up with anEdge."

Other Edges
"Return the edges that are normal or perpendicular to me."
"Answer the edge of my box that is opposite of me."
sameEdgeOf: aRectangle
"Return the equivalent edge of aRectangle to me."

attractsEdge: anEdge
"Answer whether I'm the kind of edge that would want to be considered in concert with anEdge for alignment purposes."
linesUpWith: anEdge
"Do I associate with anEdge in alignment considerations?"

There's more than these, but these seemed some of the more obvious. I actually came up with these classes when I first came to work at Cincom and was working on the failed next generation UIPainter with Vassili Bykov called "Splash". Having reified edges was a big help in doing snap to alignment and other interactive layout operations. And I've been using it with the work on skins (see the description of doing scrollbar layout above). And have been using it with layout prototypes on beyond the current Panel idea found in VisualWorks. It's likely it will finally be integrated in a build near you, before our next release.

Circling back around to the original examples, I now write code like

aBox rightEdge -= self scrollbarThickness

I think this makes the code more expressive of what I'm doing, instead of being lost in how I'm trying to manipulate a rectangle to get the same result. The layout algorithm is even more interesting. Parameterizing the code shown with which edge classes to use, one can make it work for either horizontal or vertical manipulation, by just changing one variable:

| sum scalar lastPosition edgeClass |
edgeClass := LeftEdge.sum := aBoxList inject: 0
into: [:sum :each | sum + (edgeClass of: each) complementDistance].
scalar := sum /
((edgeClass of: aBoxList last) complementEdge position - (edgeClass of: aBoxList first) position).
last := (edgeClass of: aBoxList first) position.aBoxList do:
[:each |
| lowEdge |
lowEdge := (edgeClass of: each) position: last.
lowEdge complementEdge
position: (last := lowEdge complementDistance * scalar + lowEdge position)]

This reified Edge object has enabled me to build layout algorithms that can be configured to work in either the x or y directions without having to have lots of code duplication.

No comments:

Post a Comment