diff --git a/README.md b/README.md index 8a6a55e..352069d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### Major Features - Microsecond layout performance - Flex-box like layout model for complex, responsive layouts including text wrapping, scrolling containers and aspect ratio scaling -- Single ~2k LOC **clay.h** file with **zero** dependencies (including no standard library) +- Single ~4k LOC **clay.h** file with **zero** dependencies (including no standard library) - Wasm support: compile with clang to a 15kb uncompressed **.wasm** file for use in the browser - Static arena based memory use with no malloc / free, and low total memory overhead (e.g. ~3.5mb for 8192 layout elements). - React-like nested declarative syntax diff --git a/bindings/odin/clay-odin/clay.odin b/bindings/odin/clay-odin/clay.odin index 3de636f..4f88bd3 100644 --- a/bindings/odin/clay-odin/clay.odin +++ b/bindings/odin/clay-odin/clay.odin @@ -17,6 +17,7 @@ when ODIN_OS == .Windows { } String :: struct { + isStaticallyAllocated: c.bool, length: c.int32_t, chars: [^]c.char, } @@ -102,6 +103,7 @@ TextAlignment :: enum EnumBackingType { } TextElementConfig :: struct { + userData: rawptr, textColor: Color, fontId: u16, fontSize: u16, @@ -172,7 +174,7 @@ FloatingElementConfig :: struct { offset: Vector2, expand: Dimensions, parentId: u32, - zIndex: i32, + zIndex: i16, attachment: FloatingAttachPoints, pointerCaptureMode: PointerCaptureMode, attachTo: FloatingAttachToElement, @@ -366,6 +368,8 @@ Context :: struct {} // opaque structure, only use as a pointer @(link_prefix = "Clay_", default_calling_convention = "c") foreign Clay { + _OpenElement :: proc() --- + _CloseElement :: proc() --- MinMemorySize :: proc() -> u32 --- CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena --- SetPointerState :: proc(position: Vector2, pointerDown: bool) --- @@ -398,9 +402,7 @@ foreign Clay { @(link_prefix = "Clay_", default_calling_convention = "c", private) foreign Clay { - _OpenElement :: proc() --- _ConfigureOpenElement :: proc(config: ElementDeclaration) --- - _CloseElement :: proc() --- _HashString :: proc(key: String, offset: u32, seed: u32) -> ElementId --- _OpenTextElement :: proc(text: String, textConfig: ^TextElementConfig) --- _StoreTextElementConfig :: proc(config: TextElementConfig) -> ^TextElementConfig --- @@ -418,7 +420,13 @@ UI :: proc() -> proc (config: ElementDeclaration) -> bool { 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) } @@ -457,3 +465,7 @@ MakeString :: proc(label: string) -> String { ID :: proc(label: string, index: u32 = 0) -> ElementId { return _HashString(MakeString(label), index, 0) } + +ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId { + return _HashString(MakeString(label), index, _GetParentElementId()) +} diff --git a/bindings/odin/clay-odin/linux/clay.a b/bindings/odin/clay-odin/linux/clay.a index e6e9f5e..9a283bd 100644 Binary files a/bindings/odin/clay-odin/linux/clay.a and b/bindings/odin/clay-odin/linux/clay.a differ diff --git a/bindings/odin/clay-odin/macos-arm64/clay.a b/bindings/odin/clay-odin/macos-arm64/clay.a index af30258..ff3339c 100644 Binary files a/bindings/odin/clay-odin/macos-arm64/clay.a and b/bindings/odin/clay-odin/macos-arm64/clay.a differ diff --git a/bindings/odin/clay-odin/macos/clay.a b/bindings/odin/clay-odin/macos/clay.a index 542be49..b978de3 100644 Binary files a/bindings/odin/clay-odin/macos/clay.a and b/bindings/odin/clay-odin/macos/clay.a differ diff --git a/bindings/odin/clay-odin/wasm/clay.o b/bindings/odin/clay-odin/wasm/clay.o index 2207763..2644c07 100644 Binary files a/bindings/odin/clay-odin/wasm/clay.o and b/bindings/odin/clay-odin/wasm/clay.o differ diff --git a/bindings/odin/clay-odin/windows/clay.lib b/bindings/odin/clay-odin/windows/clay.lib index ca2699a..f8dc37b 100644 Binary files a/bindings/odin/clay-odin/windows/clay.lib and b/bindings/odin/clay-odin/windows/clay.lib differ diff --git a/bindings/odin/examples/clay-official-website/clay-official-website.odin b/bindings/odin/examples/clay-official-website/clay-official-website.odin index d96b01b..fde0c6a 100644 --- a/bindings/odin/examples/clay-official-website/clay-official-website.odin +++ b/bindings/odin/examples/clay-official-website/clay-official-website.odin @@ -62,7 +62,7 @@ border2pxRed := clay.BorderElementConfig { 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()({ id = clay.ID("HeroBlob", index), 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} } -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) { 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()({ layout = { sizing = { width = clay.SizingFixed(300) }, padding = clay.PaddingAll(16) }, 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()({ id = clay.ID("RendererButtonInactiveInner", index), @@ -489,7 +489,7 @@ errorHandler :: proc "c" (errorData: clay.ErrorData) { } main :: proc() { - minMemorySize: u32 = clay.MinMemorySize() + minMemorySize: c.size_t = cast(c.size_t)clay.MinMemorySize() memory := make([^]u8, minMemorySize) arena: clay.Arena = clay.CreateArenaWithCapacityAndMemory(minMemorySize, memory) clay.Initialize(arena, {cast(f32)raylib.GetScreenWidth(), cast(f32)raylib.GetScreenHeight()}, { handler = errorHandler }) diff --git a/clay.h b/clay.h index 37d3c80..721c0fa 100644 --- a/clay.h +++ b/clay.h @@ -1,4 +1,4 @@ -// VERSION: 0.12 +// VERSION: 0.13 /* NOTE: In order to use this library you must define @@ -96,12 +96,16 @@ #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) -#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; +// GCC marks the above CLAY__ELEMENT_DEFINITION_LATCH as an unused variable for files that include clay.h but don't declare any layout +// This is to suppress that warning +static inline void Clay__SuppressUnusedLatchDefinitionVariableWarning(void) { (void) CLAY__ELEMENT_DEFINITION_LATCH; } + // Publicly visible layout element macros ----------------------------------------------------- /* This macro looks scary on the surface, but is actually quite simple. @@ -181,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, // but it is also used to represent slices. 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; // The underlying character memory. Note: this will not be copied and will not extend the lifetime of the underlying memory. const char *chars; @@ -266,7 +273,7 @@ typedef CLAY_PACKED_ENUM { CLAY_ALIGN_Y_TOP, // Aligns child elements to the bottom of this element, offset by padding.width.bottom CLAY_ALIGN_Y_BOTTOM, - // Aligns child elements vertiically to the center of this element + // Aligns child elements vertically to the center of this element CLAY_ALIGN_Y_CENTER, } Clay_LayoutAlignmentY; @@ -357,6 +364,8 @@ typedef CLAY_PACKED_ENUM { // Controls various functionality related to text elements. typedef struct { + // A pointer that will be transparently passed through to the resulting render command. + void *userData; // The RGBA color of the font to render, conventionally specified as 0-255. Clay_Color textColor; // An integer transparently passed to Clay_MeasureText to identify the font to use. @@ -378,10 +387,6 @@ typedef struct { // 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_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__WRAPPER_STRUCT(Clay_TextElementConfig); @@ -870,14 +875,14 @@ 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. // This may require reallocating additional memory, and re-calling Clay_Initialize(); 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. -// Similar behaviour can be achieved on an individual text element level by using Clay_TextElementConfig.hashStringContents +// Resets Clay's internal text measurement cache. Useful if font mappings have changed or fonts have been reloaded. CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void); // Internal API functions required by macros ---------------------- CLAY_DLL_EXPORT void Clay__OpenElement(void); CLAY_DLL_EXPORT void Clay__ConfigureOpenElement(const Clay_ElementDeclaration config); +CLAY_DLL_EXPORT void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *config); CLAY_DLL_EXPORT void Clay__CloseElement(void); CLAY_DLL_EXPORT Clay_ElementId Clay__HashString(Clay_String key, uint32_t offset, uint32_t seed); CLAY_DLL_EXPORT void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig *textConfig); @@ -1149,6 +1154,7 @@ CLAY__ARRAY_DEFINE(Clay__MeasuredWord, Clay__MeasuredWordArray) typedef struct { Clay_Dimensions unwrappedDimensions; int32_t measuredWordsStartIndex; + float minWidth; bool containsNewlines; // Hash map data uint32_t id; @@ -1340,26 +1346,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" } -uint32_t Clay__HashTextWithConfig(Clay_String *text, Clay_TextElementConfig *config) { - uint32_t hash = 0; - uintptr_t pointerAsNumber = (uintptr_t)text->chars; +#if !defined(CLAY_DISABLE_SIMD) && (defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)) +static inline __m128i Clay__SIMDRotateLeft(__m128i x, int r) { + return _mm_or_si128(_mm_slli_epi64(x, r), _mm_srli_epi64(x, 64 - r)); +} - if (config->hashStringContents) { - uint32_t maxLengthToHash = CLAY__MIN(text->length, 256); - for (uint32_t i = 0; i < maxLengthToHash; i++) { - hash += text->chars[i]; - hash += (hash << 10); - hash ^= (hash >> 6); +static inline void Clay__SIMDARXMix(__m128i* a, __m128i* b) { + *a = _mm_add_epi64(*a, *b); + *b = _mm_xor_si128(Clay__SIMDRotateLeft(*b, 17), *a); +} + +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 { - hash += pointerAsNumber; + else { + 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 >> 6); } + return hash; +} +#endif - hash += text->length; - hash += (hash << 10); - hash ^= (hash >> 6); +uint32_t Clay__HashStringContentsWithConfig(Clay_String *text, Clay_TextElementConfig *config) { + uint32_t hash = 0; + 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 += (hash << 10); @@ -1369,18 +1489,10 @@ uint32_t Clay__HashTextWithConfig(Clay_String *text, Clay_TextElementConfig *con hash += (hash << 10); hash ^= (hash >> 6); - hash += config->lineHeight; - hash += (hash << 10); - hash ^= (hash >> 6); - hash += config->letterSpacing; hash += (hash << 10); hash ^= (hash >> 6); - hash += config->wrapMode; - hash += (hash << 10); - hash ^= (hash >> 6); - hash += (hash << 3); hash ^= (hash >> 11); hash += (hash << 15); @@ -1415,7 +1527,7 @@ Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_Text return &Clay__MeasureTextCacheItem_DEFAULT; } #endif - uint32_t id = Clay__HashTextWithConfig(text, config); + uint32_t id = Clay__HashStringContentsWithConfig(text, config); uint32_t hashBucket = id % (context->maxMeasureTextCacheWordCount / 32); int32_t elementIndexPrevious = 0; int32_t elementIndex = context->measureTextHashMap.internalArray[hashBucket]; @@ -1497,6 +1609,7 @@ Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_Text if (current == ' ' || current == '\n') { int32_t length = end - start; Clay_Dimensions dimensions = Clay__MeasureText(CLAY__INIT(Clay_StringSlice) { .length = length, .chars = &text->chars[start], .baseChars = text->chars }, config, context->measureTextUserData); + measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth); measuredHeight = CLAY__MAX(measuredHeight, dimensions.height); if (current == ' ') { dimensions.width += spaceWidth; @@ -1522,6 +1635,7 @@ Clay__MeasureTextCacheItem *Clay__MeasureTextCached(Clay_String *text, Clay_Text Clay__AddMeasuredWord(CLAY__INIT(Clay__MeasuredWord) { .startOffset = start, .length = end - start, .width = dimensions.width, .next = -1 }, previousWord); lineWidth += dimensions.width; measuredHeight = CLAY__MAX(measuredHeight, dimensions.height); + measured->minWidth = CLAY__MAX(dimensions.width, measured->minWidth); } measuredWidth = CLAY__MAX(lineWidth, measuredWidth); @@ -1651,24 +1765,30 @@ void Clay__CloseElement(void) { elementHasScrollVertical = config->config.scrollElementConfig->vertical; context->openClipElementStack.length--; break; + } else if (config->type == CLAY__ELEMENT_CONFIG_TYPE_FLOATING) { + context->openClipElementStack.length--; } } + float leftRightPadding = (float)(layoutConfig->padding.left + layoutConfig->padding.right); + float topBottomPadding = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom); + // Attach children to the current open element openLayoutElement->childrenOrTextContent.children.elements = &context->layoutElementChildren.internalArray[context->layoutElementChildren.length]; if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) { - openLayoutElement->dimensions.width = (float)(layoutConfig->padding.left + layoutConfig->padding.right); + openLayoutElement->dimensions.width = leftRightPadding; + openLayoutElement->minDimensions.width = leftRightPadding; for (int32_t i = 0; i < openLayoutElement->childrenOrTextContent.children.length; i++) { int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->childrenOrTextContent.children.length + i); Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex); openLayoutElement->dimensions.width += child->dimensions.width; - openLayoutElement->dimensions.height = CLAY__MAX(openLayoutElement->dimensions.height, child->dimensions.height + layoutConfig->padding.top + layoutConfig->padding.bottom); + openLayoutElement->dimensions.height = CLAY__MAX(openLayoutElement->dimensions.height, child->dimensions.height + topBottomPadding); // Minimum size of child elements doesn't matter to scroll containers as they can shrink and hide their contents if (!elementHasScrollHorizontal) { openLayoutElement->minDimensions.width += child->minDimensions.width; } if (!elementHasScrollVertical) { - openLayoutElement->minDimensions.height = CLAY__MAX(openLayoutElement->minDimensions.height, child->minDimensions.height + layoutConfig->padding.top + layoutConfig->padding.bottom); + openLayoutElement->minDimensions.height = CLAY__MAX(openLayoutElement->minDimensions.height, child->minDimensions.height + topBottomPadding); } Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex); } @@ -1677,18 +1797,19 @@ void Clay__CloseElement(void) { openLayoutElement->minDimensions.width += childGap; } else if (layoutConfig->layoutDirection == CLAY_TOP_TO_BOTTOM) { - openLayoutElement->dimensions.height = (float)(layoutConfig->padding.top + layoutConfig->padding.bottom); + openLayoutElement->dimensions.height = topBottomPadding; + openLayoutElement->minDimensions.height = topBottomPadding; for (int32_t i = 0; i < openLayoutElement->childrenOrTextContent.children.length; i++) { int32_t childIndex = Clay__int32_tArray_GetValue(&context->layoutElementChildrenBuffer, (int)context->layoutElementChildrenBuffer.length - openLayoutElement->childrenOrTextContent.children.length + i); Clay_LayoutElement *child = Clay_LayoutElementArray_Get(&context->layoutElements, childIndex); openLayoutElement->dimensions.height += child->dimensions.height; - openLayoutElement->dimensions.width = CLAY__MAX(openLayoutElement->dimensions.width, child->dimensions.width + layoutConfig->padding.left + layoutConfig->padding.right); + openLayoutElement->dimensions.width = CLAY__MAX(openLayoutElement->dimensions.width, child->dimensions.width + leftRightPadding); // Minimum size of child elements doesn't matter to scroll containers as they can shrink and hide their contents if (!elementHasScrollVertical) { openLayoutElement->minDimensions.height += child->minDimensions.height; } if (!elementHasScrollHorizontal) { - openLayoutElement->minDimensions.width = CLAY__MAX(openLayoutElement->minDimensions.width, child->minDimensions.width + layoutConfig->padding.left + layoutConfig->padding.right); + openLayoutElement->minDimensions.width = CLAY__MAX(openLayoutElement->minDimensions.width, child->minDimensions.width + leftRightPadding); } Clay__int32_tArray_Add(&context->layoutElementChildren, childIndex); } @@ -1769,7 +1890,7 @@ bool Clay__MemCmp(const char *s1, const char *s2, int32_t length); uint8x16_t v2 = vld1q_u8((const uint8_t *)s2); // Compare vectors - if (vminvq_u32(vceqq_u8(v1, v2)) != 0xFFFFFFFF) { // If there's a difference + if (vminvq_u32(vreinterpretq_u32_u8(vceqq_u8(v1, v2))) != 0xFFFFFFFF) { // If there's a difference return false; } @@ -1840,7 +1961,7 @@ void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig *textConfig) Clay__StringArray_Add(&context->layoutElementIdStrings, elementId.stringId); Clay_Dimensions textDimensions = { .width = textMeasured->unwrappedDimensions.width, .height = textConfig->lineHeight > 0 ? (float)textConfig->lineHeight : textMeasured->unwrappedDimensions.height }; textElement->dimensions = textDimensions; - textElement->minDimensions = CLAY__INIT(Clay_Dimensions) { .width = textMeasured->unwrappedDimensions.height, .height = textDimensions.height }; // TODO not sure this is the best way to decide min width for text + 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->elementConfigs = CLAY__INIT(Clay__ElementConfigArraySlice) { .length = 1, @@ -1863,58 +1984,58 @@ Clay_ElementId Clay__AttachId(Clay_ElementId elementId) { return elementId; } -void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { +void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) { Clay_Context* context = Clay_GetCurrentContext(); Clay_LayoutElement *openLayoutElement = Clay__GetOpenLayoutElement(); - openLayoutElement->layoutConfig = Clay__StoreLayoutConfig(declaration.layout); - if ((declaration.layout.sizing.width.type == CLAY__SIZING_TYPE_PERCENT && declaration.layout.sizing.width.size.percent > 1) || (declaration.layout.sizing.height.type == CLAY__SIZING_TYPE_PERCENT && declaration.layout.sizing.height.size.percent > 1)) { + openLayoutElement->layoutConfig = Clay__StoreLayoutConfig(declaration->layout); + if ((declaration->layout.sizing.width.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.width.size.percent > 1) || (declaration->layout.sizing.height.type == CLAY__SIZING_TYPE_PERCENT && declaration->layout.sizing.height.size.percent > 1)) { context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { .errorType = CLAY_ERROR_TYPE_PERCENTAGE_OVER_1, .errorText = CLAY_STRING("An element was configured with CLAY_SIZING_PERCENT, but the provided percentage value was over 1.0. Clay expects a value between 0 and 1, i.e. 20% is 0.2."), .userData = context->errorHandler.userData }); } - Clay_ElementId openLayoutElementId = declaration.id; + Clay_ElementId openLayoutElementId = declaration->id; openLayoutElement->elementConfigs.internalArray = &context->elementConfigs.internalArray[context->elementConfigs.length]; Clay_SharedElementConfig *sharedConfig = NULL; - if (declaration.backgroundColor.a > 0) { - sharedConfig = Clay__StoreSharedElementConfig(CLAY__INIT(Clay_SharedElementConfig) { .backgroundColor = declaration.backgroundColor }); + if (declaration->backgroundColor.a > 0) { + sharedConfig = Clay__StoreSharedElementConfig(CLAY__INIT(Clay_SharedElementConfig) { .backgroundColor = declaration->backgroundColor }); Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .sharedElementConfig = sharedConfig }, CLAY__ELEMENT_CONFIG_TYPE_SHARED); } - if (!Clay__MemCmp((char *)(&declaration.cornerRadius), (char *)(&Clay__CornerRadius_DEFAULT), sizeof(Clay_CornerRadius))) { + if (!Clay__MemCmp((char *)(&declaration->cornerRadius), (char *)(&Clay__CornerRadius_DEFAULT), sizeof(Clay_CornerRadius))) { if (sharedConfig) { - sharedConfig->cornerRadius = declaration.cornerRadius; + sharedConfig->cornerRadius = declaration->cornerRadius; } else { - sharedConfig = Clay__StoreSharedElementConfig(CLAY__INIT(Clay_SharedElementConfig) { .cornerRadius = declaration.cornerRadius }); + sharedConfig = Clay__StoreSharedElementConfig(CLAY__INIT(Clay_SharedElementConfig) { .cornerRadius = declaration->cornerRadius }); Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .sharedElementConfig = sharedConfig }, CLAY__ELEMENT_CONFIG_TYPE_SHARED); } } - if (declaration.userData != 0) { + if (declaration->userData != 0) { if (sharedConfig) { - sharedConfig->userData = declaration.userData; + sharedConfig->userData = declaration->userData; } else { - sharedConfig = Clay__StoreSharedElementConfig(CLAY__INIT(Clay_SharedElementConfig) { .userData = declaration.userData }); + sharedConfig = Clay__StoreSharedElementConfig(CLAY__INIT(Clay_SharedElementConfig) { .userData = declaration->userData }); Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .sharedElementConfig = sharedConfig }, CLAY__ELEMENT_CONFIG_TYPE_SHARED); } } - if (declaration.image.imageData) { - Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .imageElementConfig = Clay__StoreImageElementConfig(declaration.image) }, CLAY__ELEMENT_CONFIG_TYPE_IMAGE); + if (declaration->image.imageData) { + Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .imageElementConfig = Clay__StoreImageElementConfig(declaration->image) }, CLAY__ELEMENT_CONFIG_TYPE_IMAGE); Clay__int32_tArray_Add(&context->imageElementPointers, context->layoutElements.length - 1); } - if (declaration.floating.attachTo != CLAY_ATTACH_TO_NONE) { - Clay_FloatingElementConfig floatingConfig = declaration.floating; + if (declaration->floating.attachTo != CLAY_ATTACH_TO_NONE) { + Clay_FloatingElementConfig floatingConfig = declaration->floating; // This looks dodgy but because of the auto generated root element the depth of the tree will always be at least 2 here Clay_LayoutElement *hierarchicalParent = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 2)); if (hierarchicalParent) { uint32_t clipElementId = 0; - if (declaration.floating.attachTo == CLAY_ATTACH_TO_PARENT) { + if (declaration->floating.attachTo == CLAY_ATTACH_TO_PARENT) { // Attach to the element's direct hierarchical parent floatingConfig.parentId = hierarchicalParent->id; if (context->openClipElementStack.length > 0) { clipElementId = Clay__int32_tArray_GetValue(&context->openClipElementStack, (int)context->openClipElementStack.length - 1); } - } else if (declaration.floating.attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) { + } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ELEMENT_WITH_ID) { Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingConfig.parentId); if (!parentItem) { context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { @@ -1922,14 +2043,17 @@ void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { .errorText = CLAY_STRING("A floating element was declared with a parentId, but no element with that ID was found."), .userData = context->errorHandler.userData }); } else { - clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, parentItem->layoutElement - context->layoutElements.internalArray); + clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(parentItem->layoutElement - context->layoutElements.internalArray)); } - } else if (declaration.floating.attachTo == CLAY_ATTACH_TO_ROOT) { + } else if (declaration->floating.attachTo == CLAY_ATTACH_TO_ROOT) { floatingConfig.parentId = Clay__HashString(CLAY_STRING("Clay__RootContainer"), 0, 0).id; } if (!openLayoutElementId.id) { openLayoutElementId = Clay__HashString(CLAY_STRING("Clay__FloatingContainer"), context->layoutElementTreeRoots.length, 0); } + int32_t currentElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1); + Clay__int32_tArray_Set(&context->layoutElementClipElementIds, currentElementIndex, clipElementId); + Clay__int32_tArray_Add(&context->openClipElementStack, clipElementId); Clay__LayoutElementTreeRootArray_Add(&context->layoutElementTreeRoots, CLAY__INIT(Clay__LayoutElementTreeRoot) { .layoutElementIndex = Clay__int32_tArray_GetValue(&context->openLayoutElementStack, context->openLayoutElementStack.length - 1), .parentId = floatingConfig.parentId, @@ -1939,8 +2063,8 @@ void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .floatingElementConfig = Clay__StoreFloatingElementConfig(floatingConfig) }, CLAY__ELEMENT_CONFIG_TYPE_FLOATING); } } - if (declaration.custom.customData) { - Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .customElementConfig = Clay__StoreCustomElementConfig(declaration.custom) }, CLAY__ELEMENT_CONFIG_TYPE_CUSTOM); + if (declaration->custom.customData) { + Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .customElementConfig = Clay__StoreCustomElementConfig(declaration->custom) }, CLAY__ELEMENT_CONFIG_TYPE_CUSTOM); } if (openLayoutElementId.id != 0) { @@ -1949,8 +2073,8 @@ void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { openLayoutElementId = Clay__GenerateIdForAnonymousElement(openLayoutElement); } - if (declaration.scroll.horizontal | declaration.scroll.vertical) { - Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .scrollElementConfig = Clay__StoreScrollElementConfig(declaration.scroll) }, CLAY__ELEMENT_CONFIG_TYPE_SCROLL); + if (declaration->scroll.horizontal | declaration->scroll.vertical) { + Clay__AttachElementConfig(CLAY__INIT(Clay_ElementConfigUnion) { .scrollElementConfig = Clay__StoreScrollElementConfig(declaration->scroll) }, CLAY__ELEMENT_CONFIG_TYPE_SCROLL); Clay__int32_tArray_Add(&context->openClipElementStack, (int)openLayoutElement->id); // Retrieve or create cached data to track scroll position across frames Clay__ScrollContainerDataInternal *scrollOffset = CLAY__NULL; @@ -1969,11 +2093,15 @@ void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { scrollOffset->scrollPosition = Clay__QueryScrollOffset(scrollOffset->elementId, context->queryScrollOffsetUserData); } } - 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->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); } } +void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { + Clay__ConfigureOpenElementPtr(&declaration); +} + void Clay__InitializeEphemeralMemory(Clay_Context* context) { int32_t maxElementCount = context->maxElementCount; // Ephemeral Memory - reset every frame @@ -2221,25 +2349,25 @@ void Clay__SizeContainersAlongAxis(bool xAxis) { for (int32_t childOffset = 0; childOffset < resizableContainerBuffer.length; childOffset++) { Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&resizableContainerBuffer, childOffset)); Clay_SizingAxis childSizing = xAxis ? childElement->layoutConfig->sizing.width : childElement->layoutConfig->sizing.height; + float minSize = xAxis ? childElement->minDimensions.width : childElement->minDimensions.height; float *childSize = xAxis ? &childElement->dimensions.width : &childElement->dimensions.height; if (!xAxis && Clay__ElementHasConfig(childElement, CLAY__ELEMENT_CONFIG_TYPE_IMAGE)) { continue; // Currently we don't support resizing aspect ratio images on the Y axis because it would break the ratio } - // If we're laying out the children of a scroll panel, grow containers expand to the height of the inner content, not the outer container float maxSize = parentSize - parentPadding; + // If we're laying out the children of a scroll panel, grow containers expand to the size of the inner content, not the outer container if (Clay__ElementHasConfig(parent, CLAY__ELEMENT_CONFIG_TYPE_SCROLL)) { Clay_ScrollElementConfig *scrollElementConfig = Clay__FindElementConfigWithType(parent, CLAY__ELEMENT_CONFIG_TYPE_SCROLL).scrollElementConfig; if (((xAxis && scrollElementConfig->horizontal) || (!xAxis && scrollElementConfig->vertical))) { maxSize = CLAY__MAX(maxSize, innerContentSize); } } - if (childSizing.type == CLAY__SIZING_TYPE_FIT) { - *childSize = CLAY__MAX(childSizing.size.minMax.min, CLAY__MIN(*childSize, maxSize)); - } else if (childSizing.type == CLAY__SIZING_TYPE_GROW) { + if (childSizing.type == CLAY__SIZING_TYPE_GROW) { *childSize = CLAY__MIN(maxSize, childSizing.size.minMax.max); } + *childSize = CLAY__MAX(minSize, CLAY__MIN(*childSize, maxSize)); } } } @@ -2692,7 +2820,7 @@ void Clay__CalculateFinalLayout(void) { .letterSpacing = textElementConfig->letterSpacing, .lineHeight = textElementConfig->lineHeight, }}, - .userData = sharedConfig->userData, + .userData = textElementConfig->userData, .id = Clay__HashNumber(lineIndex, currentElement->id).id, .zIndex = root->zIndex, .commandType = CLAY_RENDER_COMMAND_TYPE_TEXT, @@ -3627,7 +3755,7 @@ uint32_t Clay_MinMemorySize(void) { Clay__Context_Allocate_Arena(&fakeContext.internalArena); Clay__InitializePersistentMemory(&fakeContext); Clay__InitializeEphemeralMemory(&fakeContext); - return fakeContext.internalArena.nextAllocation + 128; + return (uint32_t)fakeContext.internalArena.nextAllocation + 128; } CLAY_WASM_EXPORT("Clay_CreateArenaWithCapacityAndMemory") @@ -3680,7 +3808,7 @@ void Clay_SetPointerState(Clay_Vector2 position, bool isPointerDown) { context->treeNodeVisited.internalArray[dfsBuffer.length - 1] = true; Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&context->layoutElements, Clay__int32_tArray_GetValue(&dfsBuffer, (int)dfsBuffer.length - 1)); Clay_LayoutElementHashMapItem *mapItem = Clay__GetHashMapItem(currentElement->id); // TODO think of a way around this, maybe the fact that it's essentially a binary tree limits the cost, but the worst case is not great - int32_t clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, currentElement - context->layoutElements.internalArray); + int32_t clipElementId = Clay__int32_tArray_GetValue(&context->layoutElementClipElementIds, (int32_t)(currentElement - context->layoutElements.internalArray)); Clay_LayoutElementHashMapItem *clipItem = Clay__GetHashMapItem(clipElementId); Clay_BoundingBox elementBox = mapItem->boundingBox; elementBox.x -= root->pointerOffset.x; diff --git a/examples/SDL2-video-demo/main.c b/examples/SDL2-video-demo/main.c index cc2d2f7..b4ec8e7 100644 --- a/examples/SDL2-video-demo/main.c +++ b/examples/SDL2-video-demo/main.c @@ -17,6 +17,41 @@ void HandleClayErrors(Clay_ErrorData errorData) { printf("%s", errorData.errorText.chars); } + +struct ResizeRenderData_ { + SDL_Window* window; + int windowWidth; + int windowHeight; + ClayVideoDemo_Data demoData; + SDL_Renderer* renderer; + SDL2_Font* fonts; +}; +typedef struct ResizeRenderData_ ResizeRenderData; + +int resizeRendering(void* userData, SDL_Event* event) { + ResizeRenderData *actualData = userData; + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_EXPOSED) { + SDL_Window* window = actualData->window; + int windowWidth = actualData->windowWidth; + int windowHeight = actualData->windowHeight; + ClayVideoDemo_Data demoData = actualData->demoData; + SDL_Renderer* renderer = actualData->renderer; + SDL2_Font* fonts = actualData->fonts; + + SDL_GetWindowSize(window, &windowWidth, &windowHeight); + Clay_SetLayoutDimensions((Clay_Dimensions) { (float)windowWidth, (float)windowHeight }); + + Clay_RenderCommandArray renderCommands = ClayVideoDemo_CreateLayout(&demoData); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + Clay_SDL2_Render(renderer, renderCommands, fonts); + + SDL_RenderPresent(renderer); + } + return 0; +} + int main(int argc, char *argv[]) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { fprintf(stderr, "Error: could not initialize SDL: %s\n", SDL_GetError()); @@ -73,6 +108,18 @@ int main(int argc, char *argv[]) { double deltaTime = 0; ClayVideoDemo_Data demoData = ClayVideoDemo_Initialize(); + + ResizeRenderData userData = { + window, // SDL_Window* + windowWidth, // int + windowHeight, // int + demoData, // CustomShit + renderer, // SDL_Renderer* + fonts // SDL2_Font[1] + }; + // add an event watcher that will render the screen while youre dragging the window to different sizes + SDL_AddEventWatch(resizeRendering, &userData); + while (true) { Clay_Vector2 scrollDelta = {}; SDL_Event event; diff --git a/examples/clay-official-website/build/clay/index.html b/examples/clay-official-website/build/clay/index.html index 1e07d31..e626e4b 100644 --- a/examples/clay-official-website/build/clay/index.html +++ b/examples/clay-official-website/build/clay/index.html @@ -119,6 +119,7 @@ {name: 'bottomRight', type: 'float'}, ]}; let textConfigDefinition = { name: 'text', type: 'struct', members: [ + { name: 'userData', type: 'uint32_t' }, { name: 'textColor', ...colorDefinition }, { name: 'fontId', type: 'uint16_t' }, { name: 'fontSize', type: 'uint16_t' }, diff --git a/examples/clay-official-website/build/clay/index.wasm b/examples/clay-official-website/build/clay/index.wasm index 5e94900..e57e631 100755 Binary files a/examples/clay-official-website/build/clay/index.wasm and b/examples/clay-official-website/build/clay/index.wasm differ diff --git a/examples/clay-official-website/index.html b/examples/clay-official-website/index.html index 1e07d31..e626e4b 100644 --- a/examples/clay-official-website/index.html +++ b/examples/clay-official-website/index.html @@ -119,6 +119,7 @@ {name: 'bottomRight', type: 'float'}, ]}; let textConfigDefinition = { name: 'text', type: 'struct', members: [ + { name: 'userData', type: 'uint32_t' }, { name: 'textColor', ...colorDefinition }, { name: 'fontId', type: 'uint16_t' }, { name: 'fontSize', type: 'uint16_t' }, diff --git a/examples/introducing-clay-video-demo/main.c b/examples/introducing-clay-video-demo/main.c index 5456ebc..89dd50f 100644 --- a/examples/introducing-clay-video-demo/main.c +++ b/examples/introducing-clay-video-demo/main.c @@ -50,4 +50,6 @@ int main(void) { Clay_Raylib_Render(renderCommands, fonts); EndDrawing(); } + // This function is new since the video was published + Clay_Raylib_Close(); } diff --git a/examples/raylib-multi-context/main.c b/examples/raylib-multi-context/main.c index b2163ba..dfbc1a3 100644 --- a/examples/raylib-multi-context/main.c +++ b/examples/raylib-multi-context/main.c @@ -72,4 +72,6 @@ int main(void) { Clay_Raylib_Render(renderCommandsBottom, fonts); EndDrawing(); } + + Clay_Raylib_Close(); } diff --git a/examples/raylib-sidebar-scrolling-container/main.c b/examples/raylib-sidebar-scrolling-container/main.c index 4ea1cb7..21f30c5 100644 --- a/examples/raylib-sidebar-scrolling-container/main.c +++ b/examples/raylib-sidebar-scrolling-container/main.c @@ -250,5 +250,6 @@ int main(void) { } UpdateDrawFrame(fonts); } + Clay_Raylib_Close(); return 0; } diff --git a/examples/win32_gdi/build.ps1 b/examples/win32_gdi/build.ps1 new file mode 100644 index 0000000..ee75bf4 --- /dev/null +++ b/examples/win32_gdi/build.ps1 @@ -0,0 +1,4 @@ + +# to build this, install mingw + +gcc main.c -ggdb -omain -lgdi32 -lmingw32 # -mwindows # comment -mwindows out for console output \ No newline at end of file diff --git a/examples/win32_gdi/main.c b/examples/win32_gdi/main.c new file mode 100644 index 0000000..761c59e --- /dev/null +++ b/examples/win32_gdi/main.c @@ -0,0 +1,218 @@ + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include + +#include + +#include "../../renderers/win32_gdi/clay_renderer_gdi.c" + +#define CLAY_IMPLEMENTATION +#include "../../clay.h" + +#include "../shared-layouts/clay-video-demo.c" + +ClayVideoDemo_Data demo_data; + +#define APPNAME "Clay GDI Example" +char szAppName[] = APPNAME; // The name of this application +char szTitle[] = APPNAME; // The title bar text + +void CenterWindow(HWND hWnd); + +long lastMsgTime = 0; +bool ui_debug_mode; + +LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + + switch (message) + { + + // ----------------------- first and last + case WM_CREATE: + CenterWindow(hwnd); + break; + + case WM_DESTROY: + PostQuitMessage(0); + break; + + case WM_MOUSEWHEEL: // scrolling data + { + short zDelta = GET_WHEEL_DELTA_WPARAM(wParam); + // todo: i think GetMessageTime can roll over, so something like if(lastmsgtime > now) ... may be needed + long now = GetMessageTime(); + float dt = (now - lastMsgTime) / 1000.00; + + lastMsgTime = now; + + // little hacky hack to make scrolling *feel* right + if (abs(zDelta) > 100) + { + zDelta = zDelta * .012; + } + + Clay_UpdateScrollContainers(true, (Clay_Vector2){.x = 0, .y = zDelta}, dt); + + InvalidateRect(hwnd, NULL, false); // force a wm_paint event + break; + } + case WM_RBUTTONUP: + case WM_LBUTTONUP: + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_MOUSEMOVE: // mouse events + { + short mouseX = GET_X_LPARAM(lParam); + short mouseY = GET_Y_LPARAM(lParam); + short mouseButtons = LOWORD(wParam); + + Clay_SetPointerState((Clay_Vector2){mouseX, mouseY}, mouseButtons & 0b01); + + InvalidateRect(hwnd, NULL, false); // force a wm_paint event + break; + } + + case WM_SIZE: // resize events + { + + RECT r = {0}; + if (GetClientRect(hwnd, &r)) + { + Clay_Dimensions dim = (Clay_Dimensions){.height = r.bottom - r.top, .width = r.right - r.left}; + Clay_SetLayoutDimensions(dim); + } + + InvalidateRect(hwnd, NULL, false); // force a wm_paint event + + break; + } + + case WM_KEYDOWN: + if (VK_ESCAPE == wParam) + { + DestroyWindow(hwnd); + break; + } + + if (wParam == VK_F12) + { + Clay_SetDebugModeEnabled(ui_debug_mode = !ui_debug_mode); + break; + } + + printf("Key Pressed: %d\r\n", wParam); + InvalidateRect(hwnd, NULL, false); // force a wm_paint event + break; + + // ----------------------- render + case WM_PAINT: + { + Clay_RenderCommandArray renderCommands = ClayVideoDemo_CreateLayout(&demo_data); + Clay_Win32_Render(hwnd, renderCommands); + break; + } + + // ----------------------- let windows do all other stuff + default: + return DefWindowProc(hwnd, message, wParam, lParam); + } + return 0; +} + +bool didAllocConsole = false; + +void HandleClayErrors(Clay_ErrorData errorData) +{ + if (!didAllocConsole) + { + didAllocConsole = AllocConsole(); + } + + printf("Handle Clay Errors: %s\r\n", errorData.errorText.chars); +} + +int APIENTRY WinMain( + HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nCmdShow) +{ + MSG msg; + WNDCLASS wc; + HWND hwnd; + + demo_data = ClayVideoDemo_Initialize(); + + uint64_t clayRequiredMemory = Clay_MinMemorySize(); + Clay_Arena clayMemory = Clay_CreateArenaWithCapacityAndMemory(clayRequiredMemory, malloc(clayRequiredMemory)); + Clay_Initialize(clayMemory, (Clay_Dimensions){.width = 800, .height = 600}, (Clay_ErrorHandler){HandleClayErrors}); // This final argument is new since the video was published + Clay_SetMeasureTextFunction(Clay_Win32_MeasureText, NULL); + + ZeroMemory(&wc, sizeof wc); + wc.hInstance = hInstance; + wc.lpszClassName = szAppName; + wc.lpfnWndProc = (WNDPROC)WndProc; + wc.style = CS_DBLCLKS | CS_VREDRAW | CS_HREDRAW; + wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + + if (FALSE == RegisterClass(&wc)) + return 0; + + + hwnd = CreateWindow( + szAppName, + szTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 800, // CW_USEDEFAULT, + 600, // CW_USEDEFAULT, + 0, + 0, + hInstance, + 0); + + if (hwnd == NULL) + return 0; + + // Main message loop: + while (GetMessage(&msg, NULL, 0, 0) > 0) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return msg.wParam; +} + +void CenterWindow(HWND hwnd_self) +{ + HWND hwnd_parent; + RECT rw_self, rc_parent, rw_parent; + int xpos, ypos; + + hwnd_parent = GetParent(hwnd_self); + if (NULL == hwnd_parent) + hwnd_parent = GetDesktopWindow(); + + GetWindowRect(hwnd_parent, &rw_parent); + GetClientRect(hwnd_parent, &rc_parent); + GetWindowRect(hwnd_self, &rw_self); + + xpos = rw_parent.left + (rc_parent.right + rw_self.left - rw_self.right) / 2; + ypos = rw_parent.top + (rc_parent.bottom + rw_self.top - rw_self.bottom) / 2; + + SetWindowPos( + hwnd_self, NULL, + xpos, ypos, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); +} + +//+--------------------------------------------------------------------------- diff --git a/renderers/SDL2/clay_renderer_SDL2.c b/renderers/SDL2/clay_renderer_SDL2.c index 3bfc164..6eccb01 100644 --- a/renderers/SDL2/clay_renderer_SDL2.c +++ b/renderers/SDL2/clay_renderer_SDL2.c @@ -3,6 +3,7 @@ #include #include #include +#include #ifndef M_PI #define M_PI 3.14159 @@ -408,7 +409,7 @@ static void Clay_SDL2_Render(SDL_Renderer *renderer, Clay_RenderCommandArray ren SDL_RenderCornerBorder(renderer, &boundingBox, config, 1, config->color); } - if (config->width.bottom > 0 & config->cornerRadius.bottomLeft > 0) { + if (config->width.bottom > 0 & config->cornerRadius.bottomRight > 0) { SDL_RenderCornerBorder(renderer, &boundingBox, config, 2, config->color); } diff --git a/renderers/raylib/clay_renderer_raylib.c b/renderers/raylib/clay_renderer_raylib.c index 2b370c6..de7857c 100644 --- a/renderers/raylib/clay_renderer_raylib.c +++ b/renderers/raylib/clay_renderer_raylib.c @@ -125,6 +125,21 @@ void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned i // EnableEventWaiting(); } +// A MALLOC'd buffer, that we keep modifying inorder to save from so many Malloc and Free Calls. +// Call Clay_Raylib_Close() to free +static char *temp_render_buffer = NULL; +static int temp_render_buffer_len = 0; + +// Call after closing the window to clean up the render buffer +void Clay_Raylib_Close() +{ + if(temp_render_buffer) free(temp_render_buffer); + temp_render_buffer_len = 0; + + CloseWindow(); +} + + void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font* fonts) { for (int j = 0; j < renderCommands.length; j++) @@ -134,14 +149,23 @@ void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font* fonts) switch (renderCommand->commandType) { case CLAY_RENDER_COMMAND_TYPE_TEXT: { - // Raylib uses standard C strings so isn't compatible with cheap slices, we need to clone the string to append null terminator Clay_TextRenderData *textData = &renderCommand->renderData.text; - char *cloned = (char *)malloc(textData->stringContents.length + 1); - memcpy(cloned, textData->stringContents.chars, textData->stringContents.length); - cloned[textData->stringContents.length] = '\0'; Font fontToUse = fonts[textData->fontId]; - DrawTextEx(fontToUse, cloned, (Vector2){boundingBox.x, boundingBox.y}, (float)textData->fontSize, (float)textData->letterSpacing, CLAY_COLOR_TO_RAYLIB_COLOR(textData->textColor)); - free(cloned); + + int strlen = textData->stringContents.length + 1; + + if(strlen > temp_render_buffer_len) { + // Grow the temp buffer if we need a larger string + if(temp_render_buffer) free(temp_render_buffer); + temp_render_buffer = malloc(strlen); + temp_render_buffer_len = strlen; + } + + // Raylib uses standard C strings so isn't compatible with cheap slices, we need to clone the string to append null terminator + memcpy(temp_render_buffer, textData->stringContents.chars, textData->stringContents.length); + temp_render_buffer[textData->stringContents.length] = '\0'; + DrawTextEx(fontToUse, temp_render_buffer, (Vector2){boundingBox.x, boundingBox.y}, (float)textData->fontSize, (float)textData->letterSpacing, CLAY_COLOR_TO_RAYLIB_COLOR(textData->textColor)); + break; } case CLAY_RENDER_COMMAND_TYPE_IMAGE: { diff --git a/renderers/win32_gdi/README.md b/renderers/win32_gdi/README.md new file mode 100644 index 0000000..950656b --- /dev/null +++ b/renderers/win32_gdi/README.md @@ -0,0 +1,5 @@ +The windows GDI renderer example is missing the following: + +- Images +- Rendering Rounded Rectangle borders +- Custom Fonts (font size) diff --git a/renderers/win32_gdi/clay_renderer_gdi.c b/renderers/win32_gdi/clay_renderer_gdi.c new file mode 100644 index 0000000..e031189 --- /dev/null +++ b/renderers/win32_gdi/clay_renderer_gdi.c @@ -0,0 +1,241 @@ +#include +#include "../../clay.h" + +HDC renderer_hdcMem = {0}; +HBITMAP renderer_hbmMem = {0}; +HANDLE renderer_hOld = {0}; + +void Clay_Win32_Render(HWND hwnd, Clay_RenderCommandArray renderCommands) +{ + bool is_clipping = false; + HRGN clipping_region = {0}; + + PAINTSTRUCT ps; + HDC hdc; + RECT rc; // Top left of our window + + GetWindowRect(hwnd, &rc); + + hdc = BeginPaint(hwnd, &ps); + + int win_width = rc.right - rc.left, + win_height = rc.bottom - rc.top; + + // Create an off-screen DC for double-buffering + renderer_hdcMem = CreateCompatibleDC(hdc); + renderer_hbmMem = CreateCompatibleBitmap(hdc, win_width, win_height); + + renderer_hOld = SelectObject(renderer_hdcMem, renderer_hbmMem); + + // draw + + for (int j = 0; j < renderCommands.length; j++) + { + Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, j); + Clay_BoundingBox boundingBox = renderCommand->boundingBox; + + switch (renderCommand->commandType) + { + case CLAY_RENDER_COMMAND_TYPE_TEXT: + { + Clay_Color c = renderCommand->renderData.text.textColor; + SetTextColor(renderer_hdcMem, RGB(c.r, c.g, c.b)); + SetBkMode(renderer_hdcMem, TRANSPARENT); + + RECT r = rc; + r.left = boundingBox.x; + r.top = boundingBox.y; + r.right = boundingBox.x + boundingBox.width + r.right; + r.bottom = boundingBox.y + boundingBox.height + r.bottom; + + DrawTextA(renderer_hdcMem, renderCommand->renderData.text.stringContents.chars, + renderCommand->renderData.text.stringContents.length, + &r, DT_TOP | DT_LEFT); + + break; + } + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: + { + Clay_RectangleRenderData rrd = renderCommand->renderData.rectangle; + RECT r = rc; + + r.left = boundingBox.x; + r.top = boundingBox.y; + r.right = boundingBox.x + boundingBox.width; + r.bottom = boundingBox.y + boundingBox.height; + + HBRUSH recColor = CreateSolidBrush(RGB(rrd.backgroundColor.r, rrd.backgroundColor.g, rrd.backgroundColor.b)); + + if (rrd.cornerRadius.topLeft > 0) + { + HRGN roundedRectRgn = CreateRoundRectRgn( + r.left, r.top, r.right + 1, r.bottom + 1, + rrd.cornerRadius.topLeft * 2, rrd.cornerRadius.topLeft * 2); + + FillRgn(renderer_hdcMem, roundedRectRgn, recColor); + DeleteObject(roundedRectRgn); + } + else + { + FillRect(renderer_hdcMem, &r, recColor); + } + + DeleteObject(recColor); + break; + } + + // The renderer should begin clipping all future draw commands, only rendering content that falls within the provided boundingBox. + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: + { + is_clipping = true; + + clipping_region = CreateRectRgn(boundingBox.x, + boundingBox.y, + boundingBox.x + boundingBox.width, + boundingBox.y + boundingBox.height); + + SelectClipRgn(renderer_hdcMem, clipping_region); + break; + } + + // The renderer should finish any previously active clipping, and begin rendering elements in full again. + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: + { + SelectClipRgn(renderer_hdcMem, NULL); + + if (clipping_region) + { + DeleteObject(clipping_region); + } + + is_clipping = false; + clipping_region = NULL; + + break; + } + + // The renderer should draw a colored border inset into the bounding box. + case CLAY_RENDER_COMMAND_TYPE_BORDER: + { + Clay_BorderRenderData brd = renderCommand->renderData.border; + RECT r = rc; + + r.left = boundingBox.x; + r.top = boundingBox.y; + r.right = boundingBox.x + boundingBox.width; + r.bottom = boundingBox.y + boundingBox.height; + + HPEN topPen = CreatePen(PS_SOLID, brd.width.top, RGB(brd.color.r, brd.color.g, brd.color.b)); + HPEN leftPen = CreatePen(PS_SOLID, brd.width.left, RGB(brd.color.r, brd.color.g, brd.color.b)); + HPEN bottomPen = CreatePen(PS_SOLID, brd.width.bottom, RGB(brd.color.r, brd.color.g, brd.color.b)); + HPEN rightPen = CreatePen(PS_SOLID, brd.width.right, RGB(brd.color.r, brd.color.g, brd.color.b)); + + HPEN oldPen = SelectObject(renderer_hdcMem, topPen); + + if (brd.cornerRadius.topLeft == 0) + { + MoveToEx(renderer_hdcMem, r.left, r.top, NULL); + LineTo(renderer_hdcMem, r.right, r.top); + + SelectObject(renderer_hdcMem, leftPen); + MoveToEx(renderer_hdcMem, r.left, r.top, NULL); + LineTo(renderer_hdcMem, r.left, r.bottom); + + SelectObject(renderer_hdcMem, bottomPen); + MoveToEx(renderer_hdcMem, r.left, r.bottom, NULL); + LineTo(renderer_hdcMem, r.right, r.bottom); + + SelectObject(renderer_hdcMem, rightPen); + MoveToEx(renderer_hdcMem, r.right, r.top, NULL); + LineTo(renderer_hdcMem, r.right, r.bottom); + } + else + { + // todo: i should be rounded + MoveToEx(renderer_hdcMem, r.left, r.top, NULL); + LineTo(renderer_hdcMem, r.right, r.top); + + SelectObject(renderer_hdcMem, leftPen); + MoveToEx(renderer_hdcMem, r.left, r.top, NULL); + LineTo(renderer_hdcMem, r.left, r.bottom); + + SelectObject(renderer_hdcMem, bottomPen); + MoveToEx(renderer_hdcMem, r.left, r.bottom, NULL); + LineTo(renderer_hdcMem, r.right, r.bottom); + + SelectObject(renderer_hdcMem, rightPen); + MoveToEx(renderer_hdcMem, r.right, r.top, NULL); + LineTo(renderer_hdcMem, r.right, r.bottom); + + } + + SelectObject(renderer_hdcMem, oldPen); + DeleteObject(topPen); + DeleteObject(leftPen); + DeleteObject(bottomPen); + DeleteObject(rightPen); + + break; + } + + // case CLAY_RENDER_COMMAND_TYPE_IMAGE: + // { + // // TODO: i couldnt get the win 32 api to load a bitmap.... So im punting on this one :( + // break; + // } + + default: + printf("Unhandled render command %d\r\n", renderCommand->commandType); + break; + } + } + + BitBlt(hdc, 0, 0, win_width, win_height, renderer_hdcMem, 0, 0, SRCCOPY); + + // Free-up the off-screen DC + SelectObject(renderer_hdcMem, renderer_hOld); + DeleteObject(renderer_hbmMem); + DeleteDC(renderer_hdcMem); + + EndPaint(hwnd, &ps); +} + +/* + Hacks due to the windows api not making sence to use.... may measure too large, but never too small +*/ + +#ifndef WIN32_FONT_HEIGHT +#define WIN32_FONT_HEIGHT (16) +#endif + +#ifndef WIN32_FONT_WIDTH +#define WIN32_FONT_WIDTH (8) +#endif + +static inline Clay_Dimensions Clay_Win32_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) +{ + Clay_Dimensions textSize = {0}; + + float maxTextWidth = 0.0f; + float lineTextWidth = 0; + float textHeight = WIN32_FONT_HEIGHT; + + for (int i = 0; i < text.length; ++i) + { + if (text.chars[i] == '\n') + { + maxTextWidth = fmax(maxTextWidth, lineTextWidth); + lineTextWidth = 0; + continue; + } + + lineTextWidth += WIN32_FONT_WIDTH; + } + + maxTextWidth = fmax(maxTextWidth, lineTextWidth); + + textSize.width = maxTextWidth; + textSize.height = textHeight; + + return textSize; +} \ No newline at end of file