Fix uilabel dirty bug and add word-wrap support

uiLabelInit now marks a fresh label dirty itself, instead of every call
site having to remember to do it. Adds uiLabelSetWrap (NULL buffer
disables wrapping, otherwise requires a positive width) built on the
existing textWrap(), and wires the fatal error overlay's message label
up to wrap at 80% of screen width.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-19 21:32:24 -05:00
co-authored by Claude Sonnet 5
parent 2abd5b0c3c
commit 3ce16cf7f4
4 changed files with 76 additions and 3 deletions
+7 -1
View File
@@ -48,7 +48,13 @@ void uiFatalErrorOpen(const char_t *message) {
stringCopy( stringCopy(
UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1 UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1
); );
UI_FATAL_ERROR.messageLabel.dirty = true;
const float_t wrapWidth =
(float_t)SCREEN.scanWidth * UI_FATAL_ERROR_MESSAGE_WRAP_RATIO;
uiLabelSetWrap(
&UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageWrapText,
UI_FATAL_ERROR_MESSAGE_MAX, wrapWidth
);
uiLabelRebuffer(&UI_FATAL_ERROR.messageLabel); uiLabelRebuffer(&UI_FATAL_ERROR.messageLabel);
UI_FATAL_ERROR.open = true; UI_FATAL_ERROR.open = true;
+2
View File
@@ -12,6 +12,7 @@
#define UI_FATAL_ERROR_MESSAGE_MAX 256 #define UI_FATAL_ERROR_MESSAGE_MAX 256
#define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX #define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX
#define UI_FATAL_ERROR_MESSAGE_WRAP_RATIO 0.8f
#define UI_FATAL_ERROR_ITEM_COUNT 1 #define UI_FATAL_ERROR_ITEM_COUNT 1
#define UI_FATAL_ERROR_QUIT_LABEL "QUIT" #define UI_FATAL_ERROR_QUIT_LABEL "QUIT"
#define UI_FATAL_ERROR_GENERIC_MESSAGE \ #define UI_FATAL_ERROR_GENERIC_MESSAGE \
@@ -23,6 +24,7 @@ typedef struct {
uilabel_t messageLabel; uilabel_t messageLabel;
char_t messageText[UI_FATAL_ERROR_MESSAGE_MAX]; char_t messageText[UI_FATAL_ERROR_MESSAGE_MAX];
char_t messageWrapText[UI_FATAL_ERROR_MESSAGE_MAX];
spritebatchsprite_t messageSprites[UI_FATAL_ERROR_MESSAGE_SPRITES_MAX]; spritebatchsprite_t messageSprites[UI_FATAL_ERROR_MESSAGE_SPRITES_MAX];
uimenu_t menu; uimenu_t menu;
+37 -2
View File
@@ -8,6 +8,7 @@
#include "uilabel.h" #include "uilabel.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h" #include "display/text/text.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
@@ -25,6 +26,30 @@ void uiLabelInit(
label->text = text; label->text = text;
label->sprites = sprites; label->sprites = sprites;
label->spritesMax = spritesMax; label->spritesMax = spritesMax;
label->dirty = true;
}
void uiLabelSetWrap(
uilabel_t *label,
char_t *wrapBuffer,
const uint32_t wrapBufferMax,
const float_t wrapWidth
) {
assertNotNull(label, "Label cannot be NULL");
// A NULL wrapBuffer always disables wrapping outright, regardless of
// wrapWidth - so callers can unconditionally pass NULL to turn wrapping
// off without also having to zero out whatever width they were using.
// Enabling it (a non-NULL buffer) always requires a positive width.
if(wrapBuffer != NULL) {
assertTrue(wrapWidth > 0.0f, "Wrap width must be greater than 0");
assertTrue(wrapBufferMax >= 1, "Wrap buffer max must be at least 1");
}
label->wrapBuffer = wrapBuffer;
label->wrapBufferMax = wrapBufferMax;
label->wrapWidth = wrapBuffer == NULL ? 0.0f : wrapWidth;
label->dirty = true;
} }
void uiLabelSetX(uilabel_t *label, const float_t x) { void uiLabelSetX(uilabel_t *label, const float_t x) {
@@ -50,13 +75,23 @@ float_t uiLabelGetY(const uilabel_t *label) {
void uiLabelRebuffer(uilabel_t *label) { void uiLabelRebuffer(uilabel_t *label) {
if(!label->dirty) return; if(!label->dirty) return;
textMeasure(label->text, &FONT_DEFAULT, &label->width, &label->height); const char_t *renderText = label->text;
if(label->wrapWidth > 0.0f) {
assertNotNull(
label->wrapBuffer, "Wrap buffer cannot be NULL when wrapping is enabled"
);
stringCopy(label->wrapBuffer, label->text, label->wrapBufferMax);
textWrap(label->wrapBuffer, &FONT_DEFAULT, label->wrapWidth);
renderText = label->wrapBuffer;
}
textMeasure(renderText, &FONT_DEFAULT, &label->width, &label->height);
int32_t charIndex = 0; int32_t charIndex = 0;
float_t posX = label->x; float_t posX = label->x;
float_t posY = label->y; float_t posY = label->y;
label->spriteCount = textBuffer( label->spriteCount = textBuffer(
label->x, label->y, label->text, &FONT_DEFAULT, label->x, label->y, renderText, &FONT_DEFAULT,
label->sprites, label->spritesMax, label->sprites, label->spritesMax,
&charIndex, &posX, &posY &charIndex, &posX, &posY
); );
+30
View File
@@ -20,6 +20,15 @@ typedef struct {
float_t x; float_t x;
float_t y; float_t y;
bool_t dirty; bool_t dirty;
// Wrapping - disabled while wrapWidth is 0 (the default). Enabling it
// requires wrapBuffer, a caller-owned scratch buffer at least as large as
// text: label->text is never written to directly (see uiLabelInit), so
// uiLabelRebuffer instead copies it into wrapBuffer and word-wraps that
// copy in place (via textWrap) before measuring/buffering it.
char_t *wrapBuffer;
uint32_t wrapBufferMax;
float_t wrapWidth;
} uilabel_t; } uilabel_t;
/** /**
@@ -73,6 +82,27 @@ void uiLabelSetY(uilabel_t *label, const float_t y);
*/ */
float_t uiLabelGetY(const uilabel_t *label); float_t uiLabelGetY(const uilabel_t *label);
/**
* Enables or disables word-wrap on the label and marks it dirty. Pass
* wrapBuffer NULL to disable wrapping outright (wrapWidth is then
* ignored); enabling it requires both a non-NULL wrapBuffer and a
* wrapWidth greater than 0.
*
* @param label The label to configure.
* @param wrapBuffer Caller-owned scratch buffer, at least as large as the
* label's text, or NULL to disable wrapping.
* @param wrapBufferMax Capacity of wrapBuffer, including the null
* terminator.
* @param wrapWidth Maximum line width in pixels; must be greater than 0
* when wrapBuffer is non-NULL.
*/
void uiLabelSetWrap(
uilabel_t *label,
char_t *wrapBuffer,
const uint32_t wrapBufferMax,
const float_t wrapWidth
);
/** /**
* Rebuffers the label's cached sprites from its current text at its * Rebuffers the label's cached sprites from its current text at its
* current x/y position. No-op unless the label is dirty. Called * current x/y position. No-op unless the label is dirty. Called