/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "uitextbox.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" #include "time/time.h" #include "display/text/text.h" #include "display/color.h" #include "display/spritebatch/spritebatch.h" #include "display/shader/shaderunlit.h" #include "ui/frame/uiframe.h" void uiTextboxInit( uitextbox_t *box, char_t *text, const uint32_t maxLength, uitextboxline_t *lines, const uint32_t linesMax ) { assertNotNull(box, "Textbox cannot be NULL"); assertNotNull(text, "Text buffer cannot be NULL"); assertTrue(maxLength >= 1, "maxLength must be at least 1"); assertNotNull(lines, "Lines buffer cannot be NULL"); assertTrue(linesMax >= 1, "linesMax must be at least 1"); memoryZero(box, sizeof(uitextbox_t)); box->text = text; box->maxLength = maxLength; box->lines = lines; box->linesMax = linesMax; box->glyphsBuiltForPage = -1; } void uiTextboxSetText(uitextbox_t *box, const char_t *text) { assertNotNull(box, "Textbox cannot be NULL"); assertNotNull(text, "Text cannot be NULL"); stringCopy(box->text, text, box->maxLength); box->currentPage = 0; box->scroll = 0; box->layoutWidth = 0.0f; box->layoutHeight = 0.0f; box->glyphsBuiltForPage = -1; } void uiTextboxBuildLayout( uitextbox_t *box, const float_t width, const float_t height ) { assertNotNull(box, "Textbox cannot be NULL"); box->glyphsBuiltForPage = -1; box->layoutWidth = width; box->layoutHeight = height; box->lineCount = 0; box->pageCount = 1; float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; if(fontW <= 0.0f || fontH <= 0.0f) return; box->charsPerLine = (int32_t)(width / fontW); box->linesPerPage = (int32_t)(height / (fontH + UI_TEXTBOX_LINE_SPACING)); if(box->linesPerPage > UI_TEXTBOX_LINES_PER_PAGE_MAX) { box->linesPerPage = UI_TEXTBOX_LINES_PER_PAGE_MAX; } if(box->charsPerLine <= 0 || box->linesPerPage <= 0) return; if(box->text[0] == '\0') return; char_t *src = box->text; int32_t i = 0; while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) { if(src[i] == '\t') { i++; int32_t rem = box->lineCount % box->linesPerPage; int32_t pad = rem > 0 ? box->linesPerPage - rem : 0; while(pad > 0 && box->lineCount < (int32_t)box->linesMax) { box->lines[box->lineCount].start = i; box->lines[box->lineCount].count = 0; box->lineCount++; pad--; } continue; } int32_t lineStart = i; int32_t lineWidth = 0; while(src[i] != '\0') { char_t c = src[i]; if(c == '\n') { i++; break; } if(c == '\t') break; if(c == ' ') { int32_t wordLen = 0; int32_t j = i + 1; while( src[j] != ' ' && src[j] != '\n' && src[j] != '\t' && src[j] != '\0' ) { wordLen++; j++; } if(lineWidth > 0 && lineWidth + 1 + wordLen > box->charsPerLine) { i++; break; } lineWidth++; i++; } else { if(lineWidth >= box->charsPerLine) break; lineWidth++; i++; } } box->lines[box->lineCount].start = lineStart; box->lines[box->lineCount].count = lineWidth; box->lineCount++; } if(box->lineCount == 0) { box->pageCount = 1; } else { box->pageCount = (box->lineCount + box->linesPerPage - 1) / box->linesPerPage; } } errorret_t uiTextboxUpdate(uitextbox_t *box) { assertNotNull(box, "Textbox cannot be NULL"); #ifdef DUSK_TIME_DYNAMIC if(TIME.dynamicUpdate) errorOk(); #endif if(!uiTextboxPageIsComplete(box)) { box->scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK; } errorOk(); } errorret_t uiTextboxDraw( uitextbox_t *box, const float_t x, const float_t y, const float_t width, const float_t height ) { assertNotNull(box, "Textbox cannot be NULL"); float_t startX = (float_t)UI_FRAME_START_X; float_t startY = (float_t)UI_FRAME_START_Y; float_t contentX = x + startX; float_t contentY = y + startY; float_t contentW = width - 2.0f * startX; float_t contentH = height - 2.0f * startY; if(contentW != box->layoutWidth || contentH != box->layoutHeight) { uiTextboxBuildLayout(box, contentW, contentH); } errorChain(uiFrameDrawCached(&box->frameCache, x, y, width, height)); if(box->lineCount == 0 || box->text[0] == '\0') errorOk(); if(box->glyphsBuiltForPage != box->currentPage) { uiTextboxBuildPageGlyphs(box); } if(box->glyphCount == 0) errorOk(); int32_t visibleCount = 0; while( visibleCount < box->glyphCount && box->glyphs[visibleCount].revealAt <= box->scroll ) visibleCount++; if(visibleCount == 0) errorOk(); spritebatchsprite_t scratch[UI_TEXTBOX_PAGE_GLYPHS_MAX]; for(int32_t i = 0; i < visibleCount; i++) { scratch[i] = spriteBatchSpriteTranslate( &box->glyphs[i].sprite, contentX, contentY ); } shadermaterial_t material = { .unlit = { .color = COLOR_WHITE, .texture = FONT_DEFAULT.texture } }; errorChain(spriteBatchBuffer(scratch, visibleCount, &SHADER_UNLIT, material)); errorOk(); } void uiTextboxBuildPageGlyphs(uitextbox_t *box) { assertNotNull(box, "Textbox cannot be NULL"); box->glyphCount = 0; int32_t pageFirst = box->currentPage * box->linesPerPage; int32_t pageLast = pageFirst + box->linesPerPage; if(pageLast > box->lineCount) pageLast = box->lineCount; float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; int32_t consumed = 0; for(int32_t li = pageFirst; li < pageLast; li++) { uitextboxline_t *line = &box->lines[li]; float_t lineY = (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING); for(int32_t ci = 0; ci < line->count; ci++) { consumed++; char_t c = box->text[line->start + ci]; if(c == ' ') continue; assertTrue( box->glyphCount < UI_TEXTBOX_PAGE_GLYPHS_MAX, "Textbox page produces too many glyphs" ); uitextboxglyph_t *glyph = &box->glyphs[box->glyphCount++]; glyph->sprite = textGetSprite( (vec2){ (float_t)ci * fontW, lineY }, c, &FONT_DEFAULT ); glyph->revealAt = consumed; } } box->glyphsBuiltForPage = box->currentPage; } int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) { assertNotNull(box, "Textbox cannot be NULL"); int32_t first = box->currentPage * box->linesPerPage; int32_t last = first + box->linesPerPage; if(last > box->lineCount) last = box->lineCount; int32_t total = 0; for(int32_t i = first; i < last; i++) { total += box->lines[i].count; } return total; } bool_t uiTextboxPageIsComplete(const uitextbox_t *box) { assertNotNull(box, "Textbox cannot be NULL"); return box->scroll >= uiTextboxGetPageCharCount(box); } bool_t uiTextboxHasNextPage(const uitextbox_t *box) { assertNotNull(box, "Textbox cannot be NULL"); return box->currentPage + 1 < box->pageCount; } void uiTextboxNextPage(uitextbox_t *box) { assertNotNull(box, "Textbox cannot be NULL"); if(!uiTextboxHasNextPage(box)) return; box->currentPage++; box->scroll = 0; }