Layout
Layout is probably the most difficult topic in any UI system. It's not only how the API gets used, but it is often a mindset about how to approach the problem at hand. Sometimes you get lucky and the design you are implementing is simple enough that built in layout components can mostly get you there, but in almost every design I've ever personally worked with, I needed to get my hands dirty and get creative with how the layout was implemented.
The goal of this document is to orient you with 'how-to-think-about-layout' in Evolve.
The basics
We'll start relatively simple. We have two cases: template and typography. Both cases follow the same default rule:
they size themselves according to their content. If there is no content, the size is 0 by default.
So what is 'content'? This can be text content in the case of typogrophy, padding, and/or other child elements.
The box model
Evolve uses the standard CSS border-box model.
+-----------------------------+
| Margin |
| +---------------------+ |
| | Border | |
| | +---------------+ | |
| | | Padding | | |
| | | +---------+ | | |
| | | | Content | | | |
| | | +---------+ | | |
| | +---------------+ | |
| +---------------------+ |
+-----------------------------+
- Margin -- Space between sibling elements in
- Padding -- Space between the edge of element's border and it's content
- Border -- Space between the theoretical edge of the element and it's padding
- Content -- All the things inside the element, ie text, images, other elements
Templates
Templates will default to their content size on both the width and the height axis, and will arrange their children vertically. A side effect of defaulting to content size is that by default, with no content, an element's size is 0 on both width and height.
template Element; // no content, no style means these have no size unless they have content.
template Basics {
// 3 elements with no size means the parent has no size.
render {
Element();
Element();
Element();
}
}
If we were to render this template, we'd see nothing. Let's give those some sizes:
style big {
PreferredSize = 300px;
BackgroundColor = red;
}
style small {
PreferredSize = 100px;
BackgroundColor = blue;
}
template Element; // no content, no style means these have no size unless they have content.
template Basics {
// 3 elements with no size means the parent has no size.
render {
Element(style = [@small]);
Element(style = [@big]);
Element(style = [@small]);
}
}
This would render the boxes vertically with their respective sizes, so the total width of Basics would be 300px,
and the total height would be 500px
We could change this to a horizontal layout like this:
// the <> syntax matches based on an element name
style <Basics> {
LayoutType = Horizontal;
}
style big {
PreferredSize = 300px;
}
style small {
PreferredSize = 100px;
}
template Element;
template Basics {
render {
Element(style = [@small]);
Element(style = [@big]);
Element(style = [@small]);
}
}
This would render the boxes vertically with their respective sizes, so the total width of Basics would be 500px,
and the total width would be 300px
Typography Size and Wrapping
Typography actually follows the exact same rules, except that usually when we think about typography elements, we assume
they contain some text. So text is content, which means that a typography element will default to be as big as its text.
This also means that because the text layout is greedy, unless a text is given a width or some explicit new lines, it will not wrap by default.
In order for text to wrap, it needs to be given a non-content sized width, or it needs to contain new lines and have
it's LineBreakModeset to NewLinesOnly.
Sizes
Evolve uses a handful of measurement types to measure elements, paddings, offsets, alignments, etc. The most important ones are UIMeasurement for sizing elements and UISpaceSize for paddings and margins.
Then to actually set an element's size, we use the style properties
- PreferredWidth
- PreferredHeight
- MinWidth
- MinHeight
- MaxWidth
- MaxHeight
We also have the shorthands which set both width and height axes at the same time
- PreferredSize
- MinSize
- MaxSize
We use Preferred width/height because the layout system has some ways of increasing/reducing the size of elements
based on the algorithm used, ie if the PreferredWidth is 300px but the MaxWidth is 200px, the element will clamp
to it's max size, disregarding the PreferredWidth value.
See the docs on style properties for more details.
Stretching
A key concept of Evolve's layout system that is not typical of other layout systems you may have seen is stretch.
Any axis of content, padding, or margin can stretch. This is a little similar to FlexBox's FlexGrow but is implemented
differently.
Stretch is divided into parts. We can have as many stretch parts as we want and each is treated equally. This makes more sense with an example. Let's say we want a layout that pushes all the items towards the right.
style <MyLayout> {
PreferredSize = 500px 1cnt; // 500px means we
[attr:stretch = "left"] {
PaddingLeft = 1s;
}
}
style <Box> {
// total layout size is 100px (80px for content and 10px on each side for margin)
PreferredSize = 80px;
BackgroundColor = yellow;
Margin = 10px;
}
template Box;
template MyLayout {
render {
Box();
Box();
Box();
}
}
Algorithms
Evolve comes with a small handful of layouts you can apply. This will be a deep outline of each of them. They all begin
the same way: we first try to resolve any sizes that are computable at layout start time into resolved pixel values.
This would be values such em, vw, vh, aw, ah. We do this for all axes on padding, border, margin, and content.
Once we've resolved those fixed sizes we need to detect if a layout paradox exists and remove it. This can happen when a parent element wants to take on the size of its children, and the child wants to be as big as its parent. This is an unsolvable paradox, so we take the approach of making the child have a fixed size of 0. THIS IS A BIG SOURCE OF ISSUES IN LAYOUTS. If a size goes to zero, and you aren't sure why, its most likely because of this paradox solving so be sure to check that you don't have elements that are sized this way.
Next we apply any margin collapses. Margin collapse is enabled with the properties CollapseSpaceHorizontal and
CollapseSpaceVertical. These are used to basically remove extra space such that the parent's padding and a child's
margin collapse into the greater of those. This can also be used to merge margins of child elements in the same way.
Once all of these up-front values are resolved we can start to actually perform layout. The layout algorithm is multi-pass but only when needed. We first perform layout of parents and then children. This (for reasons like stretching) might or might not fully resolve the size of the parents. If the parent size is not fully resolved, we continue another layout pass but this time we go bottom to top, i.e. leaves to roots. This process repeats until the layout is fully solved. As elements become fully laid out, they are removed from the tree so any subsequent passes will not waste work on them.
Horizontal and Vertical
Vertical is the default LayoutType. It simply arranges elements from top to bottom vertically. Similarly, Horizontal
lays out elements in exactly the same way, just horizontally. Any property mentioned here is applicable to both.
Horizontal and Vertical Wrap
These work similarly to standard Horizontal and Vertical but they also support wrapping. Instead of children continuing
to be laid out outside the boundaries, elements will wrap, creating a new row or column (depending on HorizontalWrap
or VerticalWrap) in which subsequent children will be arranged in.
Stack
A stack is probably the simplest layout, it arranges elements such that they all stack on top of each other, there is vertical or horizontal offsets other than the child's own margins. This basically gives you a canvas to draw elements on where an element's top and left margins are their offsets from the parent. Very useful but less frequently used.
Grid
Grids are intended for 2D layouts. Basically you can define a grid on and then slot elements into it. You can either let it fill in elements automatically or specify where certain elements should fit in the grid and how many rows and columns they should span.