Compare commits

...

3 Commits

Author SHA1 Message Date
Piggybank Studios
47c8248833
Merge fbf8251996 into 47d1d84bc8 2025-03-24 18:01:06 -04:00
Nic Barker
47d1d84bc8
[Core] Switch text content hashing to default behaviour (#335)
Some checks failed
CMake on multiple platforms / build (Release, cl, cl, windows-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, clang, clang++, ubuntu-latest) (push) Has been cancelled
CMake on multiple platforms / build (Release, gcc, g++, ubuntu-latest) (push) Has been cancelled
2025-03-25 10:13:04 +13:00
Taylor Robbins (Piggybank Studios)
fbf8251996 Adds CLAY_TEXT_ALIGN_SHRINK option to Clay_TextAlignment which tells the TEXT command producing code it is allowed to pass a boundingBox that is horizontally smaller than the result of the MeasureText call for that string. The Renderer is then able to choose how it wants to fit a piece of text in a smaller space (often this involves removing some number of characters and replacing them with "..." characters) 2025-03-05 13:04:57 -08:00
8 changed files with 160 additions and 37 deletions

View File

@ -17,6 +17,7 @@ when ODIN_OS == .Windows {
} }
String :: struct { String :: struct {
isStaticallyAllocated: c.bool,
length: c.int32_t, length: c.int32_t,
chars: [^]c.char, chars: [^]c.char,
} }
@ -419,7 +420,13 @@ UI :: proc() -> proc (config: ElementDeclaration) -> bool {
return ConfigureOpenElement return ConfigureOpenElement
} }
Text :: proc(text: string, config: ^TextElementConfig) { Text :: proc($text: string, config: ^TextElementConfig) {
wrapped := MakeString(text)
wrapped.isStaticallyAllocated = true
_OpenTextElement(wrapped, config)
}
TextDynamic :: proc(text: string, config: ^TextElementConfig) {
_OpenTextElement(MakeString(text), config) _OpenTextElement(MakeString(text), config)
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -62,7 +62,7 @@ border2pxRed := clay.BorderElementConfig {
color = COLOR_RED color = COLOR_RED
} }
LandingPageBlob :: proc(index: u32, fontSize: u16, fontId: u16, color: clay.Color, text: string, image: ^raylib.Texture2D) { LandingPageBlob :: proc(index: u32, fontSize: u16, fontId: u16, color: clay.Color, $text: string, image: ^raylib.Texture2D) {
if clay.UI()({ if clay.UI()({
id = clay.ID("HeroBlob", index), id = clay.ID("HeroBlob", index),
layout = { sizing = { width = clay.SizingGrow({ max = 480 }) }, padding = clay.PaddingAll(16), childGap = 16, childAlignment = clay.ChildAlignment{ y = .Center } }, layout = { sizing = { width = clay.SizingGrow({ max = 480 }) }, padding = clay.PaddingAll(16), childGap = 16, childAlignment = clay.ChildAlignment{ y = .Center } },
@ -252,7 +252,7 @@ ColorLerp :: proc(a: clay.Color, b: clay.Color, amount: f32) -> clay.Color {
return clay.Color{a.r + (b.r - a.r) * amount, a.g + (b.g - a.g) * amount, a.b + (b.b - a.b) * amount, a.a + (b.a - a.a) * amount} return clay.Color{a.r + (b.r - a.r) * amount, a.g + (b.g - a.g) * amount, a.b + (b.b - a.b) * amount, a.a + (b.a - a.a) * amount}
} }
LOREM_IPSUM_TEXT := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." LOREM_IPSUM_TEXT :: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
HighPerformancePage :: proc(lerpValue: f32, titleTextConfig: clay.TextElementConfig, widthSizing: clay.SizingAxis) { HighPerformancePage :: proc(lerpValue: f32, titleTextConfig: clay.TextElementConfig, widthSizing: clay.SizingAxis) {
if clay.UI()({ id = clay.ID("PerformanceLeftText"), layout = { sizing = { width = widthSizing }, layoutDirection = .TopToBottom, childGap = 8 } }) { if clay.UI()({ id = clay.ID("PerformanceLeftText"), layout = { sizing = { width = widthSizing }, layoutDirection = .TopToBottom, childGap = 8 } }) {
@ -321,7 +321,7 @@ HighPerformancePageMobile :: proc(lerpValue: f32) {
} }
} }
RendererButtonActive :: proc(index: i32, text: string) { RendererButtonActive :: proc(index: i32, $text: string) {
if clay.UI()({ if clay.UI()({
layout = { sizing = { width = clay.SizingFixed(300) }, padding = clay.PaddingAll(16) }, layout = { sizing = { width = clay.SizingFixed(300) }, padding = clay.PaddingAll(16) },
backgroundColor = COLOR_RED, backgroundColor = COLOR_RED,
@ -331,7 +331,7 @@ RendererButtonActive :: proc(index: i32, text: string) {
} }
} }
RendererButtonInactive :: proc(index: u32, text: string) { RendererButtonInactive :: proc(index: u32, $text: string) {
if clay.UI()({ border = border2pxRed }) { if clay.UI()({ border = border2pxRed }) {
if clay.UI()({ if clay.UI()({
id = clay.ID("RendererButtonInactiveInner", index), id = clay.ID("RendererButtonInactiveInner", index),

180
clay.h
View File

@ -96,9 +96,9 @@
#define CLAY__ENSURE_STRING_LITERAL(x) ("" x "") #define CLAY__ENSURE_STRING_LITERAL(x) ("" x "")
// Note: If an error led you here, it's because CLAY_STRING can only be used with string literals, i.e. CLAY_STRING("SomeString") and not CLAY_STRING(yourString) // Note: If an error led you here, it's because CLAY_STRING can only be used with string literals, i.e. CLAY_STRING("SomeString") and not CLAY_STRING(yourString)
#define CLAY_STRING(string) (CLAY__INIT(Clay_String) { .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) }) #define CLAY_STRING(string) (CLAY__INIT(Clay_String) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) })
#define CLAY_STRING_CONST(string) { .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) } #define CLAY_STRING_CONST(string) { .isStaticallyAllocated = true, .length = CLAY__STRING_LENGTH(CLAY__ENSURE_STRING_LITERAL(string)), .chars = (string) }
static uint8_t CLAY__ELEMENT_DEFINITION_LATCH; static uint8_t CLAY__ELEMENT_DEFINITION_LATCH;
@ -185,6 +185,9 @@ extern "C" {
// Note: Clay_String is not guaranteed to be null terminated. It may be if created from a literal C string, // Note: Clay_String is not guaranteed to be null terminated. It may be if created from a literal C string,
// but it is also used to represent slices. // but it is also used to represent slices.
typedef struct { typedef struct {
// Set this boolean to true if the char* data underlying this string will live for the entire lifetime of the program.
// This will automatically be set for strings created with CLAY_STRING, as the macro requires a string literal.
bool isStaticallyAllocated;
int32_t length; int32_t length;
// The underlying character memory. Note: this will not be copied and will not extend the lifetime of the underlying memory. // The underlying character memory. Note: this will not be copied and will not extend the lifetime of the underlying memory.
const char *chars; const char *chars;
@ -357,6 +360,8 @@ typedef CLAY_PACKED_ENUM {
CLAY_TEXT_ALIGN_CENTER, CLAY_TEXT_ALIGN_CENTER,
// Horizontally aligns wrapped lines of text to the right hand side of their bounding box. // Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
CLAY_TEXT_ALIGN_RIGHT, CLAY_TEXT_ALIGN_RIGHT,
// The boundingBox passed to the TEXT render command may be smaller than the measured text size. The renderer must then decide how to shorten the text to make it fit
CLAY_TEXT_ALIGN_SHRINK,
} Clay_TextAlignment; } Clay_TextAlignment;
// Controls various functionality related to text elements. // Controls various functionality related to text elements.
@ -384,10 +389,6 @@ typedef struct {
// CLAY_TEXT_ALIGN_CENTER - Horizontally aligns wrapped lines of text to the center of their bounding box. // CLAY_TEXT_ALIGN_CENTER - Horizontally aligns wrapped lines of text to the center of their bounding box.
// CLAY_TEXT_ALIGN_RIGHT - Horizontally aligns wrapped lines of text to the right hand side of their bounding box. // CLAY_TEXT_ALIGN_RIGHT - Horizontally aligns wrapped lines of text to the right hand side of their bounding box.
Clay_TextAlignment textAlignment; Clay_TextAlignment textAlignment;
// When set to true, clay will hash the entire text contents of this string as an identifier for its internal
// text measurement cache, rather than just the pointer and length. This will incur significant performance cost for
// long bodies of text.
bool hashStringContents;
} Clay_TextElementConfig; } Clay_TextElementConfig;
CLAY__WRAPPER_STRUCT(Clay_TextElementConfig); CLAY__WRAPPER_STRUCT(Clay_TextElementConfig);
@ -876,8 +877,7 @@ CLAY_DLL_EXPORT int32_t Clay_GetMaxMeasureTextCacheWordCount(void);
// Modifies the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache. // Modifies the maximum number of measured "words" (whitespace seperated runs of characters) that Clay can store in its internal text measurement cache.
// This may require reallocating additional memory, and re-calling Clay_Initialize(); // This may require reallocating additional memory, and re-calling Clay_Initialize();
CLAY_DLL_EXPORT void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount); CLAY_DLL_EXPORT void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureTextCacheWordCount);
// Resets Clay's internal text measurement cache, useful if memory to represent strings is being re-used. // Resets Clay's internal text measurement cache. Useful if font mappings have changed or fonts have been reloaded.
// Similar behaviour can be achieved on an individual text element level by using Clay_TextElementConfig.hashStringContents
CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void); CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void);
// Internal API functions required by macros ---------------------- // Internal API functions required by macros ----------------------
@ -1348,26 +1348,140 @@ Clay_ElementId Clay__HashString(Clay_String key, const uint32_t offset, const ui
return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = base + 1, .stringId = key }; // Reserve the hash result of zero as "null id" return CLAY__INIT(Clay_ElementId) { .id = hash + 1, .offset = offset, .baseId = base + 1, .stringId = key }; // Reserve the hash result of zero as "null id"
} }
uint32_t Clay__HashTextWithConfig(Clay_String *text, Clay_TextElementConfig *config) { #if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64))
uint32_t hash = 0; static inline __m128i Clay__SIMDRotateLeft(__m128i x, int r) {
uintptr_t pointerAsNumber = (uintptr_t)text->chars; return _mm_or_si128(_mm_slli_epi64(x, r), _mm_srli_epi64(x, 64 - r));
}
if (config->hashStringContents) { static inline void Clay__SIMDARXMix(__m128i* a, __m128i* b) {
uint32_t maxLengthToHash = CLAY__MIN(text->length, 256); *a = _mm_add_epi64(*a, *b);
for (uint32_t i = 0; i < maxLengthToHash; i++) { *b = _mm_xor_si128(Clay__SIMDRotateLeft(*b, 17), *a);
hash += text->chars[i]; }
hash += (hash << 10);
hash ^= (hash >> 6); uint64_t Clay__HashData(const uint8_t* data, size_t length) {
// Pinched these constants from the BLAKE implementation
__m128i v0 = _mm_set1_epi64x(0x6a09e667f3bcc908ULL);
__m128i v1 = _mm_set1_epi64x(0xbb67ae8584caa73bULL);
__m128i v2 = _mm_set1_epi64x(0x3c6ef372fe94f82bULL);
__m128i v3 = _mm_set1_epi64x(0xa54ff53a5f1d36f1ULL);
uint8_t overflowBuffer[16] = { 0 }; // Temporary buffer for small inputs
while (length > 0) {
__m128i msg;
if (length >= 16) {
msg = _mm_loadu_si128((const __m128i*)data);
data += 16;
length -= 16;
} }
} else { else {
hash += pointerAsNumber; for (int i = 0; i < length; i++) {
overflowBuffer[i] = data[i];
}
msg = _mm_loadu_si128((const __m128i*)overflowBuffer);
length = 0;
}
v0 = _mm_xor_si128(v0, msg);
Clay__SIMDARXMix(&v0, &v1);
Clay__SIMDARXMix(&v2, &v3);
v0 = _mm_add_epi64(v0, v2);
v1 = _mm_add_epi64(v1, v3);
}
Clay__SIMDARXMix(&v0, &v1);
Clay__SIMDARXMix(&v2, &v3);
v0 = _mm_add_epi64(v0, v2);
v1 = _mm_add_epi64(v1, v3);
uint64_t result[2];
_mm_storeu_si128((__m128i*)result, v0);
return result[0] ^ result[1];
}
#elif !defined(CLAY_DISABLE_SIMD) && defined(__aarch64__)
static inline uint64x2_t Clay__SIMDRotateLeft(uint64x2_t x, int r) {
return vorrq_u64(vshlq_n_u64(x, 17), vshrq_n_u64(x, 64 - 17));
}
static inline void Clay__SIMDARXMix(uint64x2_t* a, uint64x2_t* b) {
*a = vaddq_u64(*a, *b);
*b = veorq_u64(Clay__SIMDRotateLeft(*b, 17), *a);
}
uint64_t Clay__HashData(const uint8_t* data, size_t length) {
// Pinched these constants from the BLAKE implementation
uint64x2_t v0 = vdupq_n_u64(0x6a09e667f3bcc908ULL);
uint64x2_t v1 = vdupq_n_u64(0xbb67ae8584caa73bULL);
uint64x2_t v2 = vdupq_n_u64(0x3c6ef372fe94f82bULL);
uint64x2_t v3 = vdupq_n_u64(0xa54ff53a5f1d36f1ULL);
uint8_t overflowBuffer[8] = { 0 };
while (length > 0) {
uint64x2_t msg;
if (length > 16) {
msg = vld1q_u64((const uint64_t*)data);
data += 16;
length -= 16;
}
else if (length > 8) {
msg = vcombine_u64(vld1_u64((const uint64_t*)data), vdup_n_u64(0));
data += 8;
length -= 8;
}
else {
for (int i = 0; i < length; i++) {
overflowBuffer[i] = data[i];
}
uint8x8_t lower = vld1_u8(overflowBuffer);
msg = vcombine_u8(lower, vdup_n_u8(0));
length = 0;
}
v0 = veorq_u64(v0, msg);
Clay__SIMDARXMix(&v0, &v1);
Clay__SIMDARXMix(&v2, &v3);
v0 = vaddq_u64(v0, v2);
v1 = vaddq_u64(v1, v3);
}
Clay__SIMDARXMix(&v0, &v1);
Clay__SIMDARXMix(&v2, &v3);
v0 = vaddq_u64(v0, v2);
v1 = vaddq_u64(v1, v3);
uint64_t result[2];
vst1q_u64(result, v0);
return result[0] ^ result[1];
}
#else
uint64_t Clay__HashData(const uint8_t* data, size_t length) {
uint64_t hash = 0;
for (int32_t i = 0; i < length; i++) {
hash += data[i];
hash += (hash << 10); hash += (hash << 10);
hash ^= (hash >> 6); hash ^= (hash >> 6);
} }
return hash;
}
#endif
hash += text->length; uint32_t Clay__HashStringContentsWithConfig(Clay_String *text, Clay_TextElementConfig *config) {
hash += (hash << 10); uint32_t hash = 0;
hash ^= (hash >> 6); if (text->isStaticallyAllocated) {
hash += (uintptr_t)text->chars;
hash += (hash << 10);
hash ^= (hash >> 6);
hash += text->length;
hash += (hash << 10);
hash ^= (hash >> 6);
} else {
hash = Clay__HashData((const uint8_t *)text->chars, text->length) % UINT32_MAX;
}
hash += config->fontId; hash += config->fontId;
hash += (hash << 10); hash += (hash << 10);
@ -1377,18 +1491,10 @@ uint32_t Clay__HashTextWithConfig(Clay_String *text, Clay_TextElementConfig *con
hash += (hash << 10); hash += (hash << 10);
hash ^= (hash >> 6); hash ^= (hash >> 6);
hash += config->lineHeight;
hash += (hash << 10);
hash ^= (hash >> 6);
hash += config->letterSpacing; hash += config->letterSpacing;
hash += (hash << 10); hash += (hash << 10);
hash ^= (hash >> 6); hash ^= (hash >> 6);
hash += config->wrapMode;
hash += (hash << 10);
hash ^= (hash >> 6);
hash += (hash << 3); hash += (hash << 3);
hash ^= (hash >> 11); hash ^= (hash >> 11);
hash += (hash << 15); hash += (hash << 15);
@ -1423,7 +1529,7 @@ Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_Text
return &Clay__MeasureTextCacheItem_DEFAULT; return &Clay__MeasureTextCacheItem_DEFAULT;
} }
#endif #endif
uint32_t id = Clay__HashTextWithConfig(text, config); uint32_t id = Clay__HashStringContentsWithConfig(text, config);
uint32_t hashBucket = id % (context->maxMeasureTextCacheWordCount / 32); uint32_t hashBucket = id % (context->maxMeasureTextCacheWordCount / 32);
int32_t elementIndexPrevious = 0; int32_t elementIndexPrevious = 0;
int32_t elementIndex = context->measureTextHashMap.internalArray[hashBucket]; int32_t elementIndex = context->measureTextHashMap.internalArray[hashBucket];
@ -2700,14 +2806,24 @@ void Clay__CalculateFinalLayout(void) {
continue; continue;
} }
float offset = (currentElementBoundingBox.width - wrappedLine->dimensions.width); float offset = (currentElementBoundingBox.width - wrappedLine->dimensions.width);
if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_LEFT) { if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_LEFT || textElementConfig->textAlignment == CLAY_TEXT_ALIGN_SHRINK) {
offset = 0; offset = 0;
} }
if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) { if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_CENTER) {
offset /= 2; offset /= 2;
} }
Clay_BoundingBox textBoundingBox = {
currentElementBoundingBox.x + offset,
currentElementBoundingBox.y + yPosition,
wrappedLine->dimensions.width,
wrappedLine->dimensions.height
};
if (textElementConfig->textAlignment == CLAY_TEXT_ALIGN_SHRINK && boundingBox.width > currentElementBoundingBox.width) {
boundingBox.width = currentElementBoundingBox.width;
}
Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) { Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
.boundingBox = { currentElementBoundingBox.x + offset, currentElementBoundingBox.y + yPosition, wrappedLine->dimensions.width, wrappedLine->dimensions.height }, .boundingBox = { currentElementBoundingBox.x + offset, currentElementBoundingBox.y + yPosition, wrappedLine->dimensions.width, wrappedLine->dimensions.height },
.boundingBox = textBoundingBox,
.renderData = { .text = { .renderData = { .text = {
.stringContents = CLAY__INIT(Clay_StringSlice) { .length = wrappedLine->line.length, .chars = wrappedLine->line.chars, .baseChars = currentElement->childrenOrTextContent.textElementData->text.chars }, .stringContents = CLAY__INIT(Clay_StringSlice) { .length = wrappedLine->line.length, .chars = wrappedLine->line.chars, .baseChars = currentElement->childrenOrTextContent.textElementData->text.chars },
.textColor = textElementConfig->textColor, .textColor = textElementConfig->textColor,