Hi everyone,

TL;DR -- let Node implement a Layoutable interface so it can participate
in virtual layouts that do not require heavy-weight scene graph layout
container nodes.

In the past, I've fixed quite a few layout bugs related to duplicated
layout calculation code in Skins.  Skins often duplicate layout code
because they want to stay as light-weight as possible, and using layout
containers within their structure is counter to that goal.  A Label
could have used an HBox(graphic, text) internal structure, but to avoid
the overhead of using a complete scene graph Node, it instead does its
own graphic and text positioning.  This is however far more complicated
than it seems. The graphic can be full fledged Node, with its own
min/pref/max sizes, a content bias, etc.  When Label duplicates the
calculations that basically HBox does that creates two problems:

- Any bugs fixed in HBox won't fix Skins that use or are supposed to use
an equivalent horizontal layout (many skins have or can have horizontal
layouts: Labels, ComboBox, Slider, Spinner, TitledPane)
- The duplicated calculations often make assumptions and take short-cuts
(ie. they assume a fixed size, calling only prefWidth(-1) and often
ignore things like content bias).

*Virtual layout containers*

At some point on one of these fix PR's, we discussed perhaps allowing
the layout calculations of the major layout containers to be made
re-usable.  In other words, a Label skin and other skins or controls
could use the layout calculation of HBox but without creating an HBox in
the scene graph.  

This could work by having the Label skin create an *HBoxLayout* -- a
virtual layout container which is given the Text and Graphic nodes.  The
children are also added as direct children of Label (just like they are
now).  This creates two trees: the standard scene graph (unchanged)
where Text and Graphic are direct children of Label; and a virtual
layout tree, where an HBoxLayout is referenced by Label which in turn
references the Text and Graphic nodes:

The solid lines indicate the SceneGraph relation, and the dashed lines
are the virtual layout.

A more complicated label that has both a title and subtitle would look
like this:

*Introducing the Layoutable interface*

From the above you may have noticed that the HBoxLayout can contain both
Nodes and other virtual layout containers.  As the layout containers are
supposed to be light-weight, these won't be Nodes. So in order to be
able to compose virtual layouts, both Nodes and virtual layout
containers need to have some kind of common ancestor.  This would be
the *Layoutable* interface.  It contains basically a subset of methods
that Node already has, all related to layout calculations:

The measuring methods:

doubleminWidth(doubleheight);

doubleprefWidth(doubleheight);

doublemaxWidth(doubleheight);

doubleminHeight(doublewidth);

doubleprefHeight(doublewidth);

doublemaxHeight(doublewidth);

booleanisResizable();

doublegetBaselineOffset();

Orientation getContentBias();

The positioning query methods (used primarily for base line calculations
and testing):

doublegetLayoutX();

doublegetLayoutY();

Bounds getLayoutBounds();

The positioning setting methods (used by layout containers to place
nodes or other virtual containers):

voidresizeRelocate(doublex, doubley, doublew, doubleh);

voidrelocate(doublex, doubley);

These are the basic methods that the interface would need (or split over
several interfaces, the measurement methods can for example be in a
Measurable interface).  To support constraints, the interface would also
need to expose `getProperties`, however, I suggest abstracting this by
introducing two new methods:

<T> T getConstraint(ConstraintKey<T> key, T defaultValue);

<T> voidsetConstraint(ConstraintKey<T> key, T value);

Where ConstraintKey is a simple immutable key helper class that layout
containers can create for easy type safe access:

staticfinalConstraintKey<Priority> CHILD_GROW=
ConstraintKey.of(Priority.class)

These can still be stored in the properties map as usual, so their
implementation is trivial.  I think however this is nicer than exposing
the properties map in this interface.

The above interface would be implemented by Node and by any virtual
layout container.  Layouts like HBoxLayout would then accept a list
of *Layoutable* children, which can be either Nodes or nested virtual
layouts.  

There is still one missing piece to being able to fully virtualize
layouts like this: we need to know how screen scaling and snapping is
set up in order to position nodes that take part of a virtual layout
according to the wishes of the owner of the virtual layout (for example
Label).

*The LayoutContext interface*

This interface provides general information related to rendering pixels
to the screen; this information can often be shared amongst all virtual
containers in a virtual layout tree. It contains the following methods:

doublegetSnapScaleX();

doublegetSnapScaleY();

booleanisSnapToPixel();

These methods are used by a virtual layout container to do correct
positioning according to the wishes of the owner of the virtual layout. 
So in our Label example, the Label would provide this scope matching its
own snap settings.  All virtual containers it creates use this
information when calculating sizes and positions.

The above methods are not new either; they are currently provided by
Region as protected methods (and so they are public API already), aside
from `isSnapToPixel` which is already public.  My proposal would then be
to simply have Region implement this interface, promoting the two snap
scale methods to public.  Alternatively, we could name them
`getRenderScaleX` and `getRenderScaleY` as that's basically what they
are (Region gets this from Window directly).

*Result*

If Node implements Layoutable, and Region implements LayoutContext, then
we have all the things we need to create a virtual layout container.  A
LabelSkin can provide the Label itself as the LayoutContext, and the
Node children that should participate as its Layoutables:

- LabelSkin creates a HBoxLayout container passing itself (as
LayoutContext) and the two children the layout should position (Graphic
and Text)
- It also still adds the two Node children as its children in the scene
graph (unchanged)
- In its layoutChildren callback it simply does:
hboxLayout.resizeRelocate(x, y, w, h)

When resizeRelocate is called, the virtual layout container will:

- Call computeMin/Pref/Max methods which cascade as usual down the
(virtual) tree (which will end up at real Nodes, like Text and Graphic)
- Use appropriate snapping by delegating this to the LayoutContext
provided (using Label's settings effectively)
- Positioning and resizing each Layoutable in its virtual tree which
ends up positioning also the real Nodes

The algorithm here can then be shared by HBox. HBox would be stripped of
all its layout code, leaving roughly only a set of property
definitions.  It would then internally create the light-weight
HBoxLayout as well (much lighter than a full Node) and delegate the
relevant properties like alignment/spacing to the virtual layout, as
well as its measurement methods (like minSize, prefSize, etc).  Its
layoutChildren would similarly simply call `resizeRelocate` to apply the
layout.

*Bonus*

Generally, the leafs of a virtual layout tree are Nodes, but they don't
strictly have to be.

This opens up a few interesting use cases.  A spacer needed in a virtual
layout doesn't have to be a Node; it can simply be a Layoutable with the
desired metrics, with a no-op resizeRelocate.  The space would be
reserved, but no actual Node needs to occupy it.

It can also open other use cases, like having a Canvas display boxes
that are positioned via standard layout algorithms that come with
JavaFX.  A leaf Layoutable would then simple draw some primitive on a
Canvas in its resizeRelocate method.

Let me know what you think,

I've got a proof of concept working for all this where I've implemented
HBoxLayout.  The missing to make virtual layouts a lot more integrated
is having Node being able to participate without having to wrap it.

--John

Reply via email to