/* Copyright (c) 2023 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "uitoolkit/UITextLayout.h" #include "utilities/UTFUtilities.h" #define stemobject_implementation UITextLayout stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_entry(setString); stemobject_vtable_entry(setTypeface); stemobject_vtable_entry(setAlignMode); stemobject_vtable_entry(setWrapBehavior); stemobject_vtable_entry(setWrapWidth); stemobject_vtable_entry(getLineCount); stemobject_vtable_entry(getLineIndex); stemobject_vtable_entry(getLineStartCharIndex); stemobject_vtable_entry(getLineEndCharIndex); stemobject_vtable_entry(measureString); stemobject_vtable_entry(indexAtPosition); stemobject_vtable_entry(positionAtIndex); stemobject_vtable_entry(writeGlyphsWithCallback); stemobject_vtable_end(); UITextLayout * UITextLayout_create(compat_type(UITypeface *) typeface, String string, TextAlignMode alignMode, WordWrapBehavior wrapBehavior, float wrapWidth) { stemobject_create_implementation(init, typeface, string, alignMode, wrapBehavior, wrapWidth) } bool UITextLayout_init(UITextLayout * self, compat_type(UITypeface *) typeface, String string, TextAlignMode alignMode, WordWrapBehavior wrapBehavior, float wrapWidth) { call_super(init, self); self->typeface = typeface; self->string = String_copy(string); self->alignMode = alignMode; self->wrapBehavior = wrapBehavior; self->wrapWidth = wrapWidth; self->dirty = true; self->private_ivar(wrapInfo).wrapPointAllocatedCount = 0; self->private_ivar(wrapInfo).wrapPoints = NULL; self->private_ivar(wrapInfo).wrapPointCount = 0; return true; } void UITextLayout_dispose(UITextLayout * self) { String_free(self->string); free(self->private_ivar(wrapInfo).wrapPoints); call_super_virtual(dispose, self); } void UITextLayout_setString(UITextLayout * self, String string) { String_free(self->string); self->string = String_copy(string); self->dirty = true; } void UITextLayout_setTypeface(UITextLayout * self, compat_type(UITypeface *) typeface) { self->typeface = typeface; self->dirty = true; } void UITextLayout_setAlignMode(UITextLayout * self, TextAlignMode alignMode) { self->alignMode = alignMode; self->dirty = true; } void UITextLayout_setWrapBehavior(UITextLayout * self, WordWrapBehavior wrapBehavior) { self->wrapBehavior = wrapBehavior; self->dirty = true; } void UITextLayout_setWrapWidth(UITextLayout * self, float wrapWidth) { self->wrapWidth = wrapWidth; self->dirty = true; } static WordWrapCharacterClassification classifyCallback(const void * stringUntyped, size_t index, void * context) { UITextLayout * self = context; uint32_t codepoint = 0; switch (self->string.encoding) { case ENCODING_UTF8: { const uint8_t * string = stringUntyped; codepoint = string[index]; if (getUTF8ExpectedCharCount(codepoint) == 0) { return CHAR_INDIVISIBLE; } break; } case ENCODING_UTF16: { const uint16_t * string = stringUntyped; codepoint = string[index]; if (getUTF16ExpectedCharCount(codepoint) == 0) { return CHAR_INDIVISIBLE; } break; } case ENCODING_UTF32: { const uint32_t * string = stringUntyped; codepoint = string[index]; break; } case ENCODING_CHARACTER_INDEX: { const unsigned int * string = stringUntyped; codepoint = call_virtual(characterIndexToCodepoint, self->typeface, string[index]); break; } } switch (codepoint) { case ' ': case '\t': return CHAR_WHITESPACE; case '\n': return CHAR_NEWLINE; } return CHAR_PRINTABLE; } static float measureCallback(const void * string, size_t offset, size_t length, void * context) { UITextLayout * self = context; String substring = {(void *) string + offset * String_charSize(self->string), length, self->string.encoding}; return call_virtual(measureString, self->typeface, substring); } static size_t getIndexCallback(const void * string, size_t offset, size_t length, float positionX, void * context) { UITextLayout * self = context; String substring = {(void *) string + offset * String_charSize(self->string), length, self->string.encoding}; return call_virtual(indexAtPositionX, self->typeface, substring, positionX, NULL) + offset; } static void wrapCallback(size_t index, size_t collapsedWhitespaceCount, void * context) { UITextLayout * self = context; if (self->private_ivar(wrapInfo).wrapPointAllocatedCount <= self->private_ivar(wrapInfo).wrapPointCount) { self->private_ivar(wrapInfo).wrapPointAllocatedCount = self->private_ivar(wrapInfo).wrapPointAllocatedCount * 2 + (self->private_ivar(wrapInfo).wrapPointAllocatedCount == 0); self->private_ivar(wrapInfo).wrapPoints = realloc(self->private_ivar(wrapInfo).wrapPoints, self->private_ivar(wrapInfo).wrapPointAllocatedCount * sizeof(*self->private_ivar(wrapInfo).wrapPoints)); } self->private_ivar(wrapInfo).wrapPoints[self->private_ivar(wrapInfo).wrapPointCount].wrapPoint = index; self->private_ivar(wrapInfo).wrapPoints[self->private_ivar(wrapInfo).wrapPointCount].ignoredWhitespaceCount = collapsedWhitespaceCount; self->private_ivar(wrapInfo).wrapPointCount++; } static void updateWrapInfo(UITextLayout * self) { if (self->dirty) { self->dirty = false; self->private_ivar(wrapInfo).wrapPointCount = 0; wordWrapWithCallbacks(self->string.bytes, self->string.length, self->wrapWidth, self->wrapBehavior, classifyCallback, measureCallback, getIndexCallback, wrapCallback, self); } } unsigned int UITextLayout_getLineCount(UITextLayout * self) { updateWrapInfo(self); return self->private_ivar(wrapInfo).wrapPointCount + 1; } unsigned int UITextLayout_getLineIndex(UITextLayout * self, size_t charIndex) { for (size_t wrapPointIndex = 0; wrapPointIndex < self->private_ivar(wrapInfo).wrapPointCount; wrapPointIndex++) { if (charIndex < self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint) { return wrapPointIndex; } } return self->private_ivar(wrapInfo).wrapPointCount; } size_t UITextLayout_getLineStartCharIndex(UITextLayout * self, unsigned int lineIndex) { if (lineIndex == 0) { return 0; } if (lineIndex >= self->private_ivar(wrapInfo).wrapPointCount) { if (self->private_ivar(wrapInfo).wrapPointCount == 0) { return 0; } return self->private_ivar(wrapInfo).wrapPoints[self->private_ivar(wrapInfo).wrapPointCount - 1].wrapPoint; } return self->private_ivar(wrapInfo).wrapPoints[lineIndex - 1].wrapPoint; } size_t UITextLayout_getLineEndCharIndex(UITextLayout * self, unsigned int lineIndex) { if (lineIndex >= self->private_ivar(wrapInfo).wrapPointCount) { return self->string.length; } return self->private_ivar(wrapInfo).wrapPoints[lineIndex].wrapPoint - self->private_ivar(wrapInfo).wrapPoints[lineIndex].ignoredWhitespaceCount; } Vector2f UITextLayout_measureString(UITextLayout * self) { float width, maxWidth = 0.0f; size_t charIndex = 0; updateWrapInfo(self); for (size_t wrapPointIndex = 0; wrapPointIndex < self->private_ivar(wrapInfo).wrapPointCount; wrapPointIndex++) { width = call_virtual(measureString, self->typeface, String_substring(self->string, charIndex, self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint - self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].ignoredWhitespaceCount - charIndex)); if (width > maxWidth) { maxWidth = width; } charIndex = self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint; } width = call_virtual(measureString, self->typeface, String_substring(self->string, charIndex, self->string.length - charIndex)); if (width > maxWidth) { maxWidth = width; } return VECTOR2f(maxWidth, (self->private_ivar(wrapInfo).wrapPointCount + 1) * call_virtual(getLineHeight, self->typeface)); } static float getLineOffsetX(UITextLayout * self, unsigned int lineIndex) { if (self->alignMode == ALIGN_LEFT) { return 0.0f; } size_t lineStartIndex = call_virtual(getLineStartCharIndex, self, lineIndex), lineEndIndex = call_virtual(getLineEndCharIndex, self, lineIndex); float lineWidth = call_virtual(measureString, self->typeface, String_substring(self->string, lineStartIndex, lineEndIndex - lineStartIndex)); if (self->alignMode == ALIGN_CENTER) { lineWidth *= 0.5f; } return -lineWidth; } size_t UITextLayout_indexAtPosition(UITextLayout * self, Vector2f position, bool * outLeadingEdge) { updateWrapInfo(self); int lineIndex = floorf(position.y / call_virtual(getLineHeight, self->typeface)); if (lineIndex < 0) { if (outLeadingEdge != NULL) { *outLeadingEdge = true; } return 0; } if (lineIndex > (int) self->private_ivar(wrapInfo).wrapPointCount) { if (outLeadingEdge != NULL) { *outLeadingEdge = false; } if (self->string.length == 0) { return 0; } return self->string.length - 1; } if (lineIndex == 0) { return call_virtual(indexAtPositionX, self->typeface, String_substring(self->string, 0, self->private_ivar(wrapInfo).wrapPointCount > 0 ? self->private_ivar(wrapInfo).wrapPoints[0].wrapPoint - self->private_ivar(wrapInfo).wrapPoints[0].ignoredWhitespaceCount : self->string.length), position.x - getLineOffsetX(self, (unsigned int) lineIndex), outLeadingEdge); } return call_virtual(indexAtPositionX, self->typeface, String_substring(self->string, self->private_ivar(wrapInfo).wrapPoints[lineIndex - 1].wrapPoint, lineIndex == (int) self->private_ivar(wrapInfo).wrapPointCount ? self->string.length - self->private_ivar(wrapInfo).wrapPoints[lineIndex - 1].wrapPoint : self->private_ivar(wrapInfo).wrapPoints[lineIndex].wrapPoint - self->private_ivar(wrapInfo).wrapPoints[lineIndex - 1].wrapPoint - self->private_ivar(wrapInfo).wrapPoints[lineIndex - 1].ignoredWhitespaceCount), position.x - getLineOffsetX(self, (unsigned int) lineIndex), outLeadingEdge) + self->private_ivar(wrapInfo).wrapPoints[lineIndex - 1].wrapPoint; } Vector2f UITextLayout_positionAtIndex(UITextLayout * self, size_t charIndex) { updateWrapInfo(self); float lineHeight = call_virtual(getLineHeight, self->typeface); size_t lineOffset = 0; for (size_t wrapPointIndex = 0; wrapPointIndex < self->private_ivar(wrapInfo).wrapPointCount; wrapPointIndex++) { if (charIndex < self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint - lineOffset) { return VECTOR2f(call_virtual(positionXAtIndex, self->typeface, String_substring(self->string, lineOffset, self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint - lineOffset), charIndex) + getLineOffsetX(self, wrapPointIndex), wrapPointIndex * lineHeight); } charIndex -= self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint - lineOffset; lineOffset = self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint; } if (charIndex > self->string.length - lineOffset) { charIndex = self->string.length - lineOffset; } return VECTOR2f(call_virtual(positionXAtIndex, self->typeface, String_substring(self->string, lineOffset, self->string.length - lineOffset), charIndex) + getLineOffsetX(self, self->private_ivar(wrapInfo).wrapPointCount), self->private_ivar(wrapInfo).wrapPointCount * lineHeight); } struct glyphCallbackContext { UITypeface_glyphCallback callback; void * callbackContext; }; static void glyphCallback(Rect4f vertexBounds, Rect4f textureBounds, bool fixedColor, void * context) { struct glyphCallbackContext * contextStruct = context; contextStruct->callback(vertexBounds, textureBounds, fixedColor, contextStruct->callbackContext); } void UITextLayout_writeGlyphsWithCallback(UITextLayout * self, Vector2f offset, float scale, UITypeface_glyphCallback callback, void * callbackContext) { size_t lineCharOffset = 0; float lineHeight = call_virtual(getLineHeight, self->typeface); updateWrapInfo(self); for (size_t wrapPointIndex = 0; wrapPointIndex < self->private_ivar(wrapInfo).wrapPointCount; wrapPointIndex++) { Vector2f lineOffset = {getLineOffsetX(self, wrapPointIndex), wrapPointIndex * -lineHeight}; struct glyphCallbackContext contextStruct = {callback, callbackContext}; call_virtual(writeGlyphsWithCallback, self->typeface, String_substring(self->string, lineCharOffset, self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint - lineCharOffset), Vector2f_add(offset, lineOffset), scale, glyphCallback, &contextStruct); lineCharOffset = self->private_ivar(wrapInfo).wrapPoints[wrapPointIndex].wrapPoint; } Vector2f lineOffset = {getLineOffsetX(self, self->private_ivar(wrapInfo).wrapPointCount), self->private_ivar(wrapInfo).wrapPointCount * -lineHeight}; struct glyphCallbackContext contextStruct = {callback, callbackContext}; call_virtual(writeGlyphsWithCallback, self->typeface, String_substring(self->string, lineCharOffset, self->string.length - lineCharOffset), Vector2f_add(offset, lineOffset), scale, glyphCallback, &contextStruct); }