[Core] Add cascading property support for text element configs.

This change introduces an "inheritance" mechanism for all text-related
properties, enabling developers to define base styles once and have
child elements automatically pick up defaults unless explicitly
overridden, much like CSS’s cascade model.

By providing individual sentinel values (`CLAY_COLOR_INHERIT`,
`CLAY_TEXT_INHERIT_U16`, `CLAY_TEXT_WRAP_INHERIT`, and
`CLAY_TEXT_ALIGN_INHERIT`), or `CLAY_TEXT_CONFIG_INHERIT_ALL`, we can
now defer resolution of properties,

Backward compatibility is preserved: if no `_INHERIT` values are used,
behavior remains identical to previous versions.

Previously, Clay used the presence of `CLAY__ELEMENT_CONFIG_TYPE_TEXT`
in `Clay_LayoutElement.elementConfigs` to distinguish between a text and
non-text element. This is not possible to use anymore, since parent
non-text elements may have a `CLAY__ELEMENT_CONFIG_TYPE_TEXT` with text
properties.

I introduced a `hasTextElementData`, but I can understand if this design
is considered to not be acceptable due to the higher memory usage in
this core struct, perhaps in alternative a new
`CLAY__ELEMENT_CONFIG_TYPE_TEXT_STRING` element config can be
introduced?
This commit is contained in:
tritao 2025-04-24 19:10:33 +01:00
parent b33ba4ff62
commit d6ed6cb5c2

124
clay.h
View file

@ -350,6 +350,8 @@ typedef CLAY_PACKED_ENUM {
CLAY_TEXT_WRAP_NEWLINES,
// Disable text wrapping entirely.
CLAY_TEXT_WRAP_NONE,
// Inherit from nearest ancestor (or default `CLAY_TEXT_WRAP_WORDS` if none)
CLAY_TEXT_WRAP_INHERIT
} Clay_TextElementConfigWrapMode;
// Controls how wrapped lines of text are horizontally aligned within the outer text bounding box.
@ -360,6 +362,8 @@ typedef CLAY_PACKED_ENUM {
CLAY_TEXT_ALIGN_CENTER,
// Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
CLAY_TEXT_ALIGN_RIGHT,
// Inherit from nearest ancestor (or default `CLAY_TEXT_ALIGN_LEFT` if none)
CLAY_TEXT_ALIGN_INHERIT,
} Clay_TextAlignment;
// Controls various functionality related to text elements.
@ -391,6 +395,29 @@ typedef struct {
CLAY__WRAPPER_STRUCT(Clay_TextElementConfig);
// Cascading support --------------------
/* 0xFFFF is outside the signed 16-bit range Clay already uses
and cannot conflict with real font sizes, IDs, spacing, etc. */
#define CLAY_TEXT_INHERIT_U16 ((uint16_t)0xFFFF)
#define CLAY_TEXT_WRAP_INHERIT ((Clay_TextElementConfigWrapMode)-1)
#define CLAY_TEXT_ALIGN_INHERIT ((Clay_TextAlignment)-1)
/* Chosen because fullytransparent black should never be rendered. */
static const Clay_Color CLAY_COLOR_INHERIT = {0,0,0,0};
#define CLAY_TEXT_CONFIG_INHERIT_ALL \
((Clay_TextElementConfig){ \
.userData = NULL, \
.textColor = CLAY_COLOR_INHERIT, \
.fontId = CLAY_TEXT_INHERIT_U16, \
.fontSize = CLAY_TEXT_INHERIT_U16, \
.letterSpacing = CLAY_TEXT_INHERIT_U16, \
.lineHeight = CLAY_TEXT_INHERIT_U16, \
.wrapMode = CLAY_TEXT_WRAP_INHERIT, \
.textAlignment = CLAY_TEXT_ALIGN_INHERIT \
})
// Image --------------------------------
// Controls various settings related to image elements.
@ -723,6 +750,8 @@ typedef struct {
Clay_Color backgroundColor;
// Controls the "radius", or corner rounding of elements, including rectangles, borders and images.
Clay_CornerRadius cornerRadius;
// Controls settings related to text elements.
Clay_TextElementConfig text;
// Controls settings related to image elements.
Clay_ImageElementConfig image;
// Controls whether and how an element "floats", which means it layers over the top of other elements in z order, and doesn't affect the position and size of siblings or parent elements.
@ -1100,6 +1129,7 @@ typedef struct {
Clay_LayoutConfig *layoutConfig;
Clay__ElementConfigArraySlice elementConfigs;
uint32_t id;
bool hasTextElementData;
} Clay_LayoutElement;
CLAY__ARRAY_DEFINE(Clay_LayoutElement, Clay_LayoutElementArray)
@ -1731,6 +1761,10 @@ bool Clay__ElementHasConfig(Clay_LayoutElement *layoutElement, Clay__ElementConf
return false;
}
bool Clay__ElementHasTextElementData(Clay_LayoutElement *layoutElement) {
return layoutElement->hasTextElementData;
}
void Clay__UpdateAspectRatioBox(Clay_LayoutElement *layoutElement) {
for (int32_t j = 0; j < layoutElement->elementConfigs.length; j++) {
Clay_ElementConfig *config = Clay__ElementConfigArraySlice_Get(&layoutElement->elementConfigs, j);
@ -1938,6 +1972,63 @@ void Clay__OpenElement(void) {
}
}
#define CLAY__IS_INHERIT_U16(v) ((v) == CLAY_TEXT_INHERIT_U16)
#define CLAY__IS_INHERIT_WRAP(v) ((v) == CLAY_TEXT_WRAP_INHERIT)
#define CLAY__IS_INHERIT_ALIGN(v) ((v) == CLAY_TEXT_ALIGN_INHERIT)
#define CLAY__IS_INHERIT_COLOR(v) \
((v).r == CLAY_COLOR_INHERIT.r && \
(v).g == CLAY_COLOR_INHERIT.g && \
(v).b == CLAY_COLOR_INHERIT.b && \
(v).a == CLAY_COLOR_INHERIT.a)
// Returns true if any of the text properties still needs merging.
static inline bool Clay__TextConfigHasInherit(const Clay_TextElementConfig *c)
{
return CLAY__IS_INHERIT_U16(c->fontId) || CLAY__IS_INHERIT_U16(c->fontSize) || CLAY__IS_INHERIT_U16(c->letterSpacing) ||
CLAY__IS_INHERIT_U16(c->lineHeight) || CLAY__IS_INHERIT_WRAP(c->wrapMode) || CLAY__IS_INHERIT_ALIGN(c->textAlignment) ||
(c->textColor.a == CLAY_COLOR_INHERIT.a);
}
static Clay_TextElementConfig Clay__ResolveTextConfig(const Clay_TextElementConfig *local)
{
Clay_Context* ctx = Clay_GetCurrentContext();
Clay_TextElementConfig out = *local;
int32_t stackLen = (int32_t)ctx->openLayoutElementStack.length;
for (int32_t si = stackLen - 1; si >= 0 && Clay__TextConfigHasInherit(&out); --si)
{
int32_t elemIndex = Clay__int32_tArray_GetValue(&ctx->openLayoutElementStack, si);
Clay_LayoutElement *ancestor = Clay_LayoutElementArray_Get(&ctx->layoutElements, elemIndex);
for (int32_t ci = 0; ci < ancestor->elementConfigs.length; ++ci) {
Clay_ElementConfig *cfg =
Clay__ElementConfigArraySlice_Get(&ancestor->elementConfigs, ci);
if (cfg->type != CLAY__ELEMENT_CONFIG_TYPE_TEXT)
continue;
Clay_TextElementConfig *anc = cfg->config.textElementConfig;
if (CLAY__IS_INHERIT_U16(out.fontId)) out.fontId = anc->fontId;
if (CLAY__IS_INHERIT_U16(out.fontSize)) out.fontSize = anc->fontSize;
if (CLAY__IS_INHERIT_U16(out.letterSpacing)) out.letterSpacing = anc->letterSpacing;
if (CLAY__IS_INHERIT_U16(out.lineHeight)) out.lineHeight = anc->lineHeight;
if (CLAY__IS_INHERIT_WRAP(out.wrapMode)) out.wrapMode = anc->wrapMode;
if (CLAY__IS_INHERIT_ALIGN(out.textAlignment)) out.textAlignment = anc->textAlignment;
if (CLAY__IS_INHERIT_COLOR(out.textColor)) out.textColor = anc->textColor;
}
}
// Final fall-backs if there are still "inherit" properties to resolve.
if (CLAY__IS_INHERIT_U16(out.fontId)) out.fontId = Clay_TextElementConfig_DEFAULT.fontId;
if (CLAY__IS_INHERIT_U16(out.fontSize)) out.fontSize = Clay_TextElementConfig_DEFAULT.fontSize;
if (CLAY__IS_INHERIT_U16(out.letterSpacing)) out.letterSpacing = Clay_TextElementConfig_DEFAULT.letterSpacing;
if (CLAY__IS_INHERIT_U16(out.lineHeight)) out.lineHeight = Clay_TextElementConfig_DEFAULT.lineHeight;
if (CLAY__IS_INHERIT_WRAP(out.wrapMode)) out.wrapMode = Clay_TextElementConfig_DEFAULT.wrapMode;
if (CLAY__IS_INHERIT_ALIGN(out.textAlignment)) out.textAlignment = Clay_TextElementConfig_DEFAULT.textAlignment;
if (CLAY__IS_INHERIT_COLOR(out.textColor)) out.textColor = Clay_TextElementConfig_DEFAULT.textColor;
return out;
}
void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig *textConfig) {
Clay_Context* context = Clay_GetCurrentContext();
if (context->layoutElements.length == context->layoutElements.capacity - 1 || context->booleanWarnings.maxElementsExceeded) {
@ -1955,6 +2046,12 @@ void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig *textConfig)
}
Clay__int32_tArray_Add(&context->layoutElementChildrenBuffer, context->layoutElements.length - 1);
if (Clay__TextConfigHasInherit(textConfig)) {
Clay_TextElementConfig resolvedTextConfig = Clay__ResolveTextConfig(textConfig);
textConfig = Clay__StoreTextElementConfig(resolvedTextConfig);
}
Clay__MeasureTextCacheItem *textMeasured = Clay__MeasureTextCached(&text, textConfig);
Clay_ElementId elementId = Clay__HashNumber(parentElement->childrenOrTextContent.children.length, parentElement->id);
textElement->id = elementId.id;
@ -1964,6 +2061,7 @@ void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig *textConfig)
textElement->dimensions = textDimensions;
textElement->minDimensions = CLAY__INIT(Clay_Dimensions) { .width = textMeasured->minWidth, .height = textDimensions.height };
textElement->childrenOrTextContent.textElementData = Clay__TextElementDataArray_Add(&context->textElementData, CLAY__INIT(Clay__TextElementData) { .text = text, .preferredDimensions = textMeasured->unwrappedDimensions, .elementIndex = context->layoutElements.length - 1 });
textElement->hasTextElementData = true;
textElement->elementConfigs = CLAY__INIT(Clay__ElementConfigArraySlice) {
.length = 1,
.internalArray = Clay__ElementConfigArray_Add(&context->elementConfigs, CLAY__INIT(Clay_ElementConfig) { .type = CLAY__ELEMENT_CONFIG_TYPE_TEXT, .config = { .textElementConfig = textConfig }})
@ -2097,6 +2195,9 @@ void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) {
if (!Clay__MemCmp((char *)(&declaration->border.width), (char *)(&Clay__BorderWidth_DEFAULT), sizeof(Clay_BorderWidth))) {
Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .borderElementConfig = Clay__StoreBorderElementConfig(declaration->border) }, CLAY__ELEMENT_CONFIG_TYPE_BORDER);
}
if (!Clay__MemCmp((char *)(&declaration->text), (char *)(&Clay_TextElementConfig_DEFAULT), sizeof(Clay_TextElementConfig))) {
Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .textElementConfig = Clay__StoreTextElementConfig(declaration->text) }, CLAY__ELEMENT_CONFIG_TYPE_TEXT);
}
}
void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) {
@ -2212,13 +2313,13 @@ void Clay__SizeContainersAlongAxis(bool xAxis) {
Clay_SizingAxis childSizing = xAxis ? childElement->layoutConfig->sizing.width : childElement->layoutConfig->sizing.height;
float childSize = xAxis ? childElement->dimensions.width : childElement->dimensions.height;
if (!Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT) && childElement->childrenOrTextContent.children.length > 0) {
if (!Clay__ElementHasTextElementData(childElement) && childElement->childrenOrTextContent.children.length > 0) {
Clay__int32_tArray_Add(&bfsBuffer, childElementIndex);
}
if (childSizing.type != CLAY__SIZING_TYPE_PERCENT
&& childSizing.type != CLAY__SIZING_TYPE_FIXED
&& (!Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT) || (Clay__FindElementConfigWithType(childElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT).textElementConfig->wrapMode == CLAY_TEXT_WRAP_WORDS)) // todo too many loops
&& (!Clay__ElementHasTextElementData(childElement) || (Clay__FindElementConfigWithType(childElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT).textElementConfig->wrapMode == CLAY_TEXT_WRAP_WORDS)) // todo too many loops
&& (xAxis || !Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_IMAGE))
) {
Clay__int32_tArray_Add(&resizableContainerBuffer, childElementIndex);
@ -2514,7 +2615,7 @@ void Clay__CalculateFinalLayout(void) {
if (!context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
// If the element has no children or is the container for a text element, don't bother inspecting it
if (Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT) || currentElement->childrenOrTextContent.children.length == 0) {
if (Clay__ElementHasTextElementData(currentElement) || currentElement->childrenOrTextContent.children.length == 0) {
dfsBuffer.length--;
continue;
}
@ -2792,6 +2893,9 @@ void Clay__CalculateFinalLayout(void) {
break;
}
shouldRender = false;
if (!currentElement->hasTextElementData) {
break;
}
Clay_ElementConfigUnion configUnion = elementConfig->config;
Clay_TextElementConfig *textElementConfig = configUnion.textElementConfig;
float naturalLineHeight = currentElement->childrenOrTextContent.textElementData->preferredDimensions.height;
@ -2873,7 +2977,7 @@ void Clay__CalculateFinalLayout(void) {
}
// Setup initial on-axis alignment
if (!Clay__ElementHasConfig(currentElementTreeNode->layoutElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT)) {
if (!Clay__ElementHasTextElementData(currentElementTreeNode->layoutElement)) {
Clay_Dimensions contentSize = {0,0};
if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
for (int32_t i = 0; i < currentElement->childrenOrTextContent.children.length; ++i) {
@ -3001,7 +3105,7 @@ void Clay__CalculateFinalLayout(void) {
}
// Add children to the DFS buffer
if (!Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT)) {
if (!Clay__ElementHasTextElementData(currentElement)) {
dfsBuffer.length += currentElement->childrenOrTextContent.children.length;
for (int32_t i = 0; i < currentElement->childrenOrTextContent.children.length; ++i) {
Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, currentElement->childrenOrTextContent.children.elements[i]);
@ -3114,7 +3218,7 @@ Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialR
int32_t currentElementIndex = Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1);
Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, (int)currentElementIndex);
if (context->treeNodeVisited.internalArray[dfsBuffer.length - 1]) {
if (!Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT) && currentElement->childrenOrTextContent.children.length > 0) {
if (!Clay__ElementHasTextElementData(currentElement) && currentElement->childrenOrTextContent.children.length > 0) {
Clay__CloseElement();
Clay__CloseElement();
Clay__CloseElement();
@ -3138,7 +3242,7 @@ Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialR
}
CLAY({ .id = CLAY_IDI("Clay__DebugView_ElementOuter", currentElement->id), .layout = Clay__DebugView_ScrollViewItemLayoutConfig }) {
// Collapse icon / button
if (!(Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT) || currentElement->childrenOrTextContent.children.length == 0)) {
if (!(Clay__ElementHasTextElementData(currentElement) || currentElement->childrenOrTextContent.children.length == 0)) {
CLAY({
.id = CLAY_IDI("Clay__DebugView_CollapseElement", currentElement->id),
.layout = { .sizing = {CLAY_SIZING_FIXED(16), CLAY_SIZING_FIXED(16)}, .childAlignment = { CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
@ -3198,7 +3302,7 @@ Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialR
}
// Render the text contents below the element as a non-interactive row
if (Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT)) {
if (Clay__ElementHasTextElementData(currentElement)) {
layoutData.rowCount++;
Clay__TextElementData *textElementData = currentElement->childrenOrTextContent.textElementData;
Clay_TextElementConfig *rawTextConfig = offscreen ? CLAY_TEXT_CONFIG({ .textColor = CLAY__DEBUGVIEW_COLOR_3, .fontSize = 16 }) : &Clay__DebugView_TextNameConfig;
@ -3221,7 +3325,7 @@ Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialR
}
layoutData.rowCount++;
if (!(Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT) || (currentElementData && currentElementData->debugData->collapsed))) {
if (!(Clay__ElementHasTextElementData(currentElement) || (currentElementData && currentElementData->debugData->collapsed))) {
for (int32_t i = currentElement->childrenOrTextContent.children.length - 1; i >= 0; --i) {
Clay__int32_tArray_Add(&dfsBuffer, currentElement->childrenOrTextContent.children.elements[i]);
context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = false; // TODO needs to be ranged checked
@ -3826,7 +3930,7 @@ void Clay_SetPointerState(Clay_Vector2 position, bool isPointerDown) {
Clay__ElementIdArray_Add(&context->pointerOverIds, CLAY__INIT(Clay_ElementId) { .id = mapItem->idAlias });
}
}
if (Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT)) {
if (Clay__ElementHasTextElementData(currentElement)) {
dfsBuffer.length--;
continue;
}