diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c index dde87683..68f28f95 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -116,6 +116,89 @@ errorret_t textDraw( errorOk(); } +uint32_t textBuildSpriteCache( + const char_t *text, + const font_t *font, + spritebatchsprite_t *sprites, + const uint32_t spritesMax, + int32_t *outWidth, + int32_t *outHeight +) { + assertNotNull(text, "Text cannot be NULL"); + assertNotNull(font, "Font cannot be NULL"); + assertNotNull(sprites, "Sprites cannot be NULL"); + assertNotNull(outWidth, "Output width cannot be NULL"); + assertNotNull(outHeight, "Output height cannot be NULL"); + + uint32_t count = 0; + float_t posX = 0.0f; + float_t posY = 0.0f; + int32_t width = 0; + int32_t lineWidth = 0; + int32_t height = font->tileset->tileHeight; + + char_t c; + int32_t i = 0; + while((c = text[i++]) != '\0') { + if(c == '\n') { + if(lineWidth > width) width = lineWidth; + lineWidth = 0; + posX = 0.0f; + posY += font->tileset->tileHeight; + height += font->tileset->tileHeight; + continue; + } + + if(c == ' ') { + posX += font->tileset->tileWidth; + lineWidth += font->tileset->tileWidth; + continue; + } + + assertTrue(count < spritesMax, "Text produces too many sprites"); + sprites[count++] = textGetSprite((vec2){ posX, posY }, c, font); + + posX += font->tileset->tileWidth; + lineWidth += font->tileset->tileWidth; + } + + if(lineWidth > width) width = lineWidth; + *outWidth = width; + *outHeight = height; + return count; +} + +errorret_t textDrawSpriteCache( + const spritebatchsprite_t *sprites, + const uint32_t spriteCount, + spritebatchsprite_t *scratch, + const float_t x, + const float_t y, + const color_t color, + texture_t *texture +) { + assertNotNull(scratch, "Scratch buffer cannot be NULL"); + assertNotNull(texture, "Texture cannot be NULL"); + if(spriteCount == 0) errorOk(); + + for(uint32_t i = 0; i < spriteCount; i++) { + scratch[i] = sprites[i]; + scratch[i].min[0] += x; + scratch[i].min[1] += y; + scratch[i].max[0] += x; + scratch[i].max[1] += y; + } + + shadermaterial_t material = { + .unlit = { + .color = color, + .texture = texture + } + }; + errorChain(spriteBatchBuffer(scratch, spriteCount, &SHADER_UNLIT, material)); + errorOk(); +} + void textMeasure( const char_t *text, const font_t *font, diff --git a/src/dusk/display/text/text.h b/src/dusk/display/text/text.h index a62bf3bd..82a0dc95 100644 --- a/src/dusk/display/text/text.h +++ b/src/dusk/display/text/text.h @@ -60,6 +60,54 @@ errorret_t textDraw( font_t *font ); +/** + * Builds a cache of sprites (glyph geometry + UVs, relative to origin + * 0,0) for a string of text. Callers that redraw the same text every + * frame (e.g. UI labels/widgets) should build this once and reuse it + * via textDrawSpriteCache, instead of re-deriving glyph geometry every + * frame the way textDraw does. + * + * @param text The null-terminated string to build sprites for. + * @param font Font to use for tile lookup. + * @param sprites Destination array to write sprites into. + * @param spritesMax Capacity of the sprites array. + * @param outWidth Pointer to store the measured width in pixels. + * @param outHeight Pointer to store the measured height in pixels. + * @return The number of sprites written. + */ +uint32_t textBuildSpriteCache( + const char_t *text, + const font_t *font, + spritebatchsprite_t *sprites, + const uint32_t spritesMax, + int32_t *outWidth, + int32_t *outHeight +); + +/** + * Draws a previously-built sprite cache (see textBuildSpriteCache) at the + * given position in a single batched draw call. + * + * @param sprites Cached sprites, relative to origin 0,0. + * @param spriteCount Number of sprites in the cache. + * @param scratch Caller-owned scratch buffer, at least spriteCount + * entries, used to translate the cached sprites into position. + * @param x The x-coordinate to draw the text at. + * @param y The y-coordinate to draw the text at. + * @param color The color to draw the text in. + * @param texture The font's texture to sample glyphs from. + * @return Either an error or success result. + */ +errorret_t textDrawSpriteCache( + const spritebatchsprite_t *sprites, + const uint32_t spriteCount, + spritebatchsprite_t *scratch, + const float_t x, + const float_t y, + const color_t color, + texture_t *texture +); + /** * Measures the width and height of the given text string when rendered. * diff --git a/src/dusk/ui/debug/uifps.c b/src/dusk/ui/debug/uifps.c index b75d6774..796fdc49 100644 --- a/src/dusk/ui/debug/uifps.c +++ b/src/dusk/ui/debug/uifps.c @@ -7,12 +7,21 @@ #include "uifps.h" #include "time/time.h" +#include "util/string.h" #include "display/text/text.h" #include "display/screen/screen.h" #include "engine/engine.h" uifps_t UIFPS; +errorret_t uiFPSInit() { + uiLabelInit(&UIFPS.fpsLabel, &FONT_DEFAULT); + uiLabelInit(&UIFPS.versionLabel, &FONT_DEFAULT); + uiLabelSetText(&UIFPS.versionLabel, ENGINE.version); + uiLabelSetColor(&UIFPS.versionLabel, color(255, 255, 255, 128)); + errorOk(); +} + errorret_t uiFPSDraw() { char_t fpsText[32]; @@ -49,21 +58,18 @@ errorret_t uiFPSDraw() { textColor = COLOR_RED; } - errorChain(textDraw( - (float_t)SCREEN.scanX, - (float_t)SCREEN.scanY, - fpsText, textColor, - &FONT_DEFAULT + uiLabelSetColor(&UIFPS.fpsLabel, textColor); + uiLabelSetText(&UIFPS.fpsLabel, fpsText); + errorChain(uiLabelDraw( + &UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY )); int32_t versionWidth, versionHeight; - textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight); - errorChain(textDraw( + uiLabelGetSize(&UIFPS.versionLabel, &versionWidth, &versionHeight); + errorChain(uiLabelDraw( + &UIFPS.versionLabel, (float_t)(SCREEN.scanX + SCREEN.scanWidth - versionWidth), - (float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight), - ENGINE.version, - color(255, 255, 255, 128), - &FONT_DEFAULT + (float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight) )); errorOk(); diff --git a/src/dusk/ui/debug/uifps.h b/src/dusk/ui/debug/uifps.h index 6f9c4592..93807457 100644 --- a/src/dusk/ui/debug/uifps.h +++ b/src/dusk/ui/debug/uifps.h @@ -8,17 +8,27 @@ #pragma once #include "error/error.h" #include "time/timeepoch.h" +#include "ui/widget/uilabel.h" typedef struct { dusktimeepoch_t lastTick; float_t fpsAverage; + uilabel_t fpsLabel; + uilabel_t versionLabel; } uifps_t; extern uifps_t UIFPS; +/** + * Initializes the FPS counter, caching the (static) version label. + * + * @return Any error that occurs. + */ +errorret_t uiFPSInit(); + /** * Draws the FPS counter on the screen, and also does the update (for now). - * + * * @return Any error that occurs. */ errorret_t uiFPSDraw(); \ No newline at end of file diff --git a/src/dusk/ui/frame/uiconfirm.c b/src/dusk/ui/frame/uiconfirm.c index 58cfdb94..740fb9e5 100644 --- a/src/dusk/ui/frame/uiconfirm.c +++ b/src/dusk/ui/frame/uiconfirm.c @@ -9,7 +9,6 @@ #include "uiframe.h" #include "assert/assert.h" #include "util/memory.h" -#include "util/string.h" #include "util/math.h" #include "display/screen/screen.h" #include "display/text/text.h" @@ -39,6 +38,7 @@ void uiConfirmClosed(const uimenu_t *menu) { errorret_t uiConfirmInit(void) { memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t)); + uiLabelInit(&UI_CONFIRM.textLabel, &FONT_DEFAULT); MENU_BEGIN( &UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL @@ -71,7 +71,7 @@ errorret_t uiConfirmDraw(void) { ); int32_t textW, textH; - textMeasure(UI_CONFIRM.text, &FONT_DEFAULT, &textW, &textH); + uiLabelGetSize(&UI_CONFIRM.textLabel, &textW, &textH); float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; float_t width = mathMax( @@ -88,7 +88,7 @@ errorret_t uiConfirmDraw(void) { float_t contentY = y + UI_FRAME_START_Y; float_t contentWidth = width - (UI_FRAME_START_X * 2); - errorChain(textDraw(contentX, contentY, UI_CONFIRM.text, COLOR_WHITE, &FONT_DEFAULT)); + errorChain(uiLabelDraw(&UI_CONFIRM.textLabel, contentX, contentY)); float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y; errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight)); @@ -110,7 +110,7 @@ void uiConfirmOpen( void *user ) { assertNotNull(question, "Question cannot be NULL"); - stringCopy(UI_CONFIRM.text, question, UI_CONFIRM_TEXT_MAX); + uiLabelSetText(&UI_CONFIRM.textLabel, question); UI_CONFIRM.callback = callback; UI_CONFIRM.user = user; UI_CONFIRM.result = false; diff --git a/src/dusk/ui/frame/uiconfirm.h b/src/dusk/ui/frame/uiconfirm.h index 38697048..73b362c4 100644 --- a/src/dusk/ui/frame/uiconfirm.h +++ b/src/dusk/ui/frame/uiconfirm.h @@ -8,8 +8,8 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "ui/widget/uilabel.h" -#define UI_CONFIRM_TEXT_MAX 256 #define UI_CONFIRM_MIN_WIDTH 160.0f #define UI_CONFIRM_INDEX_CONFIRM 0 #define UI_CONFIRM_INDEX_CANCEL 1 @@ -25,7 +25,7 @@ typedef void (*uiconfirmcallback_t)(const bool_t result, void *user); typedef struct { - char_t text[UI_CONFIRM_TEXT_MAX]; + uilabel_t textLabel; uimenu_t menu; uimenuitem_t items[UI_CONFIRM_ITEM_COUNT]; uiconfirmcallback_t callback; diff --git a/src/dusk/ui/overlay/uiloading.c b/src/dusk/ui/overlay/uiloading.c index 05b932a4..1c1c800c 100644 --- a/src/dusk/ui/overlay/uiloading.c +++ b/src/dusk/ui/overlay/uiloading.c @@ -19,6 +19,8 @@ uiloading_t UI_LOADING; errorret_t uiLoadingInit(void) { memoryZero(&UI_LOADING, sizeof(uiloading_t)); + uiLabelInit(&UI_LOADING.textLabel, &FONT_DEFAULT); + uiLabelSetText(&UI_LOADING.textLabel, UI_LOADING_TEXT); eventInit( &UI_LOADING.onTransitionEnd, UI_LOADING.onTransitionEndCallbacks, @@ -52,7 +54,7 @@ errorret_t uiLoadingDraw(void) { if(alpha <= 0.0f) errorOk(); int32_t textW, textH; - textMeasure(UI_LOADING_TEXT, &FONT_DEFAULT, &textW, &textH); + uiLabelGetSize(&UI_LOADING.textLabel, &textW, &textH); float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - (float_t)textW - UI_LOADING_MARGIN; @@ -61,8 +63,9 @@ errorret_t uiLoadingDraw(void) { color_t color = COLOR_WHITE; color.a = (uint8_t)(alpha * 255.0f); + uiLabelSetColor(&UI_LOADING.textLabel, color); - errorChain(textDraw(x, y, UI_LOADING_TEXT, color, &FONT_DEFAULT)); + errorChain(uiLabelDraw(&UI_LOADING.textLabel, x, y)); errorOk(); } diff --git a/src/dusk/ui/overlay/uiloading.h b/src/dusk/ui/overlay/uiloading.h index af67db6c..91f4c427 100644 --- a/src/dusk/ui/overlay/uiloading.h +++ b/src/dusk/ui/overlay/uiloading.h @@ -8,6 +8,7 @@ #pragma once #include "error/error.h" #include "event/event.h" +#include "ui/widget/uilabel.h" #define UI_LOADING_FADE_DURATION 0.5f #define UI_LOADING_MARGIN 8.0f @@ -20,6 +21,7 @@ typedef struct { eventcallback_t onTransitionEndCallbacks[4]; void *onTransitionEndUsers[4]; event_t onTransitionEnd; + uilabel_t textLabel; } uiloading_t; extern uiloading_t UI_LOADING; diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c index 2d89d901..e875be04 100644 --- a/src/dusk/ui/ui.c +++ b/src/dusk/ui/ui.c @@ -18,6 +18,7 @@ ui_t UI; errorret_t uiInit(void) { memoryZero(&UI, sizeof(ui_t)); uiFocusInit(); + uiElementsSort(); uielement_t *element = &UI_ELEMENTS[0]; while(!uiElementIsNull(element)) { diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index a16ac836..f119f534 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -1,12 +1,13 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "uielement.h" #include "assert/assert.h" +#include "util/sort.h" #include "ui/frame/uiframe.h" #include "ui/debug/uifps.h" #include "engine/engine.h" @@ -15,64 +16,26 @@ #include "ui/overlay/uicrop.h" #include "ui/transition/uitransition.h" #include "ui/debug/uiconsole.h" -#include "ui/frame/settings/uisettings.h" #include "ui/frame/uiconfirm.h" +// Priming pass: X does nothing here, so this only exists to process any +// #include directives nested in uielementlist.h/ui/uielementgame.h at +// file scope (each such header's own #pragma once makes it a no-op on +// the real pass below, which happens inside UI_ELEMENTS[]'s braces, +// where a raw #include of a declaration would be invalid). +#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing +#include "uielementlist.h" +#undef X + uielement_t UI_ELEMENTS[] = { - { - .init = uiFrameInit, - .dispose = uiFrameDispose - }, + #define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \ + { \ + .init = initFn, .update = updateFn, .draw = drawFn, \ + .dispose = disposeFn, .order = elementOrder \ + }, + #include "uielementlist.h" + #undef X - // Fullbox under: above scene, below system UI. - { - .init = uiFullboxUnderInit, - .update = uiFullboxUnderUpdate, - .draw = uiFullboxUnderDraw - }, - - { - .init = uiSettingsInit, - .update = uiSettingsUpdate, - .draw = uiSettingsDraw, - .dispose = uiSettingsDispose - }, - - // Text stuffs - { - .init = uiConfirmInit, - .draw = uiConfirmDraw, - .dispose = uiConfirmDispose - }, - - { - .init = uiTransitionInit, - .update = uiTransitionUpdate, - .draw = uiTransitionDraw - }, - - // Fullbox over: above absolutely everything. - { - .init = uiFullboxOverInit, - .update = uiFullboxOverUpdate, - .draw = uiFullboxOverDraw - }, - - { - .init = uiLoadingInit, - .update = uiLoadingUpdate, - .draw = uiLoadingDraw - }, - - { - .init = uiCropInit, - .draw = uiCropDraw - }, - - // Debug items - { .draw = uiConsoleDraw, .dispose = uiConsoleDispose }, - { .draw = uiFPSDraw }, - { 0 } // Null terminator }; @@ -83,9 +46,23 @@ bool_t uiElementIsNull(const uielement_t *element) { element->dispose == NULL; } +int_t uiElementCompareOrder(const void *a, const void *b) { + const uielement_t *elementA = (const uielement_t *)a; + const uielement_t *elementB = (const uielement_t *)b; + if(elementA->order < elementB->order) return -1; + if(elementA->order > elementB->order) return 1; + return 0; +} + +void uiElementsSort(void) { + // The trailing null terminator is always the last static entry -- sort + // everything before it, and leave it in place. + const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1; + sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder); +} + errorret_t uiElementInit(uielement_t *element) { assertNotNull(element, "element must not be NULL"); - errorChain(uiFrameInit()); if(element->init != NULL) errorChain(element->init()); errorOk(); } @@ -105,6 +82,5 @@ errorret_t uiElementDraw(const uielement_t *element) { errorret_t uiElementDispose(uielement_t *element) { assertNotNull(element, "element must not be NULL"); if(element->dispose != NULL) errorChain(element->dispose()); - errorChain(uiFrameDispose()); errorOk(); -} \ No newline at end of file +} diff --git a/src/dusk/ui/uielement.h b/src/dusk/ui/uielement.h index 49db9d60..d9b8a525 100644 --- a/src/dusk/ui/uielement.h +++ b/src/dusk/ui/uielement.h @@ -8,11 +8,19 @@ #pragma once #include "error/error.h" +// Built-in order tiers. Lower values update/render first; ties preserve +// their relative UI_ELEMENTS declaration order (uiElementsSort uses a +// stable sort). Game-specific elements can use any int32_t value -- these +// are just the ones the engine itself relies on. +#define UI_ELEMENT_ORDER_DEFAULT 0 +#define UI_ELEMENT_ORDER_DEBUG 1000 + typedef struct { errorret_t (*init)(); errorret_t (*update)(); errorret_t (*draw)(); errorret_t (*dispose)(); + int32_t order; } uielement_t; extern uielement_t UI_ELEMENTS[]; @@ -26,6 +34,23 @@ extern uielement_t UI_ELEMENTS[]; */ bool_t uiElementIsNull(const uielement_t *element); +/** + * Compares two elements by their .order field, ascending. Matches + * sortcompare_t, for use with the project's sort utilities. + * + * @param a First uielement_t to compare. + * @param b Second uielement_t to compare. + * @return Negative if a < b, zero if a == b, positive if a > b. + */ +int_t uiElementCompareOrder(const void *a, const void *b); + +/** + * Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing + * null terminator is never moved. Called once by uiInit -- element order + * is static after that, so there's no need to re-sort every frame. + */ +void uiElementsSort(void); + /** * Initializes a UI element, invoking its init callback if set. * diff --git a/src/dusk/ui/uielementlist.h b/src/dusk/ui/uielementlist.h new file mode 100644 index 00000000..5732cd8a --- /dev/null +++ b/src/dusk/ui/uielementlist.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +// X(init, update, draw, dispose, order) -- pass NULL for any callback the +// element doesn't need. order controls update/draw sequence (lower first, +// see UI_ELEMENT_ORDER_* in uielement.h); ties preserve declaration order. +// See uielement.c for how this expands. + +X(uiFrameInit, NULL, NULL, uiFrameDispose, UI_ELEMENT_ORDER_DEFAULT) + +// Fullbox under: above scene, below system UI. +X( + uiFullboxUnderInit, uiFullboxUnderUpdate, uiFullboxUnderDraw, NULL, + UI_ELEMENT_ORDER_DEFAULT +) + +// Text stuffs +X(uiConfirmInit, NULL, uiConfirmDraw, uiConfirmDispose, UI_ELEMENT_ORDER_DEFAULT) + +X( + uiTransitionInit, uiTransitionUpdate, uiTransitionDraw, NULL, + UI_ELEMENT_ORDER_DEFAULT +) + +// Fullbox over: above absolutely everything (except debug). +X( + uiFullboxOverInit, uiFullboxOverUpdate, uiFullboxOverDraw, NULL, + UI_ELEMENT_ORDER_DEFAULT +) + +X( + uiLoadingInit, uiLoadingUpdate, uiLoadingDraw, NULL, + UI_ELEMENT_ORDER_DEFAULT +) + +X(uiCropInit, NULL, uiCropDraw, NULL, UI_ELEMENT_ORDER_DEFAULT) + +// Debug items -- always last. +X(NULL, NULL, uiConsoleDraw, uiConsoleDispose, UI_ELEMENT_ORDER_DEBUG) +X(uiFPSInit, NULL, uiFPSDraw, NULL, UI_ELEMENT_ORDER_DEBUG) + +// Game-specific UI elements (duskrpg) +#include "ui/uielementgame.h" diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index 77cd1ebd..6beebee4 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -12,4 +12,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} uidropdown.c uiscrolling.c uimenu.c + uilabel.c + uiwidgetlabel.c ) diff --git a/src/dusk/ui/widget/uibutton.c b/src/dusk/ui/widget/uibutton.c index 732c122d..8ff33b45 100644 --- a/src/dusk/ui/widget/uibutton.c +++ b/src/dusk/ui/widget/uibutton.c @@ -7,15 +7,15 @@ #include "uibutton.h" #include "assert/assert.h" -#include "util/memory.h" #include "display/text/text.h" #include "display/color.h" void uiButtonInit(uibutton_t *button, const char_t *label) { assertNotNull(button, "Button cannot be NULL"); assertNotNull(label, "Label cannot be NULL"); - memoryZero(button, sizeof(uibutton_t)); - button->label = label; + uiWidgetLabelInit(&button->label, &FONT_DEFAULT); + uiWidgetLabelSetText(&button->label, label); + button->highlighted = false; } bool_t uiButtonIsHighlighted(const uibutton_t *button) { @@ -26,6 +26,9 @@ bool_t uiButtonIsHighlighted(const uibutton_t *button) { void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) { assertNotNull(button, "Button cannot be NULL"); button->highlighted = highlighted; + uiWidgetLabelSetColor( + &button->label, highlighted ? COLOR_RED : COLOR_WHITE + ); } errorret_t uiButtonDraw( @@ -34,10 +37,6 @@ errorret_t uiButtonDraw( const float_t y ) { assertNotNull(button, "Button cannot be NULL"); - errorChain(textDraw( - x, y, button->label, - button->highlighted ? COLOR_RED : COLOR_WHITE, - &FONT_DEFAULT - )); + errorChain(uiWidgetLabelDraw(&button->label, x, y)); errorOk(); } diff --git a/src/dusk/ui/widget/uibutton.h b/src/dusk/ui/widget/uibutton.h index 0085f564..348317ab 100644 --- a/src/dusk/ui/widget/uibutton.h +++ b/src/dusk/ui/widget/uibutton.h @@ -7,9 +7,10 @@ #pragma once #include "error/error.h" +#include "ui/widget/uiwidgetlabel.h" typedef struct { - const char_t *label; + uiwidgetlabel_t label; bool_t highlighted; } uibutton_t; diff --git a/src/dusk/ui/widget/uicheckbox.c b/src/dusk/ui/widget/uicheckbox.c index 59e6ad10..abbbcdf2 100644 --- a/src/dusk/ui/widget/uicheckbox.c +++ b/src/dusk/ui/widget/uicheckbox.c @@ -7,6 +7,7 @@ #include "uicheckbox.h" #include "util/memory.h" +#include "util/string.h" #include "display/text/text.h" #include "display/color.h" @@ -15,7 +16,9 @@ void uiCheckboxInit( const char_t *label ) { memoryZero(checkbox, sizeof(uicheckbox_t)); - checkbox->label = label; + checkbox->rawLabel = label; + uiWidgetLabelInit(&checkbox->label, &FONT_DEFAULT); + uiCheckboxRebuildLabel(checkbox); } bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) { @@ -24,6 +27,7 @@ bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) { void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) { checkbox->checked = checked; + uiCheckboxRebuildLabel(checkbox); } void uiCheckboxToggle(uicheckbox_t *checkbox) { @@ -36,6 +40,9 @@ bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox) { void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted) { checkbox->highlighted = highlighted; + uiWidgetLabelSetColor( + &checkbox->label, highlighted ? COLOR_RED : COLOR_WHITE + ); } errorret_t uiCheckboxDraw( @@ -43,15 +50,15 @@ errorret_t uiCheckboxDraw( const float_t x, const float_t y ) { - color_t color = checkbox->highlighted ? COLOR_RED : COLOR_WHITE; - const char_t *mark = checkbox->checked ? "Y " : "N "; - errorChain(textDraw(x, y, mark, color, &FONT_DEFAULT)); - - int32_t markW, markH; - textMeasure(mark, &FONT_DEFAULT, &markW, &markH); - errorChain(textDraw( - x + (float_t)markW, y, checkbox->label, color, &FONT_DEFAULT - )); - + errorChain(uiWidgetLabelDraw(&checkbox->label, x, y)); errorOk(); } + +void uiCheckboxRebuildLabel(uicheckbox_t *checkbox) { + char_t combined[UI_WIDGET_LABEL_TEXT_MAX]; + stringFormat( + combined, UI_WIDGET_LABEL_TEXT_MAX - 1, "%s %s", + checkbox->checked ? "Y" : "N", checkbox->rawLabel + ); + uiWidgetLabelSetText(&checkbox->label, combined); +} diff --git a/src/dusk/ui/widget/uicheckbox.h b/src/dusk/ui/widget/uicheckbox.h index b32d52cc..f7ca094e 100644 --- a/src/dusk/ui/widget/uicheckbox.h +++ b/src/dusk/ui/widget/uicheckbox.h @@ -7,9 +7,11 @@ #pragma once #include "error/error.h" +#include "ui/widget/uiwidgetlabel.h" typedef struct uicheckbox_s { - const char_t *label; + const char_t *rawLabel; + uiwidgetlabel_t label; bool_t checked; bool_t highlighted; } uicheckbox_t; @@ -77,3 +79,12 @@ errorret_t uiCheckboxDraw( const float_t x, const float_t y ); + +/** + * Rebuilds the checkbox's cached label ("Y "/"N " mark plus rawLabel) + * from its current checked state. Called internally whenever checked + * changes. + * + * @param checkbox The checkbox to update. + */ +void uiCheckboxRebuildLabel(uicheckbox_t *checkbox); diff --git a/src/dusk/ui/widget/uidropdown.c b/src/dusk/ui/widget/uidropdown.c index ae72bd4e..861b38d2 100644 --- a/src/dusk/ui/widget/uidropdown.c +++ b/src/dusk/ui/widget/uidropdown.c @@ -25,10 +25,13 @@ void uiDropdownInit( assertTrue(optionCount > 0, "Dropdown must have at least one option"); memoryZero(dropdown, sizeof(uidropdown_t)); - dropdown->label = label; + uiWidgetLabelInit(&dropdown->label, &FONT_DEFAULT); + uiWidgetLabelSetText(&dropdown->label, label); + uiWidgetLabelInit(&dropdown->value, &FONT_DEFAULT); dropdown->options = options; dropdown->optionCount = optionCount; dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0; + uiDropdownRebuildValue(dropdown); } uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) { @@ -45,18 +48,21 @@ void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index) { assertNotNull(dropdown, "Dropdown cannot be NULL"); assertTrue(index < dropdown->optionCount, "Dropdown index out of range"); dropdown->selectedIndex = index; + uiDropdownRebuildValue(dropdown); } void uiDropdownStepNext(uidropdown_t *dropdown) { assertNotNull(dropdown, "Dropdown cannot be NULL"); dropdown->selectedIndex = (dropdown->selectedIndex + 1) % dropdown->optionCount; + uiDropdownRebuildValue(dropdown); } void uiDropdownStepPrev(uidropdown_t *dropdown) { assertNotNull(dropdown, "Dropdown cannot be NULL"); dropdown->selectedIndex = dropdown->selectedIndex == 0 ? dropdown->optionCount - 1 : dropdown->selectedIndex - 1; + uiDropdownRebuildValue(dropdown); } bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) { @@ -70,6 +76,9 @@ void uiDropdownSetHighlighted( ) { assertNotNull(dropdown, "Dropdown cannot be NULL"); dropdown->highlighted = highlighted; + color_t color = highlighted ? COLOR_RED : COLOR_WHITE; + uiWidgetLabelSetColor(&dropdown->label, color); + uiWidgetLabelSetColor(&dropdown->value, color); } errorret_t uiDropdownDraw( @@ -79,22 +88,23 @@ errorret_t uiDropdownDraw( ) { assertNotNull(dropdown, "Dropdown cannot be NULL"); - color_t color = dropdown->highlighted ? COLOR_RED : COLOR_WHITE; - - errorChain(textDraw(x, y, dropdown->label, color, &FONT_DEFAULT)); + errorChain(uiWidgetLabelDraw(&dropdown->label, x, y)); int32_t labelW, labelH; - textMeasure(dropdown->label, &FONT_DEFAULT, &labelW, &labelH); + uiWidgetLabelGetSize(&dropdown->label, &labelW, &labelH); - char_t valueText[UI_DROPDOWN_VALUE_TEXT_MAX]; - stringFormat( - valueText, UI_DROPDOWN_VALUE_TEXT_MAX - 1, "< %s >", - uiDropdownGetSelectedOption(dropdown) - ); - - errorChain(textDraw( - x + (float_t)labelW + UI_DROPDOWN_GAP, y, valueText, color, &FONT_DEFAULT + errorChain(uiWidgetLabelDraw( + &dropdown->value, x + (float_t)labelW + UI_DROPDOWN_GAP, y )); errorOk(); } + +void uiDropdownRebuildValue(uidropdown_t *dropdown) { + char_t valueText[UI_WIDGET_LABEL_TEXT_MAX]; + stringFormat( + valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "< %s >", + uiDropdownGetSelectedOption(dropdown) + ); + uiWidgetLabelSetText(&dropdown->value, valueText); +} diff --git a/src/dusk/ui/widget/uidropdown.h b/src/dusk/ui/widget/uidropdown.h index 0c6884ac..ad36e2ff 100644 --- a/src/dusk/ui/widget/uidropdown.h +++ b/src/dusk/ui/widget/uidropdown.h @@ -7,12 +7,13 @@ #pragma once #include "error/error.h" +#include "ui/widget/uiwidgetlabel.h" #define UI_DROPDOWN_GAP 4.0f -#define UI_DROPDOWN_VALUE_TEXT_MAX 64 typedef struct { - const char_t *label; + uiwidgetlabel_t label; + uiwidgetlabel_t value; const char_t *const *options; uint8_t optionCount; uint8_t selectedIndex; @@ -111,3 +112,12 @@ errorret_t uiDropdownDraw( const float_t x, const float_t y ); + +/** + * Rebuilds the dropdown's cached value label ("< option >") from its + * current selectedIndex. Called internally whenever selectedIndex + * changes. + * + * @param dropdown The dropdown to update. + */ +void uiDropdownRebuildValue(uidropdown_t *dropdown); diff --git a/src/dusk/ui/widget/uilabel.c b/src/dusk/ui/widget/uilabel.c new file mode 100644 index 00000000..53afb233 --- /dev/null +++ b/src/dusk/ui/widget/uilabel.c @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uilabel.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/text/text.h" + +void uiLabelInit(uilabel_t *label, font_t *font) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(font, "Font cannot be NULL"); + + memoryZero(label, sizeof(uilabel_t)); + label->font = font; + label->color = COLOR_WHITE; +} + +void uiLabelSetText(uilabel_t *label, const char_t *text) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(text, "Text cannot be NULL"); + assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long"); + + stringCopy(label->text, text, UI_LABEL_TEXT_MAX); + label->spriteCount = textBuildSpriteCache( + label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX, + &label->width, &label->height + ); +} + +void uiLabelSetColor(uilabel_t *label, const color_t color) { + assertNotNull(label, "Label cannot be NULL"); + label->color = color; +} + +void uiLabelGetSize( + const uilabel_t *label, + int32_t *outWidth, + int32_t *outHeight +) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(outWidth, "Output width cannot be NULL"); + assertNotNull(outHeight, "Output height cannot be NULL"); + *outWidth = label->width; + *outHeight = label->height; +} + +errorret_t uiLabelDraw( + const uilabel_t *label, + const float_t x, + const float_t y +) { + assertNotNull(label, "Label cannot be NULL"); + if(label->spriteCount == 0) errorOk(); + + // Cached sprites are relative to (0,0); textDrawSpriteCache translates + // into the requested screen position here instead of in + // uiLabelSetText, so a label can be repositioned every frame without + // rebuilding the (much more expensive) glyph/UV cache. + spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX]; + errorChain(textDrawSpriteCache( + label->sprites, label->spriteCount, scratch, x, y, label->color, + label->font->texture + )); + errorOk(); +} diff --git a/src/dusk/ui/widget/uilabel.h b/src/dusk/ui/widget/uilabel.h new file mode 100644 index 00000000..d0f6e10d --- /dev/null +++ b/src/dusk/ui/widget/uilabel.h @@ -0,0 +1,85 @@ +/** + * 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/text/font.h" +#include "display/spritebatch/spritebatchsprite.h" +#include "display/color.h" + +#define UI_LABEL_TEXT_MAX 256 +#define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX + +typedef struct { + char_t text[UI_LABEL_TEXT_MAX]; + color_t color; + font_t *font; + + spritebatchsprite_t sprites[UI_LABEL_SPRITE_COUNT_MAX]; + uint32_t spriteCount; + int32_t width; + int32_t height; +} uilabel_t; + +/** + * Initializes a label, defaulting to white text and no text set. + * + * @param label The label to initialize. + * @param font Font to use for rendering. Must outlive the label. + */ +void uiLabelInit(uilabel_t *label, font_t *font); + +/** + * Sets the label's text, rebuilding its cached sprites (glyph lookup, UV, + * and layout) immediately. This is the only way the sprite cache gets + * rebuilt -- uiLabelDraw never recomputes it, so call this whenever the + * text changes rather than once up front and expecting it to stay in sync. + * + * @param label The label to update. + * @param text Null-terminated string to display. Must be shorter than + * UI_LABEL_TEXT_MAX. + */ +void uiLabelSetText(uilabel_t *label, const char_t *text); + +/** + * Sets the label's tint color. Cheap -- doesn't touch the sprite cache, + * since color is applied via the draw material, not baked into sprites. + * + * @param label The label to update. + * @param color The new tint color. + */ +void uiLabelSetColor(uilabel_t *label, const color_t color); + +/** + * Gets the measured size (in pixels) of the label's current text, cached + * from the last uiLabelSetText call. + * + * @param label The label to query. + * @param outWidth Pointer to store the width. + * @param outHeight Pointer to store the height. + */ +void uiLabelGetSize( + const uilabel_t *label, + int32_t *outWidth, + int32_t *outHeight +); + +/** + * Draws the label's cached sprites at the given screen position in a + * single batched buffer call. Does not recompute glyph layout -- call + * uiLabelSetText first whenever the text changes. + * + * @param label The label to draw. + * @param x Screen x position. + * @param y Screen y position. + * @return Any error that occurs. + */ +errorret_t uiLabelDraw( + const uilabel_t *label, + const float_t x, + const float_t y +); diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c index 5eb10597..344e5352 100644 --- a/src/dusk/ui/widget/uimenu.c +++ b/src/dusk/ui/widget/uimenu.c @@ -10,6 +10,7 @@ #include "util/memory.h" #include "display/text/text.h" #include "display/color.h" +#include "ui/widget/uiwidgetlabel.h" #include "ui/widget/uibutton.h" #include "ui/widget/uitab.h" #include "ui/widget/uislider.h" @@ -102,12 +103,8 @@ errorret_t uiMenuDraw( if(item->type == UI_MENU_WIDGET_TYPE_LABEL) { if(col > 0) { row++; col = 0; } - errorChain(textDraw( - x, - y + (float_t)row * rowHeight, - item->label, - COLOR_WHITE, - &FONT_DEFAULT + errorChain(uiWidgetLabelDraw( + &item->label, x, y + (float_t)row * rowHeight )); row++; continue; @@ -274,6 +271,13 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) { return 0xFF; } +void uiMenuLabelInit(uimenuitem_t *item, const char_t *text) { + assertNotNull(item, "Item cannot be NULL"); + assertNotNull(text, "Text cannot be NULL"); + uiWidgetLabelInit(&item->label, &FONT_DEFAULT); + uiWidgetLabelSetText(&item->label, text); +} + void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) { if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { uiCheckboxSetHighlighted(&item->checkbox, highlighted); diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h index 870ed130..ffd909cb 100644 --- a/src/dusk/ui/widget/uimenu.h +++ b/src/dusk/ui/widget/uimenu.h @@ -8,14 +8,13 @@ #pragma once #include "error/error.h" #include "ui/focus/uifocus.h" +#include "ui/widget/uiwidgetlabel.h" #include "ui/widget/uibutton.h" #include "ui/widget/uicheckbox.h" #include "ui/widget/uitab.h" #include "ui/widget/uislider.h" #include "ui/widget/uidropdown.h" -#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX - typedef struct uimenu_s uimenu_t; typedef enum { @@ -32,7 +31,7 @@ typedef enum { typedef struct { uimenuwidgettype_t type; union { - char_t *label; + uiwidgetlabel_t label; uicheckbox_t checkbox; uibutton_t button; uitab_t tab; @@ -178,6 +177,15 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot); */ void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted); +/** + * Initializes a menu item as a standalone label, caching its text. + * Used internally by the MENU_LABEL helper macro. + * + * @param item The item to initialize. + * @param text Display text. + */ +void uiMenuLabelInit(uimenuitem_t *item, const char_t *text); + /** * Internal focus callback - forwards selection to the menu's selected handler. * @@ -227,7 +235,7 @@ bool_t uiMenuFocusDirection( #define MENU_LABEL(text) \ assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \ - menu->items[menuIndex].label = text; \ + uiMenuLabelInit(&menu->items[menuIndex], text); \ ++menuIndex #define MENU_SPACER() \ diff --git a/src/dusk/ui/widget/uislider.c b/src/dusk/ui/widget/uislider.c index e08a041d..647c5df5 100644 --- a/src/dusk/ui/widget/uislider.c +++ b/src/dusk/ui/widget/uislider.c @@ -30,12 +30,15 @@ void uiSliderInitFloat( assertTrue(step > 0.0f, "Slider step must be greater than zero"); memoryZero(slider, sizeof(uislider_t)); - slider->label = label; + uiWidgetLabelInit(&slider->label, &FONT_DEFAULT); + uiWidgetLabelSetText(&slider->label, label); + uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT); slider->type = UI_SLIDER_TYPE_FLOAT; slider->min.f = min; slider->max.f = max; slider->step.f = step; slider->value.f = mathClamp(value, min, max); + uiSliderRebuildValueLabel(slider); } void uiSliderInitInt( @@ -52,12 +55,15 @@ void uiSliderInitInt( assertTrue(step > 0, "Slider step must be greater than zero"); memoryZero(slider, sizeof(uislider_t)); - slider->label = label; + uiWidgetLabelInit(&slider->label, &FONT_DEFAULT); + uiWidgetLabelSetText(&slider->label, label); + uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT); slider->type = UI_SLIDER_TYPE_INT; slider->min.i = min; slider->max.i = max; slider->step.i = step; slider->value.i = mathClamp(value, min, max); + uiSliderRebuildValueLabel(slider); } float_t uiSliderGetFloat(const uislider_t *slider) { @@ -80,6 +86,7 @@ void uiSliderSetFloat(uislider_t *slider, const float_t value) { slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider" ); slider->value.f = mathClamp(value, slider->min.f, slider->max.f); + uiSliderRebuildValueLabel(slider); } void uiSliderSetInt(uislider_t *slider, const int32_t value) { @@ -88,6 +95,7 @@ void uiSliderSetInt(uislider_t *slider, const int32_t value) { slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider" ); slider->value.i = mathClamp(value, slider->min.i, slider->max.i); + uiSliderRebuildValueLabel(slider); } void uiSliderStepUp(uislider_t *slider) { @@ -138,6 +146,9 @@ bool_t uiSliderIsHighlighted(const uislider_t *slider) { void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) { assertNotNull(slider, "Slider cannot be NULL"); slider->highlighted = highlighted; + color_t color = highlighted ? COLOR_RED : COLOR_WHITE; + uiWidgetLabelSetColor(&slider->label, color); + uiWidgetLabelSetColor(&slider->valueLabel, color); } errorret_t uiSliderDraw( @@ -149,10 +160,10 @@ errorret_t uiSliderDraw( color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE; - errorChain(textDraw(x, y, slider->label, color, &FONT_DEFAULT)); + errorChain(uiWidgetLabelDraw(&slider->label, x, y)); int32_t labelW, labelH; - textMeasure(slider->label, &FONT_DEFAULT, &labelW, &labelH); + uiWidgetLabelGetSize(&slider->label, &labelW, &labelH); float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP; float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f; @@ -219,21 +230,23 @@ errorret_t uiSliderDraw( } } - char_t valueText[UI_SLIDER_VALUE_TEXT_MAX]; - if(slider->type == UI_SLIDER_TYPE_INT) { - stringFormat( - valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%d", slider->value.i - ); - } else { - stringFormat( - valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%.2f", slider->value.f - ); - } - - errorChain(textDraw( - trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y, - valueText, color, &FONT_DEFAULT + errorChain(uiWidgetLabelDraw( + &slider->valueLabel, trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y )); errorOk(); } + +void uiSliderRebuildValueLabel(uislider_t *slider) { + char_t valueText[UI_WIDGET_LABEL_TEXT_MAX]; + if(slider->type == UI_SLIDER_TYPE_INT) { + stringFormat( + valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "%d", slider->value.i + ); + } else { + stringFormat( + valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "%.2f", slider->value.f + ); + } + uiWidgetLabelSetText(&slider->valueLabel, valueText); +} diff --git a/src/dusk/ui/widget/uislider.h b/src/dusk/ui/widget/uislider.h index d53dee00..8da1b5a0 100644 --- a/src/dusk/ui/widget/uislider.h +++ b/src/dusk/ui/widget/uislider.h @@ -7,13 +7,13 @@ #pragma once #include "error/error.h" +#include "ui/widget/uiwidgetlabel.h" #define UI_SLIDER_TRACK_WIDTH 80.0f #define UI_SLIDER_TRACK_HEIGHT 4.0f #define UI_SLIDER_STEP_MARKER_WIDTH 2.0f #define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f #define UI_SLIDER_GAP 4.0f -#define UI_SLIDER_VALUE_TEXT_MAX 16 /** * Number of discrete steps below which an int slider renders individual @@ -34,7 +34,8 @@ typedef union { } uislidervalue_t; typedef struct { - const char_t *label; + uiwidgetlabel_t label; + uiwidgetlabel_t valueLabel; uislidertype_t type; uislidervalue_t value; uislidervalue_t min; @@ -183,3 +184,11 @@ errorret_t uiSliderDraw( const float_t x, const float_t y ); + +/** + * Rebuilds the slider's cached value label from its current value. + * Called internally whenever value changes. + * + * @param slider The slider to update. + */ +void uiSliderRebuildValueLabel(uislider_t *slider); diff --git a/src/dusk/ui/widget/uitab.c b/src/dusk/ui/widget/uitab.c index 3ba0fdab..684b2f02 100644 --- a/src/dusk/ui/widget/uitab.c +++ b/src/dusk/ui/widget/uitab.c @@ -7,7 +7,6 @@ #include "uitab.h" #include "assert/assert.h" -#include "util/memory.h" #include "display/text/text.h" #include "display/color.h" #include "display/spritebatch/spritebatch.h" @@ -17,8 +16,9 @@ void uiTabInit(uitab_t *tab, const char_t *label) { assertNotNull(tab, "Tab cannot be NULL"); assertNotNull(label, "Label cannot be NULL"); - memoryZero(tab, sizeof(uitab_t)); - tab->label = label; + uiWidgetLabelInit(&tab->label, &FONT_DEFAULT); + uiWidgetLabelSetText(&tab->label, label); + tab->active = false; } bool_t uiTabIsActive(const uitab_t *tab) { @@ -39,7 +39,7 @@ errorret_t uiTabDraw( assertNotNull(tab, "Tab cannot be NULL"); int32_t labelW, labelH; - textMeasure(tab->label, &FONT_DEFAULT, &labelW, &labelH); + uiWidgetLabelGetSize(&tab->label, &labelW, &labelH); spritebatchsprite_t sprite = { .min = { x, y, 0.0f }, @@ -55,7 +55,7 @@ errorret_t uiTabDraw( }; errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT)); + errorChain(uiWidgetLabelDraw(&tab->label, x, y)); errorOk(); } diff --git a/src/dusk/ui/widget/uitab.h b/src/dusk/ui/widget/uitab.h index 678f54b6..2c5e3944 100644 --- a/src/dusk/ui/widget/uitab.h +++ b/src/dusk/ui/widget/uitab.h @@ -7,9 +7,10 @@ #pragma once #include "error/error.h" +#include "ui/widget/uiwidgetlabel.h" typedef struct { - const char_t *label; + uiwidgetlabel_t label; bool_t active; } uitab_t; diff --git a/src/dusk/ui/widget/uiwidgetlabel.c b/src/dusk/ui/widget/uiwidgetlabel.c new file mode 100644 index 00000000..049d7579 --- /dev/null +++ b/src/dusk/ui/widget/uiwidgetlabel.c @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiwidgetlabel.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/text/text.h" + +void uiWidgetLabelInit(uiwidgetlabel_t *label, font_t *font) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(font, "Font cannot be NULL"); + + memoryZero(label, sizeof(uiwidgetlabel_t)); + label->font = font; + label->color = COLOR_WHITE; +} + +void uiWidgetLabelSetText(uiwidgetlabel_t *label, const char_t *text) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(text, "Text cannot be NULL"); + assertStrLenMax(text, UI_WIDGET_LABEL_TEXT_MAX, "Label text too long"); + + stringCopy(label->text, text, UI_WIDGET_LABEL_TEXT_MAX); + label->spriteCount = textBuildSpriteCache( + label->text, label->font, label->sprites, + UI_WIDGET_LABEL_SPRITE_COUNT_MAX, &label->width, &label->height + ); +} + +void uiWidgetLabelSetColor(uiwidgetlabel_t *label, const color_t color) { + assertNotNull(label, "Label cannot be NULL"); + label->color = color; +} + +void uiWidgetLabelGetSize( + const uiwidgetlabel_t *label, + int32_t *outWidth, + int32_t *outHeight +) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(outWidth, "Output width cannot be NULL"); + assertNotNull(outHeight, "Output height cannot be NULL"); + *outWidth = label->width; + *outHeight = label->height; +} + +errorret_t uiWidgetLabelDraw( + const uiwidgetlabel_t *label, + const float_t x, + const float_t y +) { + assertNotNull(label, "Label cannot be NULL"); + if(label->spriteCount == 0) errorOk(); + + spritebatchsprite_t scratch[UI_WIDGET_LABEL_SPRITE_COUNT_MAX]; + errorChain(textDrawSpriteCache( + label->sprites, label->spriteCount, scratch, x, y, label->color, + label->font->texture + )); + errorOk(); +} diff --git a/src/dusk/ui/widget/uiwidgetlabel.h b/src/dusk/ui/widget/uiwidgetlabel.h new file mode 100644 index 00000000..da00a224 --- /dev/null +++ b/src/dusk/ui/widget/uiwidgetlabel.h @@ -0,0 +1,90 @@ +/** + * 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/text/font.h" +#include "display/spritebatch/spritebatchsprite.h" +#include "display/color.h" + +// A cached, cheap-to-redraw label sized for embedding directly inside +// small widgets (buttons, checkboxes, sliders, ...) rather than +// referencing text by pointer and re-deriving glyph geometry every +// frame. Capacity is intentionally much smaller than uilabel_t's -- it +// gets embedded by value in every widget instance (and inside +// uimenuitem_t's union, which is often arrayed), so keeping it small +// matters on memory-constrained targets. See uilabel_t for the +// general-purpose, larger-capacity equivalent. +#define UI_WIDGET_LABEL_TEXT_MAX 64 +#define UI_WIDGET_LABEL_SPRITE_COUNT_MAX UI_WIDGET_LABEL_TEXT_MAX + +typedef struct { + char_t text[UI_WIDGET_LABEL_TEXT_MAX]; + color_t color; + font_t *font; + + spritebatchsprite_t sprites[UI_WIDGET_LABEL_SPRITE_COUNT_MAX]; + uint32_t spriteCount; + int32_t width; + int32_t height; +} uiwidgetlabel_t; + +/** + * Initializes a widget label, defaulting to white text and no text set. + * + * @param label The label to initialize. + * @param font Font to use for rendering. Must outlive the label. + */ +void uiWidgetLabelInit(uiwidgetlabel_t *label, font_t *font); + +/** + * Sets the label's text, rebuilding its cached sprites immediately. + * Call this whenever the displayed text changes -- uiWidgetLabelDraw + * never recomputes it. + * + * @param label The label to update. + * @param text Null-terminated string to display. Must be shorter than + * UI_WIDGET_LABEL_TEXT_MAX. + */ +void uiWidgetLabelSetText(uiwidgetlabel_t *label, const char_t *text); + +/** + * Sets the label's tint color. Cheap -- doesn't touch the sprite cache. + * + * @param label The label to update. + * @param color The new tint color. + */ +void uiWidgetLabelSetColor(uiwidgetlabel_t *label, const color_t color); + +/** + * Gets the measured size (in pixels) of the label's current text, cached + * from the last uiWidgetLabelSetText call. + * + * @param label The label to query. + * @param outWidth Pointer to store the width. + * @param outHeight Pointer to store the height. + */ +void uiWidgetLabelGetSize( + const uiwidgetlabel_t *label, + int32_t *outWidth, + int32_t *outHeight +); + +/** + * Draws the label's cached sprites at the given screen position in a + * single batched buffer call. Does not recompute glyph layout. + * + * @param label The label to draw. + * @param x Screen x position. + * @param y Screen y position. + * @return Any error that occurs. + */ +errorret_t uiWidgetLabelDraw( + const uiwidgetlabel_t *label, + const float_t x, + const float_t y +); diff --git a/src/duskrpg/CMakeLists.txt b/src/duskrpg/CMakeLists.txt index 9c6131c0..b74ba477 100644 --- a/src/duskrpg/CMakeLists.txt +++ b/src/duskrpg/CMakeLists.txt @@ -14,3 +14,4 @@ add_subdirectory(game) add_subdirectory(input) add_subdirectory(item) add_subdirectory(scene) +add_subdirectory(ui) diff --git a/src/duskrpg/ui/CMakeLists.txt b/src/duskrpg/ui/CMakeLists.txt new file mode 100644 index 00000000..df6679d7 --- /dev/null +++ b/src/duskrpg/ui/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + uitestlabel.c +) diff --git a/src/duskrpg/ui/uielementgame.h b/src/duskrpg/ui/uielementgame.h new file mode 100644 index 00000000..9ec517e7 --- /dev/null +++ b/src/duskrpg/ui/uielementgame.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +// Game-specific UI elements, appended after the engine's inbuilt elements +// in ui/uielementlist.h. +// +// #include any headers this file's rows need at the top -- they're safe +// to include here even though this file also gets spliced inside +// UI_ELEMENTS[]'s braces, since uielement.c runs a priming pass over this +// same content at file scope first; each such header's own #pragma once +// makes the second (real) inclusion a no-op. +// +// X(init, update, draw, dispose, order) -- pass NULL for any callback +// the element doesn't need. order controls update/draw sequence relative +// to the engine's built-in elements -- see UI_ELEMENT_ORDER_* in +// src/dusk/ui/uielement.h, or use any int32_t value of your own. + +#include "ui/uitestlabel.h" + +X(uiTestLabelInit, NULL, uiTestLabelDraw, NULL, UI_ELEMENT_ORDER_DEBUG) diff --git a/src/duskrpg/ui/uitestlabel.c b/src/duskrpg/ui/uitestlabel.c new file mode 100644 index 00000000..c840995e --- /dev/null +++ b/src/duskrpg/ui/uitestlabel.c @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uitestlabel.h" +#include "ui/widget/uilabel.h" +#include "display/text/text.h" +#include "display/screen/screen.h" +#include "engine/engine.h" + +uilabel_t UI_TEST_LABEL; + +errorret_t uiTestLabelInit() { + uiLabelInit(&UI_TEST_LABEL, &FONT_DEFAULT); + uiLabelSetText(&UI_TEST_LABEL, "duskrpg"); + errorOk(); +} + +errorret_t uiTestLabelDraw() { + int32_t versionWidth, versionHeight; + textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight); + + errorChain(uiLabelDraw( + &UI_TEST_LABEL, + (float_t)(SCREEN.scanX + SCREEN.scanWidth - UI_TEST_LABEL.width), + (float_t)( + SCREEN.scanY + SCREEN.scanHeight - versionHeight - + UI_TEST_LABEL.height + ) + )); + errorOk(); +} diff --git a/src/duskrpg/ui/uitestlabel.h b/src/duskrpg/ui/uitestlabel.h new file mode 100644 index 00000000..07af4a4a --- /dev/null +++ b/src/duskrpg/ui/uitestlabel.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Initializes the test label, a smoke test for the duskrpg-side UI + * element extension point (uielementgame.h). + * + * @return Any error that occurs. + */ +errorret_t uiTestLabelInit(); + +/** + * Draws the test label above the engine's debug version number. + * + * @return Any error that occurs. + */ +errorret_t uiTestLabelDraw();