[BUG] Long words can break size constraints of parent elements
Hey!
I have found that long words are able to break their parents up the hierarchy (See attached image).
The difference between the left and right version in the code and the image is: .childAlignment = {.x = CLAY_ALIGN_X_RIGHT}.
Expected behaviour: The SideBar and SideBar 2 container would expand to a width of 300 - 2*16. Actual behaviour: The containers outgrow their parent according to the min width of their text children.
CLAY({ .id = CLAY_ID("OuterContainer"), .layout = {.sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)}, .padding = CLAY_PADDING_ALL(16), .childGap = 16}, .backgroundColor = {250,250,255,255} }) {
CLAY({.layout = {.sizing = {.width = CLAY_SIZING_FIXED(150), .height = CLAY_SIZING_GROW(0)} }}){}
CLAY({
.id = CLAY_ID("SideBar"),
.layout = {.sizing = {.width = CLAY_SIZING_FIXED(300), .height = CLAY_SIZING_GROW(0) }, .padding = CLAY_PADDING_ALL(16), .childGap = 16,.childAlignment = {.x = CLAY_ALIGN_X_RIGHT},.layoutDirection = CLAY_TOP_TO_BOTTOM },.backgroundColor = { 224, 215, 210, 255 }
}) {
CLAY({ .id = CLAY_ID("ProfilePictureOuter"), .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },.layoutDirection = CLAY_TOP_TO_BOTTOM}, .backgroundColor = { 168, 66, 28, 255 } }) {
CLAY_TEXT(CLAY_STRING("Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong Word"), CLAY_TEXT_CONFIG({ .textColor = {255, 100, 100, 255},.fontSize = 16 }));
}
}
CLAY({
.id = CLAY_ID("SideBar 2"),
.layout = {.sizing = {.width = CLAY_SIZING_FIXED(300), .height = CLAY_SIZING_GROW(0) }, .padding = CLAY_PADDING_ALL(16), .childGap = 16,.layoutDirection = CLAY_TOP_TO_BOTTOM },
.backgroundColor = { 224, 215, 210, 255 }
}) {
CLAY({ .id = CLAY_ID("ProfilePictureOuter 2"), .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) },.layoutDirection = CLAY_TOP_TO_BOTTOM}, .backgroundColor = { 168, 66, 28, 255 } }) {
CLAY_TEXT(CLAY_STRING("Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong Word"), CLAY_TEXT_CONFIG({ .textColor = {255, 100, 100, 255},.fontSize = 16 }));
}
}
}
Thank you!
In Clay, the minimum width of the text is that of its longest word, and it doesn't support breaking in the middle of the word. This corresponds to overflow-wrap: normal; in CSS. What you would like instead is to break the word in the middle if it overflows the max size of the parent, did I understand it right? Like overflow-wrap: break-word; https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-wrap
Do you wish it were the default behavior, or would you like to have an option to specify whether to break a long word?
Hey @SolidAlloy! Thank you for your response and sorry it seems my bug report was not clear enough. Let me try to explain a bit better:
I want neither. From my point of view Clay is creating a layout from a bunch of constraints that I as the clay user set. It is then clays job to figure out a layout that satisfies the most amount of constraints (if not all) that I set.
Lets look at the constraints of the widths of my example: SideBar: Fixed Width of 300 ProfilePictureOuter: Grow(0) -> Meaning (Min 0, Max 300-Padding) Clay Text: Fixed Width >>300
The resulting layout however breaks the constraints of ProfilePictureOuter that I as user set. Basically I do not think GROW elements should be allowed to grow beyond their parents. I think a valid layout in clay is one where the children are always smaller than their parents and this case works beautifully. But currently Clay behaves inconsistently when layouts do break depending on the exact setup. If this ends up as a matter of opinion instead of a bug I am at least advocating for specifying how Clay handles degenerate layouts to be more trustworthy in real production settings.
As a user of Clay I care about being able to ensure my layouts do not explode even when supporting lots of languages and screen resolutions. I can easily resize text that did not fit in a single box - but I cannot fix an entire layout programmatically - that would kind of miss the point.
Does this make a bit more sense?
Thanks for clarifying! Yes, it makes more sense now. I would split it into three points:
- There needs to be documentation clarifying when Clay cannot satisfy all constraints.
- A child that violates its own constraints must not change the parent's measured size. In your example, even if the
TextexceedsProfilePictureOuter’s max width,ProfilePictureOutermust still respectSideBar’s fixed 300 width and never expand past it. - For degenerate cases, we need instruments that hide an overflowing element. For text, there should be an option to break in the middle of the work or put an ellipsis. For non-text content, skip rendering the overflowing children or make the parent a clip container implicitly so that the portion of the overflowing child is hidden.
Regarding 3.: I strongly disagree. As a dev I want tools to deal with these cases myself. I do not think it is clays job to decide where linebreaks go or just skip rendering things. I just want clay to output the best possible layout and give me all the info I need to decide what to do next ideally. The current line breaking logic in clay assumes text is in a language that uses spaces.
Here is an example of me doing exactly that: https://discord.com/channels/1291501630973415474/1405114224136683622/1420912690926190672
I fixed it in a C# port of Clay. The change is in this section: https://github.com/nicbarker/clay/blob/588b93196cc7a182a024a9ef08ba8e352904a1bd/clay.h#L2367-L2401
The algorithm:
child->minDimensions is considered a soft minimum limit of a container. child->layoutConfig->sizing.width.size.minMax.min is the hard limit.
- Shrink elements as much as possible to their soft limit. If the soft limit of a resizable element is reached, change its minimum dimensions to the hard limit and add the child to a backup list of resizable elements.
- If after shrinking to soft limits, the container still hasn't shrunk to a desirable size, shrink the children to their hard limits.
static float ShrinkChildren(float sizeToDistribute, int axis, ref StructList<Ptr<UIBox>> resizableChildren, ref StructList<Ptr<UIBox>> backupList)
{
while (sizeToDistribute < -EPSILON && resizableChildren.Count > 0)
{
float largest = 0;
float secondLargest = 0;
float sizeToAdd = sizeToDistribute;
for (int i = 0; i < resizableChildren.Count; ++i)
{
ref UIBox child = ref _boxPool[resizableChildren[i]];
float childSize = child.ScreenRect.Size[axis];
if (ApproxEqual(childSize, largest))
continue;
if (childSize > largest)
{
secondLargest = largest;
largest = childSize;
}
else
{
secondLargest = Mathf.Max(secondLargest, childSize);
sizeToAdd = secondLargest - largest;
}
}
sizeToAdd = Mathf.Max(sizeToAdd, sizeToDistribute / resizableChildren.Count);
for (int i = 0; i < resizableChildren.Count; ++i)
{
Ptr<UIBox> childPtr = resizableChildren[i];
ref UIBox child = ref _boxPool[childPtr];
if (ApproxEqual(child.ScreenRect.Size[axis], largest))
{
float softMinSize = child.MinSize[axis];
float previousSize = child.ScreenRect.Size[axis];
child.ScreenRect.Size[axis] += sizeToAdd;
if (child.ScreenRect.Size[axis] <= softMinSize)
{
child.ScreenRect.Size[axis] = softMinSize;
resizableChildren.RemoveSwapBack(i--);
if (!Unsafe.IsNullRef(ref backupList) && softMinSize > child.SizeConstraints[axis].MinMax.Min + EPSILON)
{
// We no longer will need to use the soft min size during the frame, so we can change it to the hard min size.
// We will use the backup list to further compress the children if the container still hasn't shrunk enough.
child.MinSize[axis] = child.SizeConstraints[axis].MinMax.Min;
backupList.Add(childPtr);
}
}
sizeToDistribute -= child.ScreenRect.Size[axis] - previousSize;
}
}
}
return sizeToDistribute;
}
sizeToDistribute = ShrinkChildren(sizeToDistribute, axis, ref _resizableChildrenBuffer, ref _resizableChildrenSecondaryBuffer);
ShrinkChildren(sizeToDistribute, axis, ref _resizableChildrenSecondaryBuffer, ref Unsafe.NullRef<StructList<Ptr<UIBox>>>());
This requires allocating more memory for the second resizableChildren list - but this can be avoided as well. Right now, bfsBuffer is used to walk the tree. But the tree can be walked through in a depth-first manner without breaking the algorithm. Since for each node, we iterate its children, it doesn't matter if after that we go deep or broad. In pseudo-code, it looks like this:
for (Clay_LayoutElement *element = root; element != nullptr; ) {
if (element->firstChild != nullptr) {
// iterate and resize children
element = element->firstChild;
}
else if (element->nextSibling != nullptr) {
element = element->nextSibling;
}
else {
Clay_LayoutElement *parent = element->parent;
while(true) {
if (parent == nullptr) {
element = nullptr;
break;
}
if (parent->nextSibling != nullptr) {
element = parent->nextSibling;
break;
}
parent = parent->parent;
}
}
}
With bfsBuffer now free, it can be reused for the backup list of resizable children.