Rebuild UI as a scriptable element tree, archive the old system
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>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
uitextbox.c
|
||||
uitextboxmain.c
|
||||
)
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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 "ui/frame/uiframe.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
||||
|
||||
// Fixed capacity for a page's cached glyph sprites (see uitextboxglyph_t).
|
||||
// Sized generously above UI_TEXTBOX_LINES_PER_PAGE_MAX worth of glyphs at
|
||||
// typical textbox widths; uiTextboxBuildPageGlyphs asserts if a page
|
||||
// somehow produces more than this.
|
||||
#define UI_TEXTBOX_PAGE_GLYPHS_MAX 512
|
||||
|
||||
typedef struct {
|
||||
int32_t start;
|
||||
int32_t count;
|
||||
} uitextboxline_t;
|
||||
|
||||
// A single visible glyph's cached sprite (relative to the textbox's
|
||||
// content origin, i.e. (0,0)) plus the scroll value at/after which it
|
||||
// becomes visible -- lets uiTextboxDraw turn the typewriter scroll into
|
||||
// a simple prefix-count instead of recomputing glyph geometry every
|
||||
// frame.
|
||||
typedef struct {
|
||||
spritebatchsprite_t sprite;
|
||||
int32_t revealAt;
|
||||
} uitextboxglyph_t;
|
||||
|
||||
typedef struct {
|
||||
char_t *text;
|
||||
uint32_t maxLength;
|
||||
|
||||
uitextboxline_t *lines;
|
||||
uint32_t linesMax;
|
||||
int32_t lineCount;
|
||||
int32_t charsPerLine;
|
||||
int32_t linesPerPage;
|
||||
int32_t pageCount;
|
||||
|
||||
// last dimensions used for layout; rebuild triggers when these change
|
||||
float_t layoutWidth;
|
||||
float_t layoutHeight;
|
||||
|
||||
int32_t currentPage;
|
||||
int32_t scroll;
|
||||
|
||||
// Cached glyph sprites for the current page (see uitextboxglyph_t),
|
||||
// rebuilt only when currentPage no longer matches
|
||||
// glyphsBuiltForPage -- not every frame/scroll tick.
|
||||
uitextboxglyph_t glyphs[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||
int32_t glyphCount;
|
||||
int32_t glyphsBuiltForPage;
|
||||
|
||||
uiframecache_t frameCache;
|
||||
} uitextbox_t;
|
||||
|
||||
/**
|
||||
* Initializes a textbox, zeroing all state and binding it to caller-owned
|
||||
* text and line storage.
|
||||
*
|
||||
* @param box The textbox to initialize.
|
||||
* @param text Caller-owned buffer the textbox copies its text into.
|
||||
* @param maxLength Capacity of text, in characters.
|
||||
* @param lines Caller-owned buffer the textbox lays lines out into.
|
||||
* @param linesMax Capacity of lines, in entries.
|
||||
*/
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Copies text into the textbox and marks layout as dirty.
|
||||
* Resets currentPage and scroll to 0.
|
||||
*
|
||||
* @param box The textbox to update.
|
||||
* @param text Null-terminated source string.
|
||||
*/
|
||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text);
|
||||
|
||||
/**
|
||||
* Rebuilds word-wrap and page layout for the given draw dimensions.
|
||||
* Called automatically by uiTextboxDraw when width or height changes.
|
||||
*
|
||||
* @param box The textbox to rebuild.
|
||||
* @param width Available content width in pixels.
|
||||
* @param height Available content height in pixels.
|
||||
*/
|
||||
void uiTextboxBuildLayout(
|
||||
uitextbox_t *box,
|
||||
const float_t width,
|
||||
const float_t height
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances the typewriter scroll by UI_TEXTBOX_SCROLL_CHARS_PER_TICK.
|
||||
* Skipped on dynamic ticks.
|
||||
*
|
||||
* @param box The textbox to update.
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxUpdate(uitextbox_t *box);
|
||||
|
||||
/**
|
||||
* Draws the textbox frame and visible text. Rebuilds layout automatically
|
||||
* if width or height differs from the last draw call.
|
||||
*
|
||||
* @param box The textbox to draw.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @param width Draw width in pixels.
|
||||
* @param height Draw height in pixels.
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxDraw(
|
||||
uitextbox_t *box,
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width,
|
||||
const float_t height
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the total visible char count for the current page.
|
||||
*
|
||||
* @param box The textbox to query.
|
||||
* @returns Total chars on the current page.
|
||||
*/
|
||||
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box);
|
||||
|
||||
/**
|
||||
* Returns true when scroll has fully revealed the current page.
|
||||
*
|
||||
* @param box The textbox to query.
|
||||
* @returns True if the current page is fully visible.
|
||||
*/
|
||||
bool_t uiTextboxPageIsComplete(const uitextbox_t *box);
|
||||
|
||||
/**
|
||||
* Returns true when there is at least one more page after the current one.
|
||||
*
|
||||
* @param box The textbox to query.
|
||||
* @returns True if a next page exists.
|
||||
*/
|
||||
bool_t uiTextboxHasNextPage(const uitextbox_t *box);
|
||||
|
||||
/**
|
||||
* Advances to the next page and resets scroll to 0.
|
||||
* Has no effect if already on the last page.
|
||||
*
|
||||
* @param box The textbox to advance.
|
||||
*/
|
||||
void uiTextboxNextPage(uitextbox_t *box);
|
||||
|
||||
/**
|
||||
* Rebuilds the cached glyph sprites (see uitextboxglyph_t) for the
|
||||
* current page from its line layout. Called automatically by
|
||||
* uiTextboxDraw whenever currentPage no longer matches the page the
|
||||
* cache was last built for.
|
||||
*
|
||||
* @param box The textbox to rebuild.
|
||||
*/
|
||||
void uiTextboxBuildPageGlyphs(uitextbox_t *box);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "display/screen/screen.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"
|
||||
|
||||
uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
static uifocusitem_t *focusItem = NULL;
|
||||
|
||||
errorret_t uiTextboxMainInit(void) {
|
||||
uiTextboxInit(
|
||||
&UI_TEXTBOX_MAIN.box,
|
||||
UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX,
|
||||
UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTextboxMainSetText(const char_t *text) {
|
||||
uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text);
|
||||
if(focusItem != NULL) return;
|
||||
focusItem = uiFocusPush(
|
||||
1, 1,
|
||||
uiTextboxMainFocusSelected,
|
||||
NULL,
|
||||
uiTextboxMainFocusClosed,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainUpdate(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainDraw(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
|
||||
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
float_t w = (float_t)SCREEN.scanWidth;
|
||||
float_t x = (float_t)SCREEN.scanX;
|
||||
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
|
||||
errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h));
|
||||
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk();
|
||||
|
||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
float_t contentX = x + (float_t)UI_FRAME_START_X;
|
||||
float_t contentY = y + (float_t)UI_FRAME_START_Y;
|
||||
float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X;
|
||||
float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y;
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = COLOR_WHITE,
|
||||
.texture = FONT_DEFAULT.texture
|
||||
}
|
||||
};
|
||||
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainPageIsComplete(void) {
|
||||
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainHasNextPage(void) {
|
||||
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
void uiTextboxMainNextPage(void) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainIsActive(void) {
|
||||
return focusItem != NULL;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) {
|
||||
UI_TEXTBOX_MAIN.box.scroll =
|
||||
uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
uiFocusPopItem(focusItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
|
||||
focusItem = NULL;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "uitextbox.h"
|
||||
#include "ui/focus/uifocusitem.h"
|
||||
|
||||
#define UI_TEXTBOX_MAIN_LINES 4
|
||||
#define UI_TEXTBOX_MAIN_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_MAIN_LINES_MAX 64
|
||||
|
||||
typedef struct {
|
||||
uitextbox_t box;
|
||||
char_t text[UI_TEXTBOX_MAIN_TEXT_MAX];
|
||||
uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX];
|
||||
} uitextboxmain_t;
|
||||
|
||||
extern uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
|
||||
/**
|
||||
* Initializes UI_TEXTBOX_MAIN.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMainInit(void);
|
||||
|
||||
/**
|
||||
* Copies text into UI_TEXTBOX_MAIN and resets page and scroll.
|
||||
*
|
||||
* @param text Null-terminated source string.
|
||||
*/
|
||||
void uiTextboxMainSetText(const char_t *text);
|
||||
|
||||
/**
|
||||
* Advances the typewriter scroll for UI_TEXTBOX_MAIN.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMainUpdate(void);
|
||||
|
||||
/**
|
||||
* Draws UI_TEXTBOX_MAIN full-width at the bottom of the screen.
|
||||
* Position and size are derived from SCREEN each call.
|
||||
*
|
||||
* @returns Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTextboxMainDraw(void);
|
||||
|
||||
/**
|
||||
* Returns true when the current page is fully scrolled in.
|
||||
*
|
||||
* @returns True if the current page is complete.
|
||||
*/
|
||||
bool_t uiTextboxMainPageIsComplete(void);
|
||||
|
||||
/**
|
||||
* Returns true when at least one more page follows the current one.
|
||||
*
|
||||
* @returns True if a next page exists.
|
||||
*/
|
||||
bool_t uiTextboxMainHasNextPage(void);
|
||||
|
||||
/**
|
||||
* Advances UI_TEXTBOX_MAIN to the next page and resets scroll.
|
||||
* Has no effect if already on the last page.
|
||||
*/
|
||||
void uiTextboxMainNextPage(void);
|
||||
|
||||
/**
|
||||
* Returns true when UI_TEXTBOX_MAIN has focus (is visible and active).
|
||||
*
|
||||
* @returns True if the textbox is currently active.
|
||||
*/
|
||||
bool_t uiTextboxMainIsActive(void);
|
||||
|
||||
/**
|
||||
* Internal focus callback - skip scroll or advance page or dismiss.
|
||||
*
|
||||
* @param item The active focus item.
|
||||
* @returns True.
|
||||
*/
|
||||
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item);
|
||||
|
||||
/**
|
||||
* Internal focus callback - clears the focus item pointer on dismiss.
|
||||
*
|
||||
* @param item The focus item being closed.
|
||||
* @returns True.
|
||||
*/
|
||||
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user