How Clay's UI Layout Algorithm Works

Contenido del Video OriginalExpandir Video
  • I maintain an open-source UI layout library called clay.
  • Clay allows you to create user interfaces that adapt to changes in screen size or contents.
  • The core layout algorithm consists of only three functions with about 800 lines of code.
  • Building a UI layout algorithm can be simplified with a few key insights.
  • The position of an element is naturally relative to the container it's inside.
  • Elements can grow or shrink based on the available space in their containers.

I maintain an open-source UI layout library called clay. It allows you to create user interfaces that can adapt to changes in screen size or contents. One of the things about clay that people tend to find surprising is that the code that actually computes the layout is only three functions for a total of about 800 lines of code.

I think it's because the expectation is that to provide a robust solution for UI layout must be extremely complicated. Perhaps you've read a paper on constraint solvers, looked at the source code of some other UI layout libraries, or just generally been intimidated by reading comments online that seem to imply that it's beyond the reach of the average programmer.

But just because someone explains something in a complicated way doesn't mean it actually is complicated. In my experience, the tricky part of building a UI layout algorithm is that there are these ideas that seem good on the surface, but when you actually implement them or try them out with a complex layout, they end up having these unfixable edge cases or doing a huge amount of redundant work.

UI layout tends to have these long tunnels that lead to dead ends. But I've found that with just a couple of simple insights, you can short circuit almost all of that potential pain and end up building something really simple and robust.

Say I wanted to build a simple drop-down menu like this in my application. The simplest approach, of course, is just to draw all the components separately with their exact position and size on the screen. In terms of our implementation, this might just look like a series of individual function calls to draw the background rectangle, then draw each menu item box one by one, then draw all the text and then draw all the icons.

This will work fine as long as the drop-down always appears in the same place in the application and my application window itself is always the same dimensions. But say instead of a menu drop-down, I want to render a context menu that appears when I right-click somewhere. Our initial approach has a problem because I've hardcoded the position of all of these elements. This code isn't reusable.

We could make a function that requires you specify the positions of every single sub-element in the dropdown, but that would be kind of ridiculous. If we imagine ourselves as a user of this function, our expectation mentally is that we would provide a single target position that might correspond to the top left corner of where we want our menu to render, and the contents of our drop-down would be rendered relative to that single position.

And that is the first, and potentially the most important insight in UI programming, which is that the position of an element is naturally relative to the container that it's inside. So for a simple solution to our problem, instead of hardcoding the position of this text that says "delete", we can treat the label's position as relative to the menu item it's inside, the menu item relative to the dropdown that it's inside, and finally the outer drop down relative to the top left corner of the screen.

You might already know this, but it's worth mentioning that most computer graphics systems treat the top left corner of the screen as the origin where X and Y are zero, with Y increasing as it goes down, which unfortunately is the opposite of what we use for graphs in mathematics.

We can now draw our context menu wherever we want on the screen by passing in the position of the top left corner as an argument. But now we realize that this contextual dropdown might have different menu items depending on what it is you're right-clicking on. If we right-click on an image, we get image-related options like editing and filters, and if we right-click on text, we get text-related options like spell check and dictionary research.

This brings us to the second insight, which is that just as elements are positioned relative to the container they are inside, the size of the container depends on the contents. There's this interesting bi-directional dependency here. In order to calculate an element's position, we need to know the position of the container that it's inside, and in order to calculate an element's size, we need to know the sizes of potentially multiple elements that are nested inside it.

The idea of these recursively nested objects that reference one object above them and multiple objects below is probably ringing some bells in your head if you've ever done some data structure work before because what I'm describing in computing terms is called a tree. Conceptually, a tree is built out of a type of self-referencing object that has a single reference to its parent and multiple references to children, just like our UI does.

And it's very clear how that relationship looks when we visualize it, and indeed why the data structure is called a tree in the first place. Our tree starts with a single node that has no parent called the root node. The root node can have multiple children, which may or may not have children themselves, and eventually we reach a node that has a parent and no children, which we call a leaf node.

Now, data structures can seem a little abstract, so let's see how we can map the UI elements of our context menu onto this tree structure. We start with our background rectangle, the outer dropdown at the root of the tree. Then, as direct children of our dropdown, we have each of our menu items, and then deeper still we have our text labels and icons as children of our menu items.

Clay provides an API that allows you to easily construct these trees, which we refer to as UI hierarchies. You create individual UI elements by calling the clay macro in parentheses. You can configure your elements, so we'll tell our outer drop down to lay out its children from top to bottom, give it a purple background color, and then in the braces following that, you can create more elements that are automatically added as children in the internal tree structure.

We're just doing a simple loop through our menu item data array and creating our outer menu items. We can then create our text labels and our icons inside our menu items. This is very similar to other UI declaration languages like HTML. But if you're trying to figure out how a tree like this turns into a correctly positioned flexible piece of UI like this drop down, it might seem a little bit like magic, but it's surprisingly easy to understand, and I'm going to show you how it works step by step.

Let's start with the simplest possible UI we can represent in clay, which is just a colored rectangle with fixed dimensions. Say 960 pixels wide by 540 high. To do this in clay, we just tell clay that for both width and height, it should use a fixed sizing and provide a value in pixels. Internally, the representation for our hierarchy is extremely simple. Each of our layout elements is represented by a struct which has a size, a position, an array of other UI elements as children, as well as some visual data like the background color.

So to draw our simple UI to the screen, we just take the data from that one element and call our draw rectangle function with the fixed dimensions at position 0,0, which is the top left corner of the screen. Let's do something slightly more complicated. Let's give this element a child element, another fixed size rectangle, and we'll pick pink this time.

So the first UI element gets created at the root of our tree, and then our second element is added as a child of that first element. To draw our UI, we can use a similar approach to our first use case; we just start at our root node, draw its rectangle, and then continue on to the child nodes. This child also has fixed dimensions, so we draw our second rectangle the same way, just remember that the child position is relative, so we need to add it to the parent position, and we're done.

Now, perhaps we don't want our child rectangle to be right up against the edges of our parent rectangle, so we can use the padding configuration option. Padding inserts a gap between the edges of a parent element and the edges of its child elements. The way that we draw our root node is exactly the same; however, when we get to our child node, we don't draw it at position 0, 0 anymore. We add the parent's left padding to the X position and the top padding to the Y position, and we can see that this is correct—offset our child rectangle into the parent.

Let's crank it up a notch. I'm going to add a second child element to my root node—another fixed-size rectangle. Here's where we start to see some auto layout features emerging. By default, clay lays out its child elements from left to right so they don't overlap. This time when we're iterating through our child elements, we keep track of a left offset. The left offset starts off as the parent left padding, and then after we draw the first rectangle, we increment the left offset by its width.

So by the time we draw the second rectangle, it'll be drawn just to the right of the first. Of course, it's quite straightforward to also handle the other possible layout direction, which is top to bottom. The code is exactly the same logically; we just swap out our data. We have a Y offset instead, which starts as the top padding, and we add the child elements' height to the offset after we draw the rectangle.

I like that our outer rectangle has some padding, and I'd like there to be some space between my child elements as well. The way we handle this in clay is by using the child gap property. Child gap automatically inserts a gap between child elements in the direction of the layout, so a horizontal gap for left to right or a vertical gap for top to bottom.

The way that we implement this doesn’t require any new machinery at all. When we're iterating through our children, after each child, when we increment our offset by the child width or height, we also add the child gap to the offset. The code is exactly the same for both directions.

The next thing for us to address is the fact that our outer rectangle has all this extra space at the right and bottom. This isn't because of padding; it's because the outer rectangle has a fixed width and height that are larger than the contents. We could experiment until we found an exact width and height to use, but I'd much rather let the computer do it for me. In clay, that's supported by the fit sizing mode, which in this case will tightly fit our outer rectangle to the dimensions of its children plus any padding and gaps—and this is actually the default sizing mode in clay, so I could just omit it entirely.

This is a bit more tricky for us to figure out how to program. We've got a bit of a chicken and egg problem here. If we start from the top of the tree as usual when we draw our root rectangle, we don't know how large it's supposed to be, and our contents might overflow or we might overestimate the size of the contents. But if we start from the bottom of the tree and draw the children first, we can measure how large they are, but then if we draw the root rectangle, it'll get drawn over the top and obscure the children.

This property means it's physically impossible for us to lay out the UI and draw the UI in a single pass. We're going to need to split our code into two passes over the tree—one to calculate the layout and one to draw the layout. It might not seem like much, but mentally acknowledging that handling UI requires multiple passes over the data is an incredibly important realization.

Let's write the first pass to calculate our layout. If we were looking at our original drop-down menu, in order to know the size of our root rectangle that everything else is inside, we need to know the sizes of its children, the menu items. And in order to know the size of the menu items, I need to know the sizes of the children of the menu items, and so on. Essentially, what we're saying is we can only calculate the size of an element once we know the sizes of all of its children, which means the only place we can even start sizing is these leaf nodes that have no children and make our way back upwards.

What we're describing here is traversing our tree in reverse breadth-first order. Now I know that might seem complicated and intimidating, but there's actually an extremely simple solution hiding in the code that we already have. If we look at the example layout and how we declared it, it might not be immediately obvious how this clay macro is constructing our layout tree, but all the clay macro actually does is call open element before the children and close element after the children. It's literally just for convenience; it opens a scope for you and makes sure you don't forget to call close element again—that's it.

If we swap out the macros of the other two child rectangles for direct open and close calls as well, you'll see that both of them just get closed immediately because they have no children. The reason that I'm showing you this internal implementation detail is because believe it or not, the close element function naturally gets called in reverse breadth-first order, which is exactly what we need to do our sizing calculations.

Let's go through our tree and watch how it works. We open our dark blue root rectangle first. Because we haven't specified any fixed size, it starts off as 0 by 0. Next up, we call open again to add our first child. Because our first child rectangle has no children, we immediately call the close element function, and you can see how this directly corresponds to the reverse breadth-first approach we were talking about before. An element's close function will only be called after all of its children have closed.

Inside this function, we take the dimensions of the current element, which in this case we know because they're provided as fixed dimensions, and we add them to our parents' dimensions. So now our parent fit container actually has some size. We do the same thing when our second child closes, adding its dimensions to the parent as well.

This doesn't look quite right; the width is okay, but the height looks wrong, and you might already be able to intuitively see why this is the case. We're calculating the dimensions of our parent rectangle by adding up the dimensions of the children. When we're laying out our children left to right, that makes perfect sense for our width, but for our height it doesn't.

Let's look at our child rectangles one dimension at a time in the same direction as the layout, that is left to right. They don't overlap at all, so we can add up their widths. But if we look at them along the Y-axis, we can see that they're overlapping each other. This view makes it obvious that in the non-layout direction, the size of the parent should be based on the largest child rather than the sum of the children.

If we boil this down to a logical approach, we can implement our size along the layout axis as the sum of the children's sizes, whereas across the layout axis the parent size is the maximum of the children; that is, the size of the largest child. We can see that if we change our layout direction to top to bottom, we can apply exactly the same strategy: we add up the sizes along the layout axis, which in this case is the heights, and we get the maximum of the sizes of the children across the layout axis, which in this case is the width.

So if we return to our layout, we can see how we need to change our size calculation in the close element depending on the layout direction of the parent. We either add to the parent size or we take the maximum of the parent size and the current element size. Okay, our height is looking a lot better, but arriving at the end of the layout, we're still not quite correct, and that's because we've forgotten to include our padding as well as our child gap in the size of our root element.

We can extend our close element function so that when we add the element size to the parent, we also make sure to include the element's padding, which is left and right padding for the width and the top and bottom padding for the height. Our container height is now looking perfect; it's just our width that is slightly wrong because we're missing the gap between the children.

The way that we calculate our child gap is actually very simple and is related to something called the fence post problem. If we have a fence that has three posts, we only need two connecting sections of wood in between them. Four posts give us three in-between sections, five posts give us four in-between sections, and with just a single post, we have no in-between sections at all.

In the same way, if we have five elements laid out left to right, those five elements will only have four gaps between them. As a result, rather than having to add up the gaps, we can just calculate the total size taken up by the gaps as the child gap value multiplied by the length of the children minus one. This is trivial for us to implement with a slight complication being that we only add child gap to the direction of the layout, so width if we're going left to right or height if we're going top to bottom.

We can now finish closing our root blue rectangle, and we're done. So just to summarize, the result of our fit sizing algorithm in simple terms looks like this: the size of the parent towards the layout direction is the two padding values along that axis (in this case left and right) plus the child gap multiplied by the child count minus one plus the combined size of all the children, which is just this same approach applied recursively. The size of the parent against the layout direction is the top and bottom padding plus the maximum child height, that is, the height of the largest child.

These two formulas work exactly the same for left to right layout direction versus top to bottom, just with the height and width and padding values swapped. Alright, we've got our fit algorithm working well. Just before we move on, I'd like to simplify and clean up the implementation a little bit and also just point out one of the natural properties of this type of UI layout system.

The first interesting thing is notice how in our new sizing algorithm we don't reference positions at all. As it turns out, if you're using a flexible box approach like this, you can calculate the final sizing of all the elements in the tree totally independent of where they are on the screen. You can size all the elements in your tree first and then calculate the positions later.

This might not seem like such a significant distinction, but I promise you it is. If I have a dropdown like this with dynamic content and I'm trying to size and position the elements at the same time, watch what happens. I size and position the first three elements in the tree just fine, but because the fourth item is wider, I now have to go back and change the position of all the previous icons.

I need to push them over to the right-hand side. With flexible sizing like this, you can't actually know the final position of an element until you've completely finished calculating all the sizes. So to significantly simplify our implementation and completely eliminate an entire class of layout bugs, we're going to split our layout step into two—computing sizes first and then positions.

And this is why I mentioned being open to multiple passes; we can make life so much easier for ourselves handling one thing at a time. The second interesting thing that has popped up here is that we tend to think about user interfaces as expressly two-dimensional. But as we've seen building our implementation logically, we actually treat both of those two dimensions exactly the same: X versus Y or width versus height; it doesn't matter.

What matters is the direction of the layout. So rather than having to duplicate the code for handling width versus height, we can create a single function that computes the size of an element along its layout axis or across its layout axis, independent of whether we're talking about width or height.

It might sound funny, but when it comes down to it, UI is actually mostly a one-dimensional problem, not a two-dimensional one. You might not remember the function names that I showed you at the beginning of the video, but if I bring them up again, it's probably much more obvious now what they do for layout-related tasks. Clay generally has just one function that can compute either the widths or the heights of an element depending on the parameters that you pass it.

So we're done implementing containers that can fit their contents, and now we've got a powerful UI layout system already. But we've still got lots more interesting work to do. A very common requirement in a layout system like this is for a child element to be able to grow to take up all the available space in its parent. If we had a layout with a sidebar, for example, we might also want to have a main content pane that horizontally grows to take up any remaining space in the container.

Clay provides this functionality with the grow sizing mode. Rather than being entirely different, grow is an extension on top of the fit sizing mode. Grow containers will still fit their contents at the absolute minimum, but will also expand to fill any extra space in their parent. So to extend our example, we're going to change our outer container to a fixed width to make it much wider.

We're going to add a third child element, and we're going to change the width of this middle child to grow so that it takes up the remainder of the empty horizontal space pushing the third child all the way over to the right-hand side of its parent. Now, it might initially be tempting to just put the grow handling into the close element function like we do with fit.

We can size our root because it has a fixed width, and then when we're iterating through our children and reach our second child, we know how big the parent is already, so we can just expand our middle child to fill all the available space. Right? Unfortunately, no. If we did that, we would take up all the available space in the parent, and there would be no room for our third child, which we haven't reached yet.

So you won't actually know how much free space you have until you've sized the entire tree. The simplest and most foolproof way to handle grow containers is just to add another pass over our tree. In our first pass, we do exactly what we've been doing already to calculate the size of our fit containers, and then in our second pass, we're going to traverse downwards through the tree in breadth-first order and expand our grow containers into any available space.

So if we're starting at the end of our fit pass, our root container currently looks like this because it has a fixed width. You can see that it has some extra space over at the right-hand side, and our yellow container in the middle has a width of zero because it hasn't grown yet. Let's start with the containers that grow along the layout axis.

In order to figure out how much space we have available to expand into, we can start with the entire width of the root container and subtract exactly the same calculated value we use for fit sizing—that is, the two padding values, the sum of the sizes of all of the child elements, and the total gap between the child elements, which uses that fence post calculation.

As you can see, this perfectly gives us back the total remaining space along this axis. From there, our solution is very simple. All we need to do is add that remaining extra space to the width of our grow element, and we're done. This is a perfect example of how powerful the separation of sizing and position are.

If we were calculating sizes and positions at the same time, we would have needed to push all the subsequent children over to the right. But at this point in the layout, because all we're looking at is sizes, we know that positions will be calculated correctly once in the final pass after we've already finished with all of our sizing.

Now, if we wanted our yellow rectangle to also grow across the layout direction—that is, grow its height—we're not going to add up the heights of the children to find the available space. We just take the height of the parent, subtract the top and bottom padding, as well as the height of just this element, and again that gives us a value that represents the extra space available for us to expand into vertically.

We've now got something really powerful. If I resize my root element, the children dynamically resize and move with it. This is the beginnings of a UI that will respond beautifully to changes in the size of the application window. However, we're not completely there with our grow implementation just yet, and there are a couple more cases we have to cover.

In our current example, we've only got a single child element set to grow, but what happens if we have more than one? What if we want both the second and the third child to grow to take up available space? How should that extra space be shared between those two elements?

Now this is one of those decision points where there's actually no correct answer. Before we even get to the implementation, people have different opinions about how that space should be shared. The way I find to be the most aesthetically pleasing—and the way that clay works—is that Clay's algorithm tries to ensure that all the children with grow sizing will end up the same size.

That means that if you have two elements both set to grow and after our first pass finishes their starting sizes are different, the extra width in the parent container will be distributed unevenly amongst them. The smaller container will grow more than the larger one so that the end result is that they're the same size, both taking up half of the available space.

This isn't always possible, for example, if the blue element was already larger than the entire remaining width; all the remaining space would go to the smaller grow rectangle, and they still wouldn't be the same size. But I've personally found it to produce the most pleasing results in most cases. So how do we actually implement this growth strategy?

Well, we want all our containers to end up the same size, and so if we break it down into even simpler terms, what we're actually saying is that we want to expand the smallest elements. Let's look at it visually; as the smallest element in this container expands, eventually it will end up the same size as the second smallest element. From that point onwards, we can expand both our smallest elements equally until they reach the size of our largest element.

Now that all our elements are the same size, we expand all three of them together until we run out of space. Let's return to our grow implementation. We previously figured out how to calculate the remaining width; now we just need to figure out how to properly apportion it to the two right-hand children.

This involves two passes over the subset of children that have grow sizing. The first pass, we find the size of the smallest element and the size of the second smallest element. To figure out how much our smallest elements need to grow, we just subtract the smallest size from the second smallest size, and that gives us how much width we need to add.

We then iterate over our children again, this time adding that width to any child we find that matches the smallest size. This ensures that any children that are the same size will get expanded together. Any size that we add to children, we subtract from the total available extra space, and after we finished this iteration, we still have remaining width to distribute, so we repeat the process again.

This time, because both of our grow containers have the same width, this inner block of code never runs, which means our width to add is just the entire remaining width. If we added that to both of our containers, it would overflow, so we clamp our width to the remaining width divided by the number of grow containers, which is two. We then add that width to any container matching the smallest width, which is both of them, which brings our remaining width to zero.

We now have an approach that can handle multiple growing containers and much more complex layouts. But so far, all we've dealt with are rectangles, and rectangles are an extremely common and important part of UI layout, but they're not the only type of element that clay supports.

Next up, we're going to handle one of the most common and also the trickiest elements in UI layout, which is text. Whenever you read about text in user interfaces, it's very easy to get overwhelmed with all the jargon and edge cases presented, like text shaping, line height, and anti-aliasing.

But luckily, the majority of the difficulty actually comes from rendering text correctly. We're going to keep it very simple by just focusing on the characteristics of text that actually affect layout. Unlike a rectangle, which by default has no intrinsic size, if we render some text with a particular font and font size, the result will take up both horizontal and vertical space.

So as long as we have a way to measure the dimensions of some text, our current UI layout algorithm still works perfectly. If instead of hardcoding the width and height of the outer rectangle, I let it fit to its contents, you can see that because the text element has measurable dimensions, the container element fits to the text contents just fine.

This works great for things like button labels but starts to fall apart when we're dealing with large bodies of text like blogs or articles. If I try to render a text element with a few hundred words that has no explicit newline characters, my text element will end up with a huge width, and that will completely break my layout by causing every subsequent sibling element to render off the screen.

Now this might seem like a catastrophic failure of our algorithm, but we're actually just missing one small piece, which is that we figured out how to grow containers to take up extra space, but we never figured out how to shrink containers to fit into their parent if the contents are too large.

Of course, if you've used a computer for any significant amount of time before, you know that an easy way to shrink the width of our contents to fit inside our fixed width blue rectangle is that we could wrap the text onto new lines. There's a bit to unpack, so let's start by thinking at a high level about how to shrink elements that are too large for their container.

We already have a bit of a conundrum here: if we have multiple child elements, with one fitting to its text and the other two having fixed widths, if we go ahead and just naively shrink all our child elements equally, we will break the size and constraints of our other two fixed width elements.

We want to shrink our text element and wrap the text rather than shrinking the rectangles that the user has explicitly said they want to have a fixed width. Now we could, for example, say that our algorithm only shrinks elements that don't have a fixed width, which would work in this case, but imagine if our fixed width rectangle wasn't a direct sibling but instead was one level deep inside a container with fit sizing.

We would end up shrinking our text in the pink fit container at this level without realizing it would cause some of the contents somewhere further down the tree to overflow. So our algorithm here is missing some data. What we're expressing here is that the text has a preferential width that it will expand to if all that space is available, but it also has a minimum width, which is the smallest size that it could shrink down to in order to save space.

We already have that preferential size and the minimum size of our text element in English, which might be just the size of the largest single word in the text contents, by contrast, our fixed width rectangle has the same preferred and minimum sizes. So now that we've got our distinction between an element's preferred dimensions and its minimum dimensions, the algorithm to shrink our elements to fit their container is functionally identical to our algorithm to grow elements to take up available space.

When we shrink our child elements down, we want them to end up the same size, which means that we're going to start by shrinking the largest element down to the size of the second largest element and continue in the same way until everything is small enough to fit. The one extra piece of logic we add here to handle our minimum width case is simply that when we're sizing our elements down, if an element hits its minimum width, we just remove it from the array of containers that we're sizing down.

Just to really solidify this knowledge, let's create a slightly more complex container shrinking situation to solve and go through the whole thing step by step. We've got our root element with a fixed size, our first child, which is just a plain text element, our second child, which has a fixed size, and our third child, which is a fit container that has some other text inside it.

Let's walk through our tree and solve this problem. We start with our first fit sizing pass where we gather the minimum and preferred sizing of our elements. Our text elements have a large preferred size and a small minimum size, and our fixed-size elements have the same preferred and minimum size.

We can make a small change to our close element function here to propagate those minimum sizes upwards through the tree just as we do with our preferred sizes. As we start our second pass to grow our elements, our remaining width calculation here for our children is going to come out as a negative value because the contents are larger than the root container, and that is a clue.

Given how we saw that shrinking elements uses almost exactly the same strategy as growing elements, we can turn this grow sizing pass into one that handles both growing and shrinking. Because our shrink and grow strategies are almost exactly the same, I could slightly modify this loop to work for both growing and shrinking, but for the purposes of a video demo, it might get a little hard to read, so I'll just copy-paste this code and modify it slightly to handle shrinking.

So we start our shrinking algorithm with our three children. We identify the largest and second largest child elements as the two text containers, and then we shrink down our right-hand text container to be equal width to the left one. Our next iteration identifies our two text elements as the equal largest containers and our fixed size element as the second largest, so we shrink both our text elements down to the size of the fixed element.

Now it looks like we're done here, but there's actually still a small amount of width left to make up. On our third iteration, all our elements are the same size, which means we need to shrink them all equally. So we start by shrinking our first text element by a third of the remaining difference.

Then when we go to shrink our second element, the fixed size one, we can see that it's already at its minimum size, so instead of shrinking it, we just remove it from the list and continue on to shrink our second text element by a third of the remaining size. As a result, we now have one final iteration where we shrink both our text elements equally and reach our target content width.

However, even though we've resized the width of our text elements correctly, you can obviously see that the text itself is still overflowing. Even if the bounding boxes are correct, we need a new step in our algorithm that happens after we process our shrinking, which is wrapping any text elements that have shrunk.

Now, there are lots of different approaches that you can take to text wrapping that vary significantly depending on which human languages you want to support, but a common approach is to start by figuring out which character crosses the edge of the element's bounding box and then work backwards from there until you find an appropriate point to wrap, which in English would be a space character.

We then slice the string from that point onwards, increasing the height of the text element container by the height of one line of text, and just repeat that process until we reach the end of the string. So all we have left is to wrap our text elements, and then we should be done.

This doesn't quite look right. What's happened here is that when we wrap text, the element will inevitably have a larger height than it did originally, which then needs to be propagated backwards up the tree to parent containers. If propagating heights upwards through the tree sounds familiar to you, it's because we're already doing that as part of our first fit sizing step.

The problem, of course, is that that step happens before our text wrapping step, which can cause our heights to change again. So what we need to do is to split both of our current sizing steps into separate steps for width and height and defer sizing our heights until after we wrap our text.

This means at the text wrapping step, our background rectangle actually has a height of zero because it's configured to fit its contents, and we haven't done our height pass for fit sizing yet. I'm also just going to set the height sizing mode of our yellow rectangle to grow so you can see what happens. And of course, that means it's also going to be zero because we haven't grown our heights yet either.

So after wrapping our text, we do our fit sizing pass for heights, which gives us the correct height for our background rectangle and the light blue text container on the right-hand side. We can now continue on to our grow and shrink heights pass, which can now correctly grow the height of the yellow box to match the height changes caused by the text wrapping.

At this point, we have a very powerful layout algorithm that can represent a significant percentage of the layouts that you would see on the web. For example, it's just missing a couple of small key features that we can add in very easily. The first feature is minimum and maximum sizing, which is a classic feature in UI layout to help you control how your UI responds to changes in content size.

If we take our original drop-down menu that we were planning to build, you'll notice it has a minimum width that it will maintain even if the children are smaller, and it has a maximum width that it's allowed to grow to before it forces the menu text items to start wrapping. We already have the concept of minimum and preferred sizing underneath to allow our text elements to work correctly, so we're going to expose our minimum sizing as something configurable and add a maximum sizing as well.

There are two places where we need to implement handling for this. Firstly, in our fit sizing passes when we're expanding our parent containers to the size of their children, we cap that size to the configured maximum size of the container if there is one. The second place we need to handle this is when we're growing elements to take up additional space in their parent, and in exactly the same way as shrinking elements, where we simply remove elements from our shrink array if they've hit their minimum size, we do exactly the same thing for our array of growable elements if they've hit their maximum size.

Very simple and straightforward, and we already understand most of the logic that's happening there. Now, the final part of the algorithm that I'm going to show you is alignment, which is often used to center elements inside their parent container. Alignment in clay can be provided separately for each axis with a combination of left, center, and right alignment for the x-axis and top, center, and bottom for the y-axis.

Now because alignment is entirely to do with position and not to do with size at all, we're going to handle our alignment right at the end at the same time that we're calculating our positions. Alignment is actually a very straightforward little calculation, and as with most other things, varies depending on if we're aligning along the layout axis or across the layout axis.

If we're aligning along the layout axis—for example, centering the X alignment of the children of a left-to-right container—we do the same classic calculation to find the remaining width of the parent, not taken up by the contents, the padding, or the child gap. We then take that extra space, divide it by two to distribute it equally on the left and right-hand sides of our content, and then add that divided difference to the X offset of every child element, which will push them all into the center.

Of course, if the content is the same size as the container, we'll get a difference of zero, and the elements won't move at all. Our strategy is slightly different if we're aligning across the layout direction. If we take the same container laid out left to right and want to center align our elements vertically, you can see that unlike our X centering where we move all the elements by the same amount, each of these children can have a different Y offset.

But the approach is actually very similar; we simply calculate the remaining height for each child individually by subtracting the padding and the child height from the outer element height. This will produce a different alignment offset for child elements with different heights, and you might already be able to see this, but if we wanted instead to align these elements to the right or to the bottom, we would just skip dividing the remaining size by two and add the entire difference to the top instead.

It really is one of the most straightforward parts of layout, assuming that is that you finalize all your sizes before trying to approach alignment. And now to really pull it all together, we're going to lay out that nice context menu that I showed you at the beginning of the video because we've got all the tools that we need. First up, we create our root element—an empty purple fit container.

If we create our child menu items now, they'll be laid out from left to right, so we can switch that to top to bottom. Let's give our menu items some padding around the edges and the same size gap between the children. Now I want my menu items to all be the same width, so I'm going to set the width sizing to grow so they take up all the available space. And you can recognize that these are growing across the layout axis, which is top to bottom.

I want our icons to be over on the right-hand side, so what I'll do is create an invisible container around my text label and set its width to grow so that it pushes the icon over to the right-hand side. I'll also add some horizontal padding inside my menu items to make them look a little bit nicer, and rather than adding vertical padding, which could result in the menu items being a different height, I'll set the minimum height of each menu item to 80.

This is looking better, but our text and icons are still in slightly the wrong place, so I'm going to set the Y alignment of the menu items to center. We're almost there, but that comment icon is a little bit too close to the text, so I'm going to give each menu item a child gap of 32 pixels to push those apart. This is looking great, but I just like to make sure we can handle changes to the size of our content gracefully, so I'm going to set a minimum width on our root element so that if our menu text labels are all short, the menu doesn't end up too skinny, and I'll also set a maximum width so that if a menu label is too long, it will wrap to the next line.

As one final little tweak, the dictionary menu item is looking a bit cramped vertically, so I'll just add some vertical padding to the menu items, and we're done! Not only do we have a great looking flexible context menu, but you would also be able to explain to me how every single element in this UI ended up at its correct size and position.

This, of course, is just a small window into how clay works. The core layout algorithm is a piece of a larger puzzle. This might sound like an odd thing to say, but I hope, armed with this knowledge, you might decide that you don't even need to use clay at all. Building an extremely powerful and flexible UI layout algorithm is actually very straightforward once someone explains the tricks to you, and I would really encourage you to just have a try at implementing it yourself from scratch. It's a lot of fun!

Good luck!