From 3ce16cf7f4261ef13504c22b393f2dc93a7e8856 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 19 Sep 2026 21:32:24 -0500 Subject: [PATCH] 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 --- src/dusk/ui/overlay/uifatalerror.c | 8 +++++- src/dusk/ui/overlay/uifatalerror.h | 2 ++ src/dusk/ui/widget/uilabel.c | 39 ++++++++++++++++++++++++++++-- src/dusk/ui/widget/uilabel.h | 30 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/dusk/ui/overlay/uifatalerror.c b/src/dusk/ui/overlay/uifatalerror.c index 53305e48..efc59171 100644 --- a/src/dusk/ui/overlay/uifatalerror.c +++ b/src/dusk/ui/overlay/uifatalerror.c @@ -48,7 +48,13 @@ void uiFatalErrorOpen(const char_t *message) { stringCopy( 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); UI_FATAL_ERROR.open = true; diff --git a/src/dusk/ui/overlay/uifatalerror.h b/src/dusk/ui/overlay/uifatalerror.h index 043b58c1..85e8b6b1 100644 --- a/src/dusk/ui/overlay/uifatalerror.h +++ b/src/dusk/ui/overlay/uifatalerror.h @@ -12,6 +12,7 @@ #define UI_FATAL_ERROR_MESSAGE_MAX 256 #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_QUIT_LABEL "QUIT" #define UI_FATAL_ERROR_GENERIC_MESSAGE \ @@ -23,6 +24,7 @@ typedef struct { uilabel_t messageLabel; 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]; uimenu_t menu; diff --git a/src/dusk/ui/widget/uilabel.c b/src/dusk/ui/widget/uilabel.c index 4879cc49..48109474 100644 --- a/src/dusk/ui/widget/uilabel.c +++ b/src/dusk/ui/widget/uilabel.c @@ -8,6 +8,7 @@ #include "uilabel.h" #include "assert/assert.h" #include "util/memory.h" +#include "util/string.h" #include "display/text/text.h" #include "display/spritebatch/spritebatch.h" #include "display/shader/shaderunlit.h" @@ -25,6 +26,30 @@ void uiLabelInit( label->text = text; label->sprites = sprites; 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) { @@ -50,13 +75,23 @@ float_t uiLabelGetY(const uilabel_t *label) { void uiLabelRebuffer(uilabel_t *label) { 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; float_t posX = label->x; float_t posY = label->y; label->spriteCount = textBuffer( - label->x, label->y, label->text, &FONT_DEFAULT, + label->x, label->y, renderText, &FONT_DEFAULT, label->sprites, label->spritesMax, &charIndex, &posX, &posY ); diff --git a/src/dusk/ui/widget/uilabel.h b/src/dusk/ui/widget/uilabel.h index c771a89d..edc16fab 100644 --- a/src/dusk/ui/widget/uilabel.h +++ b/src/dusk/ui/widget/uilabel.h @@ -20,6 +20,15 @@ typedef struct { float_t x; float_t y; 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; /** @@ -73,6 +82,27 @@ void uiLabelSetY(uilabel_t *label, const float_t y); */ 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 * current x/y position. No-op unless the label is dirty. Called