Layouts
Nova's Layout System is responsible for determining the size and position of every active UIBlock in the scene and shares a lot in common with other modern UI frameworks and design tools (e.g. CSS Box Model), supporting a wide range of features including (but not limited to):
Feature | Value |
---|---|
Edge-based Positioning | Offset content from the edge of its parent. |
Fixed and Relative Properties | Dynamically resize/reposition elements as their parent size changes, while preserving the size/position of others. |
Padding & Margin | Apply space around elements in a more flexible manner than a pure size + position model. |
Min/Max Properties | Automatically clamp values to functional ranges without needing to write custom scripts. |
AutoSize | Configure a UIBlock to Expand to its parent's size or Shrink to the size of its children. |
AutoLayout | Automatically position all children of given parent along a selected axis using a specified Alignment and Spacing. |
Layout Engine
At the core of Nova's Layout System is the highly-optimized Layout Engine, which was built from the ground-up for Unity's Burst Compiler and Job System to efficiently perform two key tasks:
- Process the Layout configurations of UIBlocks and their dependencies to determine the calculated size and position used for rendering and input.
- Keep the Layout properties and Transforms of UIBlocks in sync.
The Layout Engine runs once at the end of each frame and consists of three phases:
Phase | Summary | Description |
---|---|---|
1 | Read Transform Positions | Convert modified Transform positions into layout positions. |
2 | Calculate Layout Properties | Process modified Layout and AutoLayout properties to determine where and how big or small a given UIBlock should be rendered. |
3 | Write Transform Positions | Apply the position calculated in Phase 2 to each updated Transform, while honoring any preservable position overrides detected in Phase 1. |
After the Layout Engine runs, the Layout properties and Transforms of all active UIBlocks will be in sync, and the calculated values for all layout properties can be accessed via their respective CalculatedX
properties (e.g. CalculatedSize, CalculatedPosition, CalculatedMargin, etc.).
In-line Updates
In some situations, you may need the up-to-date Transform positions or calculated layout properties before the Layout Engine has run for the given frame. Two common scenarios where this may occur are:
- The frame when a UIBlock's GameObject is set active (the Layout Engine will not have processed the just-activated UIBlock yet).
- Multiple layout properties which affect each other (perhaps across UIBlocks) have been modified on the same frame.
For these cases, Nova provides the uiBlock.CalculateLayout()
method which will perform an immediate layout update for the target UIBlock, accounting for all of its modified layout dependencies in the process.
// Modify the parent's padding
uiBlock.Parent.Padding.Left.Value = 5;
// Set the uiBlock width to be 50% of its parent's padded width
uiBlock.Size.X.Percent = 0.5f;
// Set horizontal Alignment and Position
uiBlock.Alignment.X = HorizontalAlignment.Left;
uiBlock.Position.X.Percent = 0.25f;
// Crunch the modified values
uiBlock.CalculateLayout();
// Now both the transform.localPosition and uiBlock.CalculatedSize
// will be in sync with the newly applied layout properties, and we
// could then use them to construct a local Bounds of the target uiBlock.
Bounds localBounds = new Bounds(uiBlock.transform.localPosition, uiBlock.CalculatedSize.Value);
Note
CalculateLayout()
always overwrites transform.localPosition
with the calculated layout position. See Synchronizing With Transforms to learn more.
UIBlock Positioning
Positioning content through Nova's Layout System differs from typical Transform positioning in that every UIBlock exposes an Alignment property, which defines a coordinate space for its Position relative to its Parent.
In doing so, the Layout System makes it quite easy to lock a child UIBlock at a fixed or relative offset from an edge of its Parent, even as its Size or its Parent's Size changes.
The following diagram on the right illustrates how changing horizontal Alignment impacts the interpretation of UIBlock.Position.X
(which behaves analogously on the Y
and Z
axes):
Horizontal Alignment | Position |
---|---|
Left | Distance between left edge of parent and left edge of child. |
Center | Distance between center of parent and center of child. |
Right | Distance between right edge of parent and right edge of child. Note that in this case, a positive position moves the child to the left while a negative position moves the child to the right. |
Note
A UIBlock is positioned based on its LayoutSize and relative to its Parent's PaddedSize.
Synchronizing With Transforms
While the Layout System can typically honor positions assigned through a UIBlock's Transform directly (e.g. position
and localPosition
), there are a few situations where the Layout Position takes priority and will overwrite
values assigned through the Transform. For this reason, general guidance for positioning Nova content via script or in an Animation Clip is as follows:
- When attempting to move UIBlock content within a UIBlock hierarchy, use
UIBlock.Position
. - When attempting to move UIBlock content in the world, make your
UIBlock.Root
a child of a non-UIBlock object, and move that non-UIBlock object instead.
Following the guidance above is the easiest and most performant way to avoid any pitfalls or nuances when trying to use transform.localPosition
and uiBlock.Position
completely interchangeably.
NOTE
When uiBlock.Alignment
is set to Center
along a given axis, writing to UIBlock.Position
will match the behavior of writing to Transform.localPosition
.
// Align to center on all axes
uiBlock.Alignment = Alignment.Center;
// Assign a world position by converting it into a local position
uiBlock.Position = uiBlock.transform.parent.InverseTransformPoint(worldPosition);
Alternatively, if either modifying uiBlock.Alignment
or setting uiBlock.Position
to Value
-type Lengths
are out of the question,
uiBlock.TrySetWorldPosition(worldPosition)
and uiBlock.TrySetLocalPosition(localPosition)
will convert the provided transform-space position into its equivalent uiBlock.Position
while preserving the LengthType
and Alignment
. uiBlock.gameObject.activeInHierarchy
must be true
for this to work, otherwise both methods will exit early and return false
.
if (uiBlock.TrySetWorldPosition(worldPosition))
{
Debug.Log($"Converted position: {worldPosition} to Layout Position: {uiBlock.Position}!");
}
Limitations with Using Transform Position
With all that in mind, should you still prefer to use transform.position
/transform.localPosition
as much as possible and only modify uiBlock.Position
when absolutely necessary, here are the cases where writing to transform.position
/transform.localPosition
will not behave as expected because uiBlock.Position
will be used instead:
Scenario | Notes |
---|---|
Every first frame a UIBlock's GameObject is set active |
|
When a UIBlock's Parent's AutoLayout is enabled |
|
When a UIBlock is the direct child of a GridView |
|
Padding and Margin
Beyond setting just Size, Nova also allows you to specify a Padding and Margin for every UIBlock, which can have a wide variety of effects, depending on the scenario:
Scenario | Effects |
---|---|
AutoSizing | When AutoSizing, both Padding and Margin are factored in to the final CalculatedSize of the UIBlock being AutoSized, depending on the configuration: |
Non-Centered Alignment | UIBlocks that are edge-aligned (left, right, top, bottom, front, and/or back) will be: |
AutoLayout | Padding can be used to modify the area in which a UIBlock will AutoLayout its children. Margin on the children is used to adjust the space between themselves and their adjacent siblings. |
Note
Unlike CSS, Margin in Nova is purely additive for adjacent UIBlocks in a shared AutoLayout and does not collapse.