UI
Core Observations
In many different UI layouts, items are placed side-by-side and then distributed and aligned in some manner. For this layout system, we have constructed some generic primitives that accomplish this and are used by all layout modes, to handle these common behaviors in a unified way.
These primitives are racks and lines, which are used by all layout modes to achieve their goals. A rack is a one-dimensional concept responsible for distributing and aligning a set of items into some available space on an axis. A line generalizes how to treat an entire row of boxes as a single rack item. This can happen when aligning the rows in a flex layout, but is most common in grid layout, where entire rows and columns are treated as single rack items.
Box
A box is a core unit of the UI. For purposes of layout, it is a rectangle, whose size and position are to be determined. Boxes furthermore have inner and outer buffer regions, which allow them to be spaced from one another.
Buffer
Buffer is an amount of space around boxes that will not have content placed into it. Any box can have both inner and outer buffer on all sides. Outer buffer can be used to space it away from other boxes, whereas inner buffer can provide a margin around its children and an area for any borders to be drawn in. Buffer collapses with adjacent buffers of siblings and parents. Buffer never collapses through box boundaries. Buffer never collapses through gap decorations. A box's inner buffer has the option to be a strong buffer. This means that arbitrarily large buffers can collapse into it. This is nice in cases where children might have large buffers to be used for spacing them away from other actual content, but the container's edges can be seen as the end of all content. The container's inner buffers are also far more concerned with giving an even spacing around children, to make the container look nice or to allow it to tightly wrap its child boxes regardless. A box's own inner buffers do not collapse with each other. This is because a box's inner buffer on a side is often what provides space for the box's border on that side to sit in. If the buffer collapses away, there might not be enough space for the border.
Gaps
A gap is a buffer inserted between items in a container. This includes gaps between things such as flex items and table rows and columns. It collapses with those item's outer buffers.
Directions
UI inherently exist in two dimensions, where it has a horizontal direction and a vertical direction. That said, for anything that will contain human text, it makes sense to conceptualize these as a primary and a secondary direction. English text has words going left to right, and if there is not enough space remaining, it wraps onto more lines, going downwards. We thus arrive at our terms for the X and Y axes: The placement direction and the wrapping direction. Every layout mode performs its layout according to these two directions.
And while in English, these always correspond to the horizontal and vertical axes in the same way, this is not necessarily the case in other languages, such as Arabic or Mongolian. Arabic goes right to left, whereas Mongolian places text top to bottom.
In a UI system, it makes sense for these differences to be represented, even by things that are not text. Think of a menu bar, where the various options are placed left to right, from the most important to the least important. If the language were arabic, it would make sense, for these menu items to go the other direction as well.
Thus, all layout modes operate on the placement and wrapping directions, which may map to the physical X and Y axes in various different ways:
Layout Modes
Layout modes very roughly follow this logic:
-
Distribute items via racks along the placement axis to determine their sizes in that direction.
-
Run layout for the children now that their sizes in the placement axis are determined.
-
Distribute items via racks in the wrapping direction.
Here, the preferred sizes of any boxes are substituted for the sizes obtained during their layout. The idea behind this is that a box should, ideally, in the absence of any stretching or size limits, perfectly wrap its contents.
Before laying out a box, it may be of indefinite size along one or both axes. This indicates that the box's own layout has infinite space available in these directions. After a box has been laid out, its size has been resolved to be definite on both axes. Note that at this point, the parent box might, during further distribution, squish the box back down to a smaller size. This will induce overflow.
Imagine an absolutely positioned box that will stretch to fill the entire screen, in a right-to-left, top-to-bottom layout. It first gets a width from the absolute layout. Then its own layout runs, filling that given width and an indefinite height, which might end up larger than the screen. Now the absolute layout squishes this box back down to the size of the screen, inducing overflow.
Root Layout
The root box is positioned at an origin. It's self alignment is used to determine how exactly it sits on the origin. This makes it possible to have boxes of indefinite size that are either centered on a point or grow into a certain direction. (or anything in-between, really)
Absolute Layout
Absolute layout exists to position boxes relative to their parent, without special alignment with regards to their siblings. Examples of this include centered modals, popups along the edges of a container and also draggable windows on a desktop.
Absolute layout is performed as follows:
-
Place each child box in its own rack along the placement direction. (These racks also include collapsed end buffers.)
-
Distribute all racks to determine their sizes. This also determines the placement axis sizes of all child boxes.
-
If the container does not have a placement axis size yet, its placement axis size is that of the largest rack.
-
Lay out all child boxes.
-
Place each child box in its own rack along the wrapping direction, using its laid-out size as the preferred size. (These racks also include collapsed end buffers.)
-
Distribute the wrapping direction racks to determine their sizes. This also determines the wrapping axis sizes of all child boxes.
-
If the container does not have a wrapping axis size yet, its wrapping axis size is that of the largest wrapping axis rack.
-
Clamp the container's size up to its min size.
-
Align all racks.
This means that the container will resolve indefinite sizes to be at least its min size and also always large enough to fit any item if that item were centered. Alignment may want to align items to be fully or partially outside of the container and this still allows for that. It also leaves enough space for those items to slide fully into view.
To determine an absolute container's min size on an axis from its children, calculate the pre-distribution min size for each child box's rack in the given axis and take the largest of those.
To determine an absolute container's preferred size on an axis from its children, calculate the pre-distribution preferred size for each child box's rack in the given axis and take the largest of those.
Flex Layout
Flex layout exists to lay out items into lines of content that may or may not be allowed to wrap. Examples of this include toolbars, lines of text, or stacks of messages in a chat history.
Flex layout is performed as follows:
-
Assign child boxes to flex rows. In a non-wrapping flex container, all children are placed in the same row. In a wrapping flex container, children are placed in rows at their preferred size (or min size if no preferred size is given) and placement wraps once placing a child would exceed the available size in the placement direction. Since gaps are only placed between items, so no gap is placed at the end of a row.
-
Create a rack for each row and use it to distribute the items in that row. This also determines the placement axis sizes of all child boxes.
-
Lay out all child boxes to determine their wrapping axis sizes. From now on, this size is used in place of the preferred size along the wrapping axis.
-
If this is a wrapping flex container: (TODO: Wrapping and non-wrapping flex containers may want to be identical by leaving the outer edges of the first and last line open.)
-
Create a rack that contains one line for each row. This rack also contains the wrapping axis gaps and inner buffers of the flex container.
-
Distribute and align that wrapping axis rack to position the rows.
-
-
Create a wrapping axis rack for each child box and use it to distribute and align that child box within its row. In a wrapping flex container, this rack includes the child's outer buffers, but they do not collapse with anything, since they are fenced off by the line that the item sits in. In a non-wrapping flex container, this instead works analogous to absolute layout.
To determine a flex container's min size in the placement direction from its children, calculate the pre-distribution min size of each row rack and take the largest of those. If it's a wrapping glex container, assume that it wrapped after every item.
To determine a flex container's min size in the wrapping direction from its children, calculate the pre-distribution min size of a rack that contains one line for all children.
To determine a flex container's preferred size in the placement direction from its children, calculate the pre-distribution preferred size of the row rack you would get if all items were placed in one row.
To determine a flex container's preferred size in the wrapping direction from its children, calculate the pre-distribution preferred size of a rack that contains one line for all children. TODO: Is this correct in a multiline (wrapping) flex container?
Grid Layout
Grid layout exists to position items in a grid-like arrangement. This is useful for tables and some general UI concepts.
Grid layout is performed as follows:
Note that any time that this algorithm 'assigns' sizes to colunms, it does not add them to the rack item immediately. It just makes note of them for later use.
-
Assign child boxes to the grid's cells. A box might span multiple cells, horizontally and vertically. TODO: How exactly?
-
Create a rack for the placement direction. This rack has one item for each column, and their properties are determined by a line that consists of each box within that column. Boxes that span multiple colunms are not part of the lines.
-
For each box that spans multiple columns:
- If the total min size of all spanned columns and the gaps between them is less than the spanning box's min size, then make note of how much size is missing and proportionally assign it to all columns in question, based on their current sizes. This means that if a box spans two columns with a size of 25 and 75 dots, respectively, then 25% of the missing space will be assigned to the slim column and the remaining 75% will be assigned to the wider column. Do not assign any additional min size to gaps.
-
For each column, if any min size increases were assigned to it, add the largest of them. If there were any other smaller min size increases assigned, re-run step 3 for the boxes that those smaller min size increases originated from. While doing this, column iteration order is carefully chosen to unify all desired min size increases in the least egregious way. To facilitate this, columns are iterated so that the columns with the largest available min size increases are processed first. Break ties using their second-largest available min size increase and so on. (A non-existant min size increase is smaller than one that does exist.) In doing so, if two spanning boxes need to increase a colunms size by different amounts, only the largest increase is taken, which allows the other spanning box to assign less additional space elsewhere. One would not want to prioritize smaller min size increases, because this forces boxes that already need a lot of extra min size to take even more elsewhere. Whereas allowing the demanding spanners to perform their size increases first, might make further size increases for less demanding spanners entirely unnecessary.
-
For each box that spans multiple columns:
-
If the total preferred size of all columns that it spans and the gaps between them is not equal to the spanning box's preferred size, note how much size is missing. In this step, if an item has no preferred size, use its min size in the sum instead. Also use its min size if it exceeds its preferred size. This models the actual amount of space with which a column will enter the rack distribution algorithm, i.e. its effective preferred size.
-
If that sum is less than the spanning box's preferred size:
- Compute the difference and proportionally assign it to all columns in question, based on their current sizes. Do not assign any preferred sizes to gaps or items that already have a set preferred size. If this means that there is no items to assign this preferred size to, do not assign it at all.
-
-
For each column, if any preferred sizes were assigned to it, determine which preferred size to use as the preferred size of that column, using rules analogous to those for min size increases from step 4. This means that each set of spanned columns will prefer to be wide enough to hold its spanners at their preferred size, while also minimizing the total preferred size of all columns.
-
Each box's placement axis size is that of the column it sits in. If it spans multiple columns, it is the sum of their sizes plus any gaps between them.
-
Lay out all child boxes.
-
Re-run steps 2 through 7 for the wrapping direction, using the child boxes' laid-out sizes as their preferred sizes. This determines the wrapping axis size of all boxes.
-
Align all row and column racks.
-
Create racks on both axes for each child box and use them to distribute and align all child boxes within their rows and columns. These rack include the children's outer buffers, but they do not collapse with anything.
TODO: How to calculate min/preferred sizes from a grid's content?
Rack
A rack is a set of items in a row that want to be placed next to one another. A rack is one-dimensional. There is two operations that can be done on a rack:
-
Distributing the rack In this, the rack's items' sizes are determined by placing and stretching them towards the rack's target size. They are not assigned any positions, just sizes. Items are not stretched if they have a stretch priority of 0 or if the rack's target size is indefinite.
-
Aligning the rack In this, the distributed rack is aligned inside of its parent, by placing the rack's self alignment point on the rack's parent alignment point
A rack has: - a list of rack items - a target size (dots or indefinite) - a self alignment (number in the range -1 to 1) - a parent alignment (number in the range -1 to 1)
A rack item consists of: - a minimum size (dots) - a preferred size (dots or indefinite) - a maximum size (dots or indefinite)
Line
Some layouts employ lines, which are a rack item that contains multiple boxes, perpendicular to the rack's direction.
A line's min size is the largest min size found among the boxes within it. (so that it is at least large enough to contain all of them.) If there is no items within the line, it's min size is 0. For purposes of this determination, a box's min size includes its outer buffers. A line does not itself have any outer buffers. Lines should generally be spaced by using gaps between them.
A line's preferred size is the largest preferred size found among the boxes within it. (so that it can comfortably wrap all of them.) If no item within it has a preferred size, the line has no preferred size. For purposes of this determination, a box's preferred size also includes its outer buffers. This might seem unintuitive for grids, where one would imagine all items to be the same size, but grids where items are of different sizes are desireable. One example would be a grid of images with different aspect ratios.