From c019271e1236a362a27e67ce35fa0ad81f2392cd Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 20 Aug 2026 09:18:35 -0500 Subject: [PATCH] Add uilabel widget and chunked text buffering textDraw now batches glyphs via a new textBuffer helper instead of buffering one sprite at a time. Fixes spriteBatchBuffer dropping/ duplicating sprites when a single call spans multiple internal flush batches, which surfaced as cut-off characters. Adds uilabel, a widget that caches its glyph sprites and only rebuilds/rebuffers them when its text or position actually changes. Co-Authored-By: Claude Sonnet 5 --- src/dusk/display/spritebatch/spritebatch.c | 2 +- src/dusk/display/text/text.c | 100 +++++++++++++++---- src/dusk/display/text/text.h | 34 ++++++- src/dusk/ui/widget/CMakeLists.txt | 1 + src/dusk/ui/widget/uilabel.c | 73 ++++++++++++++ src/dusk/ui/widget/uilabel.h | 108 +++++++++++++++++++++ 6 files changed, 295 insertions(+), 23 deletions(-) create mode 100644 src/dusk/ui/widget/uilabel.c create mode 100644 src/dusk/ui/widget/uilabel.h diff --git a/src/dusk/display/spritebatch/spritebatch.c b/src/dusk/display/spritebatch/spritebatch.c index 6fdb4774..242402de 100644 --- a/src/dusk/display/spritebatch/spritebatch.c +++ b/src/dusk/display/spritebatch/spritebatch.c @@ -76,7 +76,7 @@ errorret_t spriteBatchBuffer( // Buffer to the mesh vertices. spriteBatchBufferToMesh( - sprites, batchCount, v, batchCount * QUAD_VERTEX_COUNT + sprites + (count - remaining), batchCount, v, batchCount * QUAD_VERTEX_COUNT ); SPRITEBATCH.spriteCount += batchCount; remaining -= batchCount; diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c index a9210fa6..aea07b96 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -54,6 +54,62 @@ spritebatchsprite_t textGetSprite( return sprite; } +int32_t textBuffer( + const float_t x, + const float_t y, + const char_t *text, + font_t *font, + spritebatchsprite_t *outSprites, + const int32_t maxSprites, + int32_t *charIndex, + float_t *posX, + float_t *posY +) { + assertNotNull(text, "Text cannot be NULL"); + + if(outSprites == NULL) { + int32_t count = 0; + char_t c; + int32_t i = 0; + while((c = text[i++]) != '\0') { + if(c != ' ' && c != '\n') count++; + } + return count; + } + + assertNotNull(font, "Font cannot be NULL"); + assertTrue(maxSprites > 0, "Max sprites must be greater than zero"); + assertNotNull(posX, "Output posX cannot be NULL"); + assertNotNull(posY, "Output posY cannot be NULL"); + assertNotNull(charIndex, "Output charIndex cannot be NULL"); + + int32_t spriteIndex = 0; + char_t c; + for(;;) { + c = text[*charIndex]; + if(c == '\0') break; + (*charIndex)++; + + if(c == '\n') { + *posX = x; + *posY += font->tileset->tileHeight; + continue; + } + + if(c == ' ') { + *posX += font->tileset->tileWidth; + continue; + } + + outSprites[spriteIndex++] = textGetSprite((vec2){*posX, *posY}, c, font); + *posX += font->tileset->tileWidth; + + if(spriteIndex >= maxSprites) break; + } + + return spriteIndex; +} + errorret_t textDraw( const float_t x, const float_t y, @@ -62,42 +118,37 @@ errorret_t textDraw( font_t *font ) { assertNotNull(text, "Text cannot be NULL"); + int32_t length = strlen(text); + if(length == 0) errorOk(); if(font == NULL) font = &FONT_DEFAULT; - spritebatchsprite_t sprite; shadermaterial_t material = { .unlit = { .color = color, .texture = font->texture } }; - + + spritebatchsprite_t sprites[32]; float_t posX = x; float_t posY = y; + int32_t buffered = 0; + int32_t charIndex = 0; + do { + buffered = textBuffer( + x, y, text, font, + sprites, + sizeof(sprites) / sizeof(spritebatchsprite_t), + &charIndex, &posX, &posY + ); + errorChain(spriteBatchBuffer(sprites, buffered, &SHADER_UNLIT, material)); + } while(charIndex < length); - char_t c; - int32_t i = 0; - while((c = text[i++]) != '\0') { - if(c == '\n') { - posX = x; - posY += font->tileset->tileHeight; - continue; - } - - if(c == ' ') { - posX += font->tileset->tileWidth; - continue; - } - - sprite = textGetSprite((vec2){posX, posY}, c, font); - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - posX += font->tileset->tileWidth; - } errorOk(); } -void textMeasure( +int32_t textMeasure( const char_t *text, const font_t *font, int32_t *outWidth, @@ -110,6 +161,7 @@ void textMeasure( int32_t width = 0; int32_t height = font->tileset->tileHeight; int32_t lineWidth = 0; + int32_t spriteCount = 0; char_t c; int32_t i = 0; @@ -122,10 +174,16 @@ void textMeasure( } lineWidth += font->tileset->tileWidth; + + if(c != ' ') { + spriteCount++; + } } if(lineWidth > width) width = lineWidth; *outWidth = width; *outHeight = height; + + return spriteCount; } diff --git a/src/dusk/display/text/text.h b/src/dusk/display/text/text.h index 60f2fdfb..d79a60b4 100644 --- a/src/dusk/display/text/text.h +++ b/src/dusk/display/text/text.h @@ -40,6 +40,37 @@ spritebatchsprite_t textGetSprite( const font_t *font ); +/** + * Buffers a string into sprites for rendering. If outSprites is NULL then + * the function will only return the count of sprites necessary for the buffer. + * + * posX and posY are updated whilst buffering characters, if you need to do + * buffering in sets then these will be reusable between buffer commands. Start + * by setting these to x and y initially. + * + * @param x The x-coordinate to start buffering the text at. + * @param y The y-coordinate to start buffering the text at. + * @param text The null-terminated string of text to buffer. + * @param font Font to use for rendering. + * @param outSprites Pointer to an array of spritebatchsprite_t. + * @param maxSprites The maximum number of sprites in outSprites. + * @param charIndex Pointer to an int32_t to store the character indexed. + * @param posX Pointer to a float_t to store the final x position. + * @param posY Pointer to a float_t to store the final y position. + * @return The count of sprites buffered. + */ +int32_t textBuffer( + const float_t x, + const float_t y, + const char_t *text, + font_t *font, + spritebatchsprite_t *outSprites, + const int32_t maxSprites, + int32_t *charIndex, + float_t *posX, + float_t *posY +); + /** * Draws a string of text at the specified position. * @@ -65,8 +96,9 @@ errorret_t textDraw( * @param font Font to use for measurement. * @param outWidth Pointer to store the measured width in pixels. * @param outHeight Pointer to store the measured height in pixels. + * @return The count of sprites that will be rendered for the given text. */ -void textMeasure( +int32_t textMeasure( const char_t *text, const font_t *font, int32_t *outWidth, diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index 2e3eaf06..a71f8012 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uibutton.c uicheckbox.c + uilabel.c uitab.c uislider.c uidropdown.c diff --git a/src/dusk/ui/widget/uilabel.c b/src/dusk/ui/widget/uilabel.c new file mode 100644 index 00000000..347f8d63 --- /dev/null +++ b/src/dusk/ui/widget/uilabel.c @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uilabel.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/text/text.h" +#include "display/spritebatch/spritebatch.h" +#include "display/shader/shaderunlit.h" + +void uiLabelInit(uilabel_t *label, const char_t *text) { + memoryZero(label, sizeof(uilabel_t)); + uiLabelSetText(label, text); +} + +void uiLabelSetText(uilabel_t *label, const char_t *text) { + if(stringCompare(label->text, text) == 0) return; + stringCopy(label->text, text, UI_LABEL_TEXT_MAX); + label->dirty = true; +} + +const char_t * uiLabelGetText(const uilabel_t *label) { + return label->text; +} + +void uiLabelSetX(uilabel_t *label, const float_t x) { + if(label->x == x) return; + label->x = x; + label->dirty = true; +} + +float_t uiLabelGetX(const uilabel_t *label) { + return label->x; +} + +void uiLabelSetY(uilabel_t *label, const float_t y) { + if(label->y == y) return; + label->y = y; + label->dirty = true; +} + +float_t uiLabelGetY(const uilabel_t *label) { + return label->y; +} + +void uiLabelRebuffer(uilabel_t *label) { + if(!label->dirty) return; +} + +errorret_t uiLabelRender(uilabel_t *label, const color_t color) { + uiLabelRebuffer(label); + if(label->spriteCount == 0) errorOk(); + + shadermaterial_t material = { + .unlit = { + .color = color, + .texture = FONT_DEFAULT.texture + } + }; + errorChain(spriteBatchBuffer( + label->buffer, label->spriteCount, &SHADER_UNLIT, material + )); + + errorOk(); +} + +void uiLabelDispose(uilabel_t *label) { + memoryZero(label, sizeof(uilabel_t)); +} diff --git a/src/dusk/ui/widget/uilabel.h b/src/dusk/ui/widget/uilabel.h new file mode 100644 index 00000000..f45923bb --- /dev/null +++ b/src/dusk/ui/widget/uilabel.h @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "display/spritebatch/spritebatchsprite.h" +#include "display/color.h" + +#define UI_LABEL_TEXT_MAX 128 + +typedef struct { + char_t text[UI_LABEL_TEXT_MAX]; + spritebatchsprite_t sprites[UI_LABEL_TEXT_MAX]; + spritebatchsprite_t buffer[UI_LABEL_TEXT_MAX]; + uint32_t spriteCount; + int32_t width; + int32_t height; + float_t x; + float_t y; + bool_t dirty; +} uilabel_t; + +/** + * Initializes a label. + * + * @param label The label to initialize. + * @param text Display text; copied internally, safe to be transient. + */ +void uiLabelInit(uilabel_t *label, const char_t *text); + +/** + * Sets the display text of the label, rebuilding its cached sprites. + * + * @param label The label to update. + * @param text Display text; copied internally, safe to be transient. + */ +void uiLabelSetText(uilabel_t *label, const char_t *text); + +/** + * Returns the display text of the label. + * + * @param label The label to query. + * @returns The label's text. + */ +const char_t * uiLabelGetText(const uilabel_t *label); + +/** + * Sets the screen x position of the label. + * + * @param label The label to update. + * @param x Screen x position. + */ +void uiLabelSetX(uilabel_t *label, const float_t x); + +/** + * Returns the screen x position of the label. + * + * @param label The label to query. + * @returns The label's x position. + */ +float_t uiLabelGetX(const uilabel_t *label); + +/** + * Sets the screen y position of the label. + * + * @param label The label to update. + * @param y Screen y position. + */ +void uiLabelSetY(uilabel_t *label, const float_t y); + +/** + * Returns the screen y position of the label. + * + * @param label The label to query. + * @returns The label's y position. + */ +float_t uiLabelGetY(const uilabel_t *label); + +/** + * Rebuffers the label: rebuilds its cached local-space sprites if the + * text has changed, then re-translates them into its render buffer at + * its current x/y position. Called internally by uiLabelRender when + * either dirty flag is set. + * + * @param label The label to rebuffer. + */ +void uiLabelRebuffer(uilabel_t *label); + +/** + * Renders the label's cached sprites at its current screen position. + * Rebuffers first if the label is dirty (text or position changed). + * + * @param label The label to render. + * @param color The color to render the text in. + * @return Any error that occurs. + */ +errorret_t uiLabelRender(uilabel_t *label, const color_t color); + +/** + * Disposes of a label. + * + * @param label The label to dispose. + */ +void uiLabelDispose(uilabel_t *label);