Many moons ago, Vassili Bykov put up a good writeup or two regarding VisualWorks widget layout mechanisms.
There are multiple aspects to widget layout; there's a dance that gets played between the subView and superView as they come to an agreement about where a subView goes. For this post will leave the faults of VisualWorks superView roles alone, and just concentrate on this layout thing which is what a subView uses to tell its superView where it wants to be.
All current Layout objects are basically a set of control variables used to tune a specific layout function. If we were speaking functional parlance, we might say layout is "f of r, where r is the containing rectangle, and f outputs a rectangle relative to the input." We have a couple of different ones of these in VisualWorks. I have a couple of problem with these.
The first is that there's a limited amount of algorithms. You can pick one of a couple of flavors. The objects are not easily decomposable, so you can't easily synthesize them together. For example, what if you want the behavior of a LayoutFrame in the horizontal, but that of an AlignmentOrigin in the vertical. Here's a couple of scenarios that I've happened upon over the years:
- Position fractionally AFTER an initial pixel offset (this is inside out from a LayoutFrame).
- Center the preferredBounds in the provided rectangle, but if we run out of space, align to the top/left. This is usually the desired thing to do for labels.
- Maintain square proportions, centered within the provided rectangle.
- etc. (add your own in the comments?
My second beef with them is that it doesn't feel correctly encapsulated. Object Oriented Programming, is about binding behavior to data. The data here is rectangles. And these various algorithms are methods (or combinations of them) that belong on Rectangle. In truth, many do. But the Layout objects ignore that. Or remove that. Or something. Adding a new algorithm involves adding a whole new object with it's control values, rather than adding a method to Rectangle. Extending a base object like Rectangle is where Smalltalk draws some of its best power from. The other part of a Layout object, acting as a function... we have a wonderful power object in Smalltalk for full closure functions already: the BlockClosure. I also feel that having the algorithm exposed through it's control variables only "hides" the intent often. For example, what does the following code specify?
LayoutFrame
leftFraction: 0.5
offset: 10
topFraction: 0
offset: 10
rightFraction: 1
offset: -10
bottomFraction: 0.5
offset: -10
The new programmer has to be taken to the white board and have the details of LayoutFrame explained. What if we could just express that via "expressive" Smalltalk code? Maybe something like:
(rect topCenter corner: rect rightCenter) insetBy: 10
Personally, I like this better. I can visualize in my mind right away these center points along the edges, and the rectangle that is drawn between them. Rather than having to wonder why some offset values are -10 and some are +10, I just know right away that we're going to inset it by 10 pixels.
About 6 months ago, I thought I had a solution to this. I've been playing around with it in a couple of contexts. It is frankly, inspired by VSE's framingBlock, which goes way back in time. I've now realized this code in a package called FunctionalLayouts which I hope to integrate in to the base real soon. It has been placed in the open repository.
Said package adds a FunctionalLayout object to the Layout hierarchy. Basically, the idea is that use expressions like the one above in a block to make your layout more clear and gain all the flexibility in the world. Here's some examples:
FunctionalLayout block: [:rect : | | inner | inner := (rect insetBy: 20).
(0@ (1/3) corner: (2/3) @ (1/2)) scaledBy: inner extent ]
"a candidate of example 1 above"
FunctionalLayout block: [:rect :desired | (rect center - desired half max: rect origin) extent: (desired extent min: rect extent)] "example 2 from above"
FunctionalLayout block: [:rect : | | square | square := (rect width min: rect height) asPoint.
rect center - square half extent: square ] "example 3 above"
FunctionalLayout block: [:rect | rect ] "fully attached, the default state"
FunctionalLayout block: [10@100 extent: 40@ 50] "good old fashioned direct rectangle specification"
Note, there is nothing that precludes making a first class object out of a given algorithm here. Or we can reduce some of these expressions to helper methods on Rectangle. What's telling to me, is that I can use FunctionalLayout to do what every existing Layout does. It subsumes them all. A sort of "One Layout to Rule Them All and In Position Bind Them" sort of thing.
One may also observe from the examples, that the arguments are optional. Remember that post about block culling?
For a final fun touch, here's a quick prototype I whipped up in a workspace which does a clock layout of VisualBlocks. You definitely won't do this with the current Layout objects as easy. Especially when you resize the window and numbers layout correctly still. With relative aspect too.
composite := CompositePart new.
1
to: 12
do:
[:index |
| label |
label := Label with: index printString.
vb := VisualBlock
block:
[:gc :box |
gc
paint: ColorValue red;
displayWedgeBoundedBy: box startAngle: 0 sweepAngle: 360;
paint: ColorValue black;
display: label
at: box center - label bounds extent half].
composite
add: vb
in:
(FunctionalLayout
block:
[:box |
extent := box extent * 0.1.
angle := (index / 12 * 360 - 90) degreesToRadians.
box center + (box extent * 0.4 * (angle cos @ angle sin))
- extent half extent: extent])].
(ApplicationWindow new)
component: composite;
open