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 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 09:18:35 -05:00
parent 82ae2bce9d
commit c019271e12
6 changed files with 295 additions and 23 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ errorret_t spriteBatchBuffer(
// Buffer to the mesh vertices. // Buffer to the mesh vertices.
spriteBatchBufferToMesh( spriteBatchBufferToMesh(
sprites, batchCount, v, batchCount * QUAD_VERTEX_COUNT sprites + (count - remaining), batchCount, v, batchCount * QUAD_VERTEX_COUNT
); );
SPRITEBATCH.spriteCount += batchCount; SPRITEBATCH.spriteCount += batchCount;
remaining -= batchCount; remaining -= batchCount;
+79 -21
View File
@@ -54,6 +54,62 @@ spritebatchsprite_t textGetSprite(
return sprite; 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( errorret_t textDraw(
const float_t x, const float_t x,
const float_t y, const float_t y,
@@ -62,42 +118,37 @@ errorret_t textDraw(
font_t *font font_t *font
) { ) {
assertNotNull(text, "Text cannot be NULL"); assertNotNull(text, "Text cannot be NULL");
int32_t length = strlen(text);
if(length == 0) errorOk();
if(font == NULL) font = &FONT_DEFAULT; if(font == NULL) font = &FONT_DEFAULT;
spritebatchsprite_t sprite;
shadermaterial_t material = { shadermaterial_t material = {
.unlit = { .unlit = {
.color = color, .color = color,
.texture = font->texture .texture = font->texture
} }
}; };
spritebatchsprite_t sprites[32];
float_t posX = x; float_t posX = x;
float_t posY = y; 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(); errorOk();
} }
void textMeasure( int32_t textMeasure(
const char_t *text, const char_t *text,
const font_t *font, const font_t *font,
int32_t *outWidth, int32_t *outWidth,
@@ -110,6 +161,7 @@ void textMeasure(
int32_t width = 0; int32_t width = 0;
int32_t height = font->tileset->tileHeight; int32_t height = font->tileset->tileHeight;
int32_t lineWidth = 0; int32_t lineWidth = 0;
int32_t spriteCount = 0;
char_t c; char_t c;
int32_t i = 0; int32_t i = 0;
@@ -122,10 +174,16 @@ void textMeasure(
} }
lineWidth += font->tileset->tileWidth; lineWidth += font->tileset->tileWidth;
if(c != ' ') {
spriteCount++;
}
} }
if(lineWidth > width) width = lineWidth; if(lineWidth > width) width = lineWidth;
*outWidth = width; *outWidth = width;
*outHeight = height; *outHeight = height;
return spriteCount;
} }
+33 -1
View File
@@ -40,6 +40,37 @@ spritebatchsprite_t textGetSprite(
const font_t *font 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. * Draws a string of text at the specified position.
* *
@@ -65,8 +96,9 @@ errorret_t textDraw(
* @param font Font to use for measurement. * @param font Font to use for measurement.
* @param outWidth Pointer to store the measured width in pixels. * @param outWidth Pointer to store the measured width in pixels.
* @param outHeight Pointer to store the measured height 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 char_t *text,
const font_t *font, const font_t *font,
int32_t *outWidth, int32_t *outWidth,
+1
View File
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
uibutton.c uibutton.c
uicheckbox.c uicheckbox.c
uilabel.c
uitab.c uitab.c
uislider.c uislider.c
uidropdown.c uidropdown.c
+73
View File
@@ -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));
}
+108
View File
@@ -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);