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:
@@ -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;
|
||||
|
||||
@@ -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,10 +118,11 @@ 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,
|
||||
@@ -73,31 +130,25 @@ errorret_t textDraw(
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uibutton.c
|
||||
uicheckbox.c
|
||||
uilabel.c
|
||||
uitab.c
|
||||
uislider.c
|
||||
uidropdown.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));
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user