I’ve got a date tomorrow morning with a laptop, JavaFX, and a mic:
Interface Layout with JavaFX2.0
Wednesday, 10:00 AM, Hotel Nikko – Nikko Ballroom II/III
I’ll be speaking about all the topics I’ve been meaning to blog about.
I’ve got a date tomorrow morning with a laptop, JavaFX, and a mic:
Interface Layout with JavaFX2.0
Wednesday, 10:00 AM, Hotel Nikko – Nikko Ballroom II/III
I’ll be speaking about all the topics I’ve been meaning to blog about.
Office drawers are like attics. The other day I opened one and found a stack of pictures. This was the Java Client team in late 1996.
Left to Right: Jim Graham, Rick Levenson, Tim Prinzing, Georges Saab, Jonni Kanerva, Jeannette Hung, Tom Ball, Jeff Dinkins, Amy Fowler.
An email later and we all gathered for lunch at Hobees in Sunnyvale:
Left to Right: Jim Graham, Rick Levenson, Tim Prinzing, Georges Saab, Jonni Kanerva, Jeannette Hung, Tom Ball, Jeff Dinkins, Amy Fowler.A little more sun in the eyes and a few grey hairs. The front row still works on the jdk!
A couple others who were there back in the day came along too.
Left to Right: Jim Graham, James Gosling, Rick Levenson, Tim Prinzing, Georges Saab, Jonni Kanerva, Jeannette Hung, Tom Ball, Brian Beck, Amy Fowler.
JavaFX2.0 Beta is out. We’ve taken advantage of the language shift to extensively remodel the layout APIs based on a year’s worth of tire-kicking. This article (the first in a series) will introduce you to the basics.
In marrying the GUI to a scene-graph, each graphical element is represented by a stateful object rather than drawn inside a clipped rectangle, and this makes layout more complicated. Hence, many of the JavaFX1.3 concepts still apply, however we’ve worked hard to simplify and eliminate the seams.
An interface is created by assembling scene-graph nodes into a hierarchy within a Scene. The three Parent types have different layout characteristics:
A silly example to give you the flavor if you’ve never programmed FX:
// silly example Text text = new Text("JavaFX 2.0!"); text.setFill(Color.RED); text.setFont(new Font(24)); Line line = new Line(2, 8, 104, 8); line.setStroke(Color.BLUE); line.setStrokeWidth(4); Group group = new Group(); group.setEffect(new DropShadow()); group.getChildren().addAll(text, line); BorderPane root = new BorderPane(); root.setTop(new Label("Introducing...")); root.setCenter(group); Scene scene = new Scene(root); stage.setScene(scene); stage.setVisible(true);
To explore some core scene-graph layout concepts, let’s zoom in on the key classes:
Each Node in the scene-graph has the potential to be “resizable”, which means it expresses it’s minimum, preferred, and maximum size constraints and allows its parent to resize it during layout (hopefully within that range, if the parent is honorable).
Each layout pane implements its own policy on how it resizes a resizable child, given the child’s size range. For example, a StackPane will attempt to resize all children to fill its width/height (honoring their max limits) while a FlowPane always resizes children to their preferred sizes, which is what we mean by the term “autosize” (resize-to-preferred).
But here’s what surprises (and annoys) people: not all scene-graph node classes are resizable:
*will be made resizable in future release
If a Node class is not resizable, then isResizable() returns false, minWidth()/minHeight(), prefWidth()/prefHeight(), and maxWidth()/maxHeight() all return its current size, and resize() is a no-op. The size of a non-resizable node is affected by its various properties (width/height for Rectangle, radius for Circle, etc) and it’s the application’s responsibility to set these properties (and remember that scaling is NOT the same as resizing, as a scale will also transform the stroke, which is not usually what you want for layout). When non-resizable nodes are thrown into layout panes, they will be positioned, but not resized.
You inevitably want to ask why not make shapes resizable? While it’s straight-forward to do the math on basic shapes (Rectangle, Circle, etc), it gets a bit muddier with Paths, Polygons, and 3D. Also, keeping the core graphics shapes non-resizable allows us to make certain efficiencies, like ensuring a Group with non-resizable descendents pays no penalty for layout. So for now anyway, that’s the way it is.
Tip: if you need a resizable rectangle, use a Region and style it with CSS.
For most node classes, width and height are independent of each other, however, there are a handful where height-depends-on-width or visa-versa. In order to deal with these guys properly, the layout system needs a way to decipher it, hence Node’s getContentBias() method. This returns null by default, which means there is no width/height dependency and layout code can pass -1 as the alternate dimension to the minWidth(h)/minHeight(w), prefWidth(h)/prefHeight(w), and maxWidth(h)/maxHeight(w) methods.
Labeled controls have a HORIZONTAL content-bias when wrapping is on. FlowPane and TilePane support a content-bias that matches their orientation. Note: turns out that HBox, VBox, and StackPane return a null content-bias, however they should really report a content-bias based on their childrens’ content-biases; this is a bug that will be fixed before the final release.
Our layout classes do the heavy-lifting in supporting content-bias in the system, so unless you are writing a custom layout class that handles arbitrary nodes, you needn’t deal directly with this API. Just know layout supports content-bias automatically when you build your interfaces.
Most Control and layout pane classes are pretty good at computing how big they ought to be based on their content and property settings — we’ll call this their “intrinsic” min, pref, and max sizes. For example, a Button will compute these sizes based on its text, font, padding, and so on; additionally a Button’s intrinsic max size defaults to its preferred because you usually don’t want buttons expanding to fill space. You shouldn’t have to explicitly set the size of controls, layout panes, or even the Scene, because the system will shrink-to-fit around your content.
But that’s a lie. Or at least counter-intuitive to the nature of designers. Thus, you can directly influence the size of Controls and Regions by setting their min, pref, and max size ‘override’ properties:
// make my button 100 pixels wide! button.setPrefWidth(100);
By default these properties have the sentinel value USE_COMPUTED_SIZE, which causes Node’s minWidth(h)/minHeight(w), prefWidth(h)/prefHeight(w), maxWidth(h)/maxHeight(w) methods to return their intrinsic values. Setting them to a positive double value will override their intrinsic values.
Often you want to ensure that the min or max size tracks the preferred, so the min and max overrides may also be set to USE_PREF_SIZE, which does the obvious:
// clamp my stackpane's size stackpane.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
Beware of the subtle different between the Node methods and the size override properties:
button.getPrefWidth() returns the size override (might be a magic sentinel value) while button.prefWidth(-1) returns the actual preferred width of the button (returning the override if set). Layout code should always use Node’s minWidth(h)/minHeight(w), prefWidth(h)/prefHeight(w), maxWidth(h)/maxHeight(w) methods for layout computations.
There’s more to the story of course, but I think that’s one blogful enough for now.
With the JavaFX2.0 public beta, you have the opportunity to influence the API based on cold, hard use cases, so don’t hold back (as if you would :-)).
At last night’s JavaOne JDK BOF, the question was posed, “What are the JDK plans for Swing?” Mark Reinhold noticed me skulking in the audience and bounced the question: “Aim?”. Caught a bit off guard, I answered a pat “it’s still there and it isn’t going anywhere”. The poser of the question stormed out (I don’t blame him), as that wasn’t the answer he wanted to hear, so I’d like to elaborate.
In working on JavaFX the last couple years and occasionally posting about it, I’m accustomed to the hate mail from Swing fans. In many ways I’m heartened by the public support for a technology on which Sun itself was so fickle. Swing is a great toolkit. It’s flexibility and extensibility enables developers to build amazingly rich and complex applications. It’s understandable that the Swing community would be incensed about the way Sun prematurely unveiled an infantile JavaFX as if it could replace 10+ years of steady enhancements and tuning of Swing & Java2D. 2007-2009 were excruciating years for all of us.
But Java and the JDK are under new stewardship now. If you’re at JavaOne or reading the tweetesphere, then you know that Oracle is deadly serious about it’s plan to resurrect Java and the JDK as the cross platform solution for the future. The proof of course will be in execution over the next year or two, but this is a practical company who isn’t suffering from an identity crisis and knows how to make money from software. A sleeper detail from Thomas Kurian’s keynote is that NetBeans will be the Java development IDE of choice going forward. This is very good news for Swing, ensuring it’s support and upkeep for a long time to come.
But that’s still not what Swing developers want to hear. Which brings me to the actual point of this rambling. User-interface technology is on a collision course with graphics. For Java to be a viable client platform in the long haul, it must be inherently capable of the sort of visual delicacies that you see in iPhone/iPad applications. As I’ve stated many times, it’s possible to achieve some of this using Java2D tricks in Swing (e.g. Kirill’s very nice Trident library), but it requires sophisticated gymnastics; it’s not a natural fit. The crux of the problem is that Swing is rooted in the antiquated AWT, rather than being integrated into the 2D coordinate system. Fixing this would require some massively incompatible changes and once you march down that path, well, you start asking yourself what other incompatible improvements should be made ….pretty soon the remodel becomes reconstruction and you realize that a new foundation is really needed.
Enter Prism. If you saw Jasper’s “Scott Pilgrim” demo at Kurian’s keynote then you’d understand what a hardware-accelerated graphics pipeline with a UI rooted in a 2D/3D scene-graph can do. Sure, most UIs won’t need to kick hundreds of rotating media cubes around, but they will certainly need to translate, rotate, fade, and blend UI elements in interesting ways. JavaFX (Prism + scene-graph + UI controls) is about making it very natural and easy to construct a modern interface and allow your graphics designers to make it pretty via CSS.
And now that we’re converting JavaFX to a proper Java library, you don’t have to learn a new language to use it. Additionally, Oracle has committed to developing the UI controls under open source and our JavaFX 2.0 development plans are very clearly layed out. Much of the JavaFX stack has already been ported off of script, so this is all quite real and the team is ecstatic to be doing Java again – it’s a far better language for toolkit development. Moving to Java should also make it vastly easier for us to support embedding JavaFX in existing Swing applications, so continued investment in Swing applications is preserved.
And to those of you who are upset that we are killing JavaFX script, I say that I feel this pain also. Like most developers who gave JavaFX script a chance, I too became an addict and will miss it as a venue for constructing applications (not libraries). But restating Richard Bair’s excellent follow-up in words from the The Princess Bride: “It’s not dead. It’s only mostly dead.” And if you’ve seen the movie, then you know that ‘mostly dead’ can be resurrected with the right loving care; I recommend attending Stephen’s Chin’s JFXtras session on Wednesday.
Many of you will undoubtedly say “it’s too late”, “developers have already moved on”. That may be the case for some developers and projects, but unlike our beloved predecessor, Oracle sells a lot of applications, and those applications will need great UIs too. It’s an ambitious plan. I don’t expect words or slick demos to change your mind. The burden of proof is now on us to deliver.
There is no place like San Francisco in September. But aside from blue skies, I can say as someone who has been part of JavaOne since 1996 that I’m ecstatic that our newly acquired focus is returning to our roots — The JDK (7 & ~8). Our plans for the technology will be revealed, uncut and without the cloud of uncertainty that hung over the last couple JavaOnes. I think you may find it refreshing.
As evidence refuting the rumor that all smart people work at one Mt.View company, the session catalog has a wealth of interesting talks, but here’s a handful of sessions that pique my interest in the Java language and client technologies:
Monday (Dueling openers!):
Tuesday
Wednesday
Thursday
This will likely be the most interesting JavaOne in years. I’m already looking forward to lunch at Henry Hunan’s.
Establishing dynamic sizing behavior is one of the trickier aspects of GUI layout. For JavaFX 1.3, we’ve introduced the grow/shrink/fill layout preferences on Resizable nodes to support more sensible default resizing behavior of controls and containers out of the box. And while this feature is at first hard to get your head around, once it clicks, it pays.
We have to credit this one to Stephen Chin (JFXtras), who proposed this concept from his Grid API, which we then generalized across the containers. (Thanks, Stephen!)
Let’s take filling first. A Resizable node’s fill policy determines whether the node should be resized to fill its assigned layout area or kept to its preferred size and aligned within the area.
The screen shot below shows two stages which illustrate the difference between no-fill vs. fill:
| No Fill | Fill |
![]() |
![]() |
In 1.2, the fill policy was dependent on which container was used; Stack and Tile implemented filling while HBox, VBox, and Flow did not. So if you threw some buttons into a Tile container (the example above) you would get the fill result (on RIGHT) where all the buttons expand to fill each tile. However, if the buttons were placed in an HBox, they would remain their preferred size, regardless of how much space the Hbox had.
Turns out that whether or not a node should fill is more naturally determined by the node type, rather than its parent container. For example, a Button typically does not want to fill, whereas a ListView often does. So in 1.3 we’ve added getHFill()/getVFill() to Resizable to allow container and control classes to return appropriate values which are honored by the container parents.
When a container is resized larger or smaller than its preferred size, the question is what to do with that surplus or deficit space. In 1.2 we simply left that differential space hanging, but what an application really needs is a mechanism to identify which nodes should absorb it. We considered a number of API approaches (weights, span percentages, etc) but ultimately chose a priority scheme based on Stephen’s Grid API.
A container uses the grow and shrink priorities of its children to determine how it should distribute a surplus or deficit of space in circumstances where the children are competing for that space. Child nodes can specify one of the following grow/shrink priorities:
Below is an example an HBox using its childrens’ grow/shrink priorities to adjust layout as it is resized larger and smaller:
Note that grow priorities apply to the layout area assigned to the node. Whether or not a resizable node is sized beyond its preferred to fill that extra space is determined by its fill preference. In the example above, the toggle-group has grow=ALWAYS but fill=false, so only it’s layout area grows, whereas the search-hbox has grow=ALWAYS but a fill=true, so it resizes to fill the larger area.
Shrinking is not symmetrical, however. If a resizable node’s shrink priority causes it to lose space, it will be resized down to fit within that space.
Similar to filling, we’ve added getHGrow()/getVGrow()/getHShrink()/getVShrink() to Resizable to enable controls and containers to express appropriate preferences for their node type. See the table of Control Fill/Grow/Shrink defaults at the end of this post.
It only makes sense for containers to support these preferences when nodes are in contention for space, so HBox supports it horizontally, VBox supports it vertically, and Grid supports it in both directions. For Stack, since child nodes share the same layout area, childrens’ grow/shrink priorities do not apply. And since Tile and Flow redistribute space by wrapping child nodes rather than resizing them, child grow/shrink priorities are also ignored.
As with the other Resizable preferences, LayoutInfo can be used to override a node’s default fill/grow/shrink defaults. In the HBox example pictured above, most of the defaults took care of the desired behavior, however we had to override the defaults for MyToggleBox and the “?” Stack:
HBox {
spacing: 8
nodeVPos: VPos.BASELINE
content: [
Button {
// no grow by default, so ok
text: "Today"
}
MyToggleBox {
// no grow by default, so turn on grow
layoutInfo: LayoutInfo { hgrow: Priority.SOMETIMES hpos: HPos.CENTER }
items: [ "Day", "Week", "Month" ]
}
HBox {
// grows/fills by default, so ok
nodeVPos: VPos.BASELINE
content: [
Label { text: "Search:" },
TextBox {}
]
}
Stack {
// grows/fills by default, so turn off grow
layoutInfo: LayoutInfo { hgrow: Priority.NEVER }
content: [ Circle {..}, Text {..} ]
}
]
}
The proper default grow/shrink/fill values for Stack, HBox, and VBox should be the greatest value of their children. In other words, if Stack has any child with grow=ALWAYS or fill=true, then the stack should report its own preference as ALWAYS/true; if it only contains non-Resizables (shapes, etc) then it should report its own grow/shrink as NEVER and fill=false. This propagation of layout preferences has great power for the punch, enabling automatic resizing behavior on deeply nested hierarchies with little-to-no layoutInfo customization…..if…only….
…it weren’t for a bug in 1.3 (RT-8240) where the containers all report grow/shrink=SOMETIMES, fill=true, regardless of their content. This means that in 1.3, you’ll often see containers growing when their content doesn’t really want them to. The workaround is to set LayoutInfo as needed to adjust that behavior. Double sigh.
Following is the current set of Control defaults (derived from their skins), which we’ll certainly tune as we put more mileage on this feature:
| Class | hgrow | vgrow | hshrink | vshrink | hfill | vfill |
|---|---|---|---|---|---|---|
| Button | NEVER | NEVER | NEVER | NEVER | false | false |
| CheckBox | NEVER | NEVER | NEVER | NEVER | false | false |
| ChoiceBox | NEVER | NEVER | NEVER | NEVER | false | false |
| Hyperlink | NEVER | NEVER | NEVER | NEVER | false | false |
| Label | NEVER | NEVER | NEVER | NEVER | false | false |
| ListView | SOMETIMES | SOMETIMES | SOMETIMES | SOMETIMES | true | true |
| PasswordBox | NEVER | NEVER | NEVER | NEVER | false | false |
| ProgressBar | NEVER | NEVER | NEVER | NEVER | false | false |
| ProgressIndicator | NEVER | NEVER | NEVER | NEVER | false | false |
| RadioButton | NEVER | NEVER | NEVER | NEVER | false | false |
| ScrollBar | NEVER | NEVER | NEVER | NEVER | true* | true* |
| ScrollView | SOMETIMES | SOMETIMES | SOMETIMES | SOMETIMES | true | true |
| Separator | NEVER | NEVER | NEVER | NEVER | true* | true* |
| Slider | NEVER | NEVER | NEVER | NEVER | false | false |
| TextBox | SOMETIMES | SOMETIMES* | SOMETIMES | SOMETIMES* | true | true* |
| ToggleButton | NEVER | NEVER | NEVER | NEVER | false | false |
| Preview Controls: | ||||||
| CheckBoxMenuItem | NEVER | NEVER | NEVER | NEVER | false | false |
| Menu | NEVER | NEVER | NEVER | NEVER | false | false |
| MenuBar | SOMETIMES | NEVER | SOMETIMES | NEVER | true | false |
| MenuItem | NEVER | NEVER | NEVER | NEVER | false | false |
| MenuButton | NEVER | NEVER | NEVER | NEVER | false | false |
| PopupMenu | NEVER | NEVER | NEVER | NEVER | false | false |
| RadioMenuItem | NEVER | NEVER | NEVER | NEVER | false | false |
| SplitMenuButton | NEVER | NEVER | NEVER | NEVER | false | false |
| ToolBar | SOMETIMES* | SOMETIMES* | SOMETIMES* | SOMETIMES* | true* | true* |
| TreeView | SOMETIMES | SOMETIMES | SOMETIMES | SOMETIMES | true | true |
* = value depends on orientation
Sometimes a node just doesn’t want its parent messing with its size or position. In 1.2 you could use LayoutInfo to mark a node as ‘unmanaged’ to tell its parent Container not to factor it into layout and this concept was limited to Container parents. With 1.3’s universal auto-sizing, suddenly all parents (Group, Container, CustomNode) care about managing their children, so we’ve moved the variable from LayoutInfoBase up to Node:
Button {
managed: false // better than "layoutInfo: LayoutInfo { managed: false }"
layoutX: 10 layoutY: 10 // parent will not layout, so must position myself
width: 80 height: 45 // parent will not autosize, so must size myself
}
So if a node is unmanaged, its parent will not lay it out. If inside a Group, this simply means the node will not be auto-sized. If inside a Container, it means the node will not be factored into the container’s preferred size calculations and it will not set the node’s position (layoutX/Y) or size (width/height). So…
Another 1.3 affect of unmanaging a Parent node (Container, Group, CustomNode, or Control) is that it automatically becomes a layout root. This means that layout requests beneath it do not propagate above it and instead cause the layout root itself to be added to the scene’s dirty layout list, where it’s branch will be layed out on the next pulse. Before this change it was impossible to layout discrete branches of the scenegraph without laying out everything above it, which was a huge inefficiency. This change gave a nice boost to the performance of our controls where there are many cases that size changes within the skins do not affect the overall layout bounds of the control and thus shouldn’t trigger relayout.
Here’s a practical example to illustrate how you might utilize a layout root. Say you want to show a little ‘info’ graphic about a particular node. You want this graphic to share the coordinate space of the node so its position always remains relative to it, but you don’t want it factored into the node’s layout in any way — sort of like a popup layer. By making it an unmanaged child of the node, it won’t affect the node’s layout, but also size/layout changes within the info graphic will not trigger any re-layouts of the node itself.
The only catch is to remember the Rule above, which is that if the layout root happens to be a Resizable (Container or Control), then you must take responsibility to manage its size since its parent node will be ignoring it. If you don’t set its size then it’s like to have a size of 0 x 0 and won’t be visible. On the other hand, if the layout root happens to be a Group, then auto-sizing will ensure that it and its contents get resized automagically during layout.
Resizable nodes (Controls & Containers) generally know best how big they should be and they express this from the inside-out through their “preferred size”; as the node’s internal state changes (content, styling, etc), it’s preferred size usually also changes.
When a resizable node’s preferred size changes, rather than immediately resizing itself, it invokes requestLayout() to inform its parent that it wants to be resized on the next layout pass. The reason the resizing task falls to the parent (rather than the child itself) is because the parent may need to balance the use of real estate across its children and hence it needs to call the shots (if you have kids, you understand). And this is what we mean by “autosizing”; that is, the parent automatically sets the width and height of the child (usually to the child’s preferred size) during layout.
In 1.2, only Container parents performed autosizing, so if you placed your button or textbox inside a Group, CustomNode, or at the root of a Scene, and the text in the button changed, the button would not be automatically resized to accommodate the new text. This has lead to endless confusion because controls behaved differently depending on their parent type.
In 1.3, autosizing becomes universal across all Parent subclasses: Container, Group, CustomNode, and that secret parent hiding as the root of every Scene.
This change means you need to just learn one set of rules which apply consistently across the scene-graph:
Example:
Label {
layoutInfo: LayoutInfo { width: 100 }
}
And note that this applies only to Resizable children; non-Resizable nodes (shapes, Text, ImageView) are not affected by autosizing.
If for some reason you don’t want a Group to auto-size its resizable children, you can set autoSizeChildren to false, restoring the 1.2 behavior.
Universal autosizing puts huge pressure on all resizables to correctly report their preferred size, which leads to a second rule:
Internally after turning on universal autosizing, we discovered many bugs in our Control classes where preferred sizes weren’t being returned properly. So if your resizable node suddenly disappears in 1.3, check to make sure its preferred size isn’t 0×0!
In 1.2, prior to our CSS-stylable version of Controls, we were able to initialize Controls and Containers to their preferred size during initialization. This meant that if you then threw one of these guys into a Group, CustomNode, or Scene, it would appear with the correct initial size, even though subsequent changes to its preferred size wouldn’t update since those parents didn’t autosize in 1.2.
With 1.3’s introduction of CSS-stylable controls, we can no longer prime the size of controls during initialization. This is due to the hierarchical nature of CSS, where styles are applied lazily to nodes on the first scene pulse (which you can think of as the heartbeat that drives rendering of the scene-graph to the display), which means that Controls (and the parents that contain them) are oblivious to their preferred sizes at init time. Universal autosizing kicks in during that first layout pass (but after styles are applied) and ensures that everything is sized properly (just once) before you see it.
But what if you need to get the size of something before it’s displayed? This is certainly a valid need in some applications. Well, recall that layout happens regardless of whether a node is visible or not, so make it invisible, add it to a visible scene’s content, and it will be autosized and you can measure it. Not exactly obvious or elegant, but works until we create a better mechanism.
In the midst of celebrating JavaFX 1.3‘s performance improvements and CSS-stylable controls, let’s not forget about changes aimed to tame the beast we call “layout”. My JavaFX 1.2 layout blog covered the fundamentals of bridging our declarative, animating scene-graph with traditional GUI layout and here I’ll follow up with some layout guidelines and a series of blogs describing how the changes we made in 1.3 are expressly intended to make your life easier, even if at first, being human, you feel a little resistant to change.
Most of the core concepts in JavaFX layout (logical vs. visual bounds, resizable vs not, use of binding vs. containers) remain complex and there’s no getting around spending time to understand them (not unlike 2D or CSS). But the 1.3 API changes should make it easier to get better dynamic layout with less code and fuss and here I’ve rewritten my layout guidelines to take into account 1.3 and another year’s worth of experience in constructing apps:
doLayout function rather than a bunch of complex binding expressions.managed to visible.width/height vars directly, as all parent types now automatically set those values (usually to the resizable’s preferred size) during layout. If you need to directly control the size of a node, override it’s preferred size using LayoutInfo.layoutX: bind finalx - node.layoutBounds.minXlayoutY: bind finaly - node.layoutBounds.minY.layoutBounds as the basis of all layout-related calculations and understand that it may not correspond to the node’s visual bounds on the screen:
0,0 width x heightAnd finally, I’ve rewritten almost all of the layout related documentation in 1.3, so take a look and send me feedback. I’m still wrestling this beast to the ground.
* we reserve the right to change preview APIs when we move them into core and already anticipate changes to Grid to support more dynamic node placement.