7ee04c78cd
The old UI system (X-macro static element list, hand-authored C screens/widgets/focus stack) is archived under archive/ rather than deleted, since it's a useful reference during the rewrite. New native UI element pool (src/dusk/ui): a flat UI_ELEMENTS[128] pool of tagged-union elements (Label, Rectangle, Scripted), a real persistent parent/child tree (children[8] per element, cycle- and capacity-checked uiElementSetParent), always-fresh worldX/worldY (cheap enough to recompute on every read, no dirty-flag cache needed), and cascading dispose. Rendering stays manual/immediate: a scripted element with no render() override auto-renders its children by default, but overriding render() takes full control (an override must call renderChildren() itself to opt back in) -- this is deliberately preserved end to end via a render()-shadow trampoline so overriding render() always keeps working the same way regardless of how a node is reached. New scripting layer (src/dusk/script/module/ui): UIElement/Label/ Rectangle JS classes (Label/Rectangle share UIElement's prototype via manual chaining, not JS `extends`), exposing x/y/worldX/worldY/parent/ add()/remove()/render()/renderChildren()/dispose(). UI.add()/ UI.remove() manage top-level render roots, mutually exclusive with being someone's child. Also: Scene gains a lateUpdate() hook (called once per frame after every other update, for things like camera-follow that need to react to where everything else ended up); several duskrpg call sites (cutscene items, entityinteractable, entityplayer) that depended on the now-archived RPG textbox are stubbed to console output instead of a dialogue box, pending the new UI reaching that far. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
277 lines
7.2 KiB
C
277 lines
7.2 KiB
C
/**
|
|
* 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;
|
|
}
|