This commit is contained in:
2026-05-06 22:42:28 -05:00
parent 581dbc2b3c
commit d8fe0f6923
12 changed files with 693 additions and 3 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ CubeEntity.prototype.update = function() {
var move = Input.axis2D(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, INPUT_ACTION_UP, INPUT_ACTION_DOWN); var move = Input.axis2D(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, INPUT_ACTION_UP, INPUT_ACTION_DOWN);
this.position.position.x += move.x * speed * TIME.delta; this.position.position.x += move.x * speed * TIME.delta;
this.position.position.z += move.y * speed * TIME.delta; this.position.position.z += move.y * speed * TIME.delta;
this.material.setColor(Color.rainbow()); this.material.color = Color.rainbow();
}; };
module = CubeEntity; module = CubeEntity;
+8
View File
@@ -21,6 +21,14 @@ function CubeScene() {
Cutscene.play(new MoveCubeCutscene({ cube: this.cube })).then(function() { Cutscene.play(new MoveCubeCutscene({ cube: this.cube })).then(function() {
scene.inputEnabled = true; scene.inputEnabled = true;
}); });
Textbox.setText(
"Hello! This is a visual novel textbox. It automatically " +
"wraps long lines and splits into pages when the content " +
"is too tall to fit. Press advance to continue...\t" +
"This is a second paragraph on a new page."
);
} }
CubeScene.prototype = Object.create(Scene.prototype); CubeScene.prototype = Object.create(Scene.prototype);
+5
View File
@@ -15,6 +15,7 @@
#include "cutscene/cutscene.h" #include "cutscene/cutscene.h"
#include "asset/asset.h" #include "asset/asset.h"
#include "ui/ui.h" #include "ui/ui.h"
#include "ui/uitextbox.h"
#include "script/scriptmanager.h" #include "script/scriptmanager.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "entity/entitymanager.h" #include "entity/entitymanager.h"
@@ -53,6 +54,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(scriptManagerInit()); errorChain(scriptManagerInit());
errorChain(displayInit()); errorChain(displayInit());
errorChain(uiInit()); errorChain(uiInit());
errorChain(uiTextboxInit());
errorChain(cutsceneInit()); errorChain(cutsceneInit());
errorChain(sceneInit()); errorChain(sceneInit());
entityManagerInit(); entityManagerInit();
@@ -74,6 +77,7 @@ errorret_t engineUpdate(void) {
inputUpdate(); inputUpdate();
consoleUpdate(); consoleUpdate();
uiUpdate(); uiUpdate();
errorChain(uiTextboxUpdate());
physicsManagerUpdate(); physicsManagerUpdate();
errorChain(displayUpdate()); errorChain(displayUpdate());
@@ -92,6 +96,7 @@ void engineExit(void) {
} }
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
uiTextboxDispose();
cutsceneDispose(); cutsceneDispose();
sceneDispose(); sceneDispose();
errorChain(networkDispose()); errorChain(networkDispose());
+1
View File
@@ -36,3 +36,4 @@ errorret_t engineUpdate(void);
* Shuts down the engine. * Shuts down the engine.
*/ */
errorret_t engineDispose(void); errorret_t engineDispose(void);
+2
View File
@@ -25,6 +25,7 @@
#include "script/module/engine/moduleengine.h" #include "script/module/engine/moduleengine.h"
#include "script/module/item/moduleitem.h" #include "script/module/item/moduleitem.h"
#include "script/module/story/modulestory.h" #include "script/module/story/modulestory.h"
#include "script/module/ui/moduletextbox.h"
static void moduleRegister(void) { static void moduleRegister(void) {
moduleInclude(); moduleInclude();
@@ -46,4 +47,5 @@ static void moduleRegister(void) {
moduleEngine(); moduleEngine();
moduleItem(); moduleItem();
moduleStory(); moduleStory();
moduleTextbox();
} }
+122
View File
@@ -0,0 +1,122 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "ui/uitextbox.h"
static scriptproto_t MODULE_TEXTBOX_PROTO;
moduleBaseFunction(moduleTextboxSetText) {
if(argc < 1 || !jerry_value_is_string(args[0])) {
return moduleBaseThrow("Textbox.setText: expected string");
}
char_t buf[UI_TEXTBOX_TEXT_MAX];
moduleBaseToString(args[0], buf, sizeof(buf));
uiTextboxSetText(buf);
return jerry_undefined();
}
moduleBaseFunction(moduleTextboxNextPage) {
uiTextboxNextPage();
return jerry_undefined();
}
moduleBaseFunction(moduleTextboxUpdate) {
uiTextboxUpdate();
return jerry_undefined();
}
moduleBaseFunction(moduleTextboxDraw) {
uiTextboxDraw();
return jerry_undefined();
}
moduleBaseFunction(moduleTextboxGetScroll) {
return jerry_number((double)UI_TEXTBOX.scroll);
}
moduleBaseFunction(moduleTextboxSetScroll) {
if(argc < 1 || !jerry_value_is_number(args[0])) {
return moduleBaseThrow("Textbox.scroll: expected number");
}
UI_TEXTBOX.scroll = (int32_t)jerry_value_as_number(args[0]);
return jerry_undefined();
}
moduleBaseFunction(moduleTextboxGetAdvanceAction) {
return jerry_number((double)UI_TEXTBOX.advanceAction);
}
moduleBaseFunction(moduleTextboxSetAdvanceAction) {
if(argc < 1 || !jerry_value_is_number(args[0])) {
return moduleBaseThrow("Textbox.advanceAction: expected number");
}
UI_TEXTBOX.advanceAction = (inputaction_t)(
(int32_t)jerry_value_as_number(args[0])
);
return jerry_undefined();
}
moduleBaseFunction(moduleTextboxGetPageComplete) {
return jerry_boolean(uiTextboxPageIsComplete());
}
moduleBaseFunction(moduleTextboxGetHasNextPage) {
return jerry_boolean(uiTextboxHasNextPage());
}
moduleBaseFunction(moduleTextboxGetCurrentPage) {
return jerry_number((double)UI_TEXTBOX.currentPage);
}
moduleBaseFunction(moduleTextboxGetPageCount) {
return jerry_number((double)UI_TEXTBOX.pageCount);
}
static void moduleTextbox(void) {
scriptProtoInit(
&MODULE_TEXTBOX_PROTO, "Textbox", sizeof(uint8_t), NULL
);
scriptProtoDefineStaticFunc(
&MODULE_TEXTBOX_PROTO, "setText", moduleTextboxSetText
);
scriptProtoDefineStaticFunc(
&MODULE_TEXTBOX_PROTO, "nextPage", moduleTextboxNextPage
);
scriptProtoDefineStaticFunc(
&MODULE_TEXTBOX_PROTO, "update", moduleTextboxUpdate
);
scriptProtoDefineStaticFunc(
&MODULE_TEXTBOX_PROTO, "draw", moduleTextboxDraw
);
scriptProtoDefineStaticProp(
&MODULE_TEXTBOX_PROTO, "scroll",
moduleTextboxGetScroll, moduleTextboxSetScroll
);
scriptProtoDefineStaticProp(
&MODULE_TEXTBOX_PROTO, "advanceAction",
moduleTextboxGetAdvanceAction, moduleTextboxSetAdvanceAction
);
scriptProtoDefineStaticProp(
&MODULE_TEXTBOX_PROTO, "pageComplete",
moduleTextboxGetPageComplete, NULL
);
scriptProtoDefineStaticProp(
&MODULE_TEXTBOX_PROTO, "hasNextPage",
moduleTextboxGetHasNextPage, NULL
);
scriptProtoDefineStaticProp(
&MODULE_TEXTBOX_PROTO, "currentPage",
moduleTextboxGetCurrentPage, NULL
);
scriptProtoDefineStaticProp(
&MODULE_TEXTBOX_PROTO, "pageCount",
moduleTextboxGetPageCount, NULL
);
}
+2
View File
@@ -9,4 +9,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
ui.c ui.c
uifps.c uifps.c
uielement.c uielement.c
uiframe.c
uitextbox.c
) )
+3
View File
@@ -10,10 +10,13 @@
#include "script/scriptmanager.h" #include "script/scriptmanager.h"
#include "console/console.h" #include "console/console.h"
#include "ui/uifps.h" #include "ui/uifps.h"
#include "engine/engine.h"
#include "ui/uitextbox.h"
uielement_t UI_ELEMENTS[] = { uielement_t UI_ELEMENTS[] = {
{ .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = consoleDraw } }, { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = consoleDraw } },
{ .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiFPSDraw } }, { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiFPSDraw } },
{ .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = uiTextboxDraw } },
{ .type = UI_ELEMENT_TYPE_SCRIPT, .script = { .script = "ui/test.js" } }, { .type = UI_ELEMENT_TYPE_SCRIPT, .script = { .script = "ui/test.js" } },
{ .type = UI_ELEMENT_TYPE_NULL }, { .type = UI_ELEMENT_TYPE_NULL },
+125
View File
@@ -0,0 +1,125 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h"
#include "display/color.h"
errorret_t uiFrameInit(uiframe_t *frame) {
assertNotNull(frame, "frame must not be NULL");
memoryZero(frame, sizeof(uiframe_t));
errorOk();
}
errorret_t uiFrameDraw(
const uiframe_t *frame,
const float_t x,
const float_t y,
const float_t width,
const float_t height
) {
assertNotNull(frame, "frame must not be NULL");
assertNotNull(frame->texture, "frame texture must not be NULL");
errorChain(shaderSetTexture(
&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, frame->texture
));
float_t tileW = (float_t)frame->tileset.tileWidth;
float_t tileH = (float_t)frame->tileset.tileHeight;
vec4 uv;
tilesetPositionGetUV(&frame->tileset, 0, 0, uv);
errorChain(spriteBatchPush(
x, y, x + tileW, y + tileH,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 1, 0, uv);
errorChain(spriteBatchPush(
x + tileW, y, x + width - tileW, y + tileH,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 2, 0, uv);
errorChain(spriteBatchPush(
x + width - tileW, y, x + width, y + tileH,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 0, 1, uv);
errorChain(spriteBatchPush(
x, y + tileH, x + tileW, y + height - tileH,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 1, 1, uv);
errorChain(spriteBatchPush(
x + tileW, y + tileH, x + width - tileW, y + height - tileH,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 2, 1, uv);
errorChain(spriteBatchPush(
x + width - tileW, y + tileH, x + width, y + height - tileH,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 0, 2, uv);
errorChain(spriteBatchPush(
x, y + height - tileH, x + tileW, y + height,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 1, 2, uv);
errorChain(spriteBatchPush(
x + tileW, y + height - tileH, x + width - tileW, y + height,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
tilesetPositionGetUV(&frame->tileset, 2, 2, uv);
errorChain(spriteBatchPush(
x + width - tileW, y + height - tileH, x + width, y + height,
#if MESH_ENABLE_COLOR
COLOR_WHITE,
#endif
uv[0], uv[1], uv[2], uv[3]
));
errorOk();
}
void uiFrameDispose(uiframe_t *frame) {
assertNotNull(frame, "frame must not be NULL");
frame->texture = NULL;
}
+50
View File
@@ -0,0 +1,50 @@
/**
* 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/texture/texture.h"
#include "display/texture/tileset.h"
typedef struct {
tileset_t tileset;
texture_t *texture;
} uiframe_t;
/**
* Initializes a UI frame element.
*
* @param frame The frame to initialize.
* @return Any error that occurs.
*/
errorret_t uiFrameInit(uiframe_t *frame);
/**
* Draws a UI frame using 9-slice rendering from a 3x3 tileset.
* Pushes quads to the sprite batch without flushing.
*
* @param frame The frame to draw.
* @param x Screen x position.
* @param y Screen y position.
* @param width Total width of the frame.
* @param height Total height of the frame.
* @return Any error that occurs.
*/
errorret_t uiFrameDraw(
const uiframe_t *frame,
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
/**
* Disposes of a UI frame element. Does not dispose the texture.
*
* @param frame The frame to dispose.
*/
void uiFrameDispose(uiframe_t *frame);
+260
View File
@@ -0,0 +1,260 @@
// 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 "input/input.h"
#include "display/screen/screen.h"
#include "display/texture/texture.h"
#include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h"
uitextbox_t UI_TEXTBOX;
errorret_t uiTextboxInit(void) {
memoryZero(&UI_TEXTBOX, sizeof(uitextbox_t));
UI_TEXTBOX.textColor = COLOR_WHITE;
UI_TEXTBOX.advanceAction = INPUT_ACTION_ACCEPT;
float_t fontW = (float_t)FONT_TILESET_DEFAULT.tileWidth;
float_t fontH = (float_t)FONT_TILESET_DEFAULT.tileHeight;
float_t tbHeight = (
(float_t)UI_TEXTBOX_LINES_PER_PAGE_MAX * fontH +
(float_t)(UI_TEXTBOX_LINES_PER_PAGE_MAX - 1) * UI_TEXTBOX_LINE_SPACING +
2.0f * fontH
);
UI_TEXTBOX.x = 10.0f;
UI_TEXTBOX.y = (float_t)SCREEN.height - tbHeight - 10.0f;
UI_TEXTBOX.width = (float_t)SCREEN.width - 20.0f;
UI_TEXTBOX.height = tbHeight;
UI_TEXTBOX.frame.tileset.columns = 3;
UI_TEXTBOX.frame.tileset.rows = 3;
UI_TEXTBOX.frame.tileset.tileCount = 9;
UI_TEXTBOX.frame.tileset.tileWidth = FONT_TILESET_DEFAULT.tileWidth;
UI_TEXTBOX.frame.tileset.tileHeight = FONT_TILESET_DEFAULT.tileHeight;
UI_TEXTBOX.frame.tileset.uv[0] = 1.0f / 3.0f;
UI_TEXTBOX.frame.tileset.uv[1] = 1.0f / 3.0f;
UI_TEXTBOX.frame.texture = &TEXTURE_WHITE;
UI_TEXTBOX.textTileset = FONT_TILESET_DEFAULT;
UI_TEXTBOX.textTexture = &FONT_TEXTURE_DEFAULT;
errorOk();
}
void uiTextboxBuildLayout(void) {
UI_TEXTBOX.lineCount = 0;
UI_TEXTBOX.pageCount = 1;
float_t frameTileW = (float_t)UI_TEXTBOX.frame.tileset.tileWidth;
float_t frameTileH = (float_t)UI_TEXTBOX.frame.tileset.tileHeight;
float_t fontW = (float_t)UI_TEXTBOX.textTileset.tileWidth;
float_t fontH = (float_t)UI_TEXTBOX.textTileset.tileHeight;
if(fontW <= 0.0f || fontH <= 0.0f) return;
float_t contentW = UI_TEXTBOX.width - 2.0f * frameTileW;
float_t contentH = UI_TEXTBOX.height - 2.0f * frameTileH;
(void)contentH;
UI_TEXTBOX.charsPerLine = (int32_t)(contentW / fontW);
UI_TEXTBOX.linesPerPage = UI_TEXTBOX_LINES_PER_PAGE_MAX;
if(UI_TEXTBOX.charsPerLine <= 0 || UI_TEXTBOX.linesPerPage <= 0) return;
if(UI_TEXTBOX.text[0] == '\0') return;
char_t *src = UI_TEXTBOX.text;
int32_t i = 0;
while(src[i] != '\0' && UI_TEXTBOX.lineCount < UI_TEXTBOX_LINES_MAX) {
if(src[i] == '\t') {
i++;
int32_t rem = UI_TEXTBOX.lineCount % UI_TEXTBOX.linesPerPage;
int32_t pad = rem > 0 ? UI_TEXTBOX.linesPerPage - rem : 0;
while(pad > 0 && UI_TEXTBOX.lineCount < UI_TEXTBOX_LINES_MAX) {
UI_TEXTBOX.lines[UI_TEXTBOX.lineCount].start = i;
UI_TEXTBOX.lines[UI_TEXTBOX.lineCount].count = 0;
UI_TEXTBOX.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 > UI_TEXTBOX.charsPerLine
) {
i++;
break;
}
lineWidth++;
i++;
} else {
if(lineWidth >= UI_TEXTBOX.charsPerLine) break;
lineWidth++;
i++;
}
}
UI_TEXTBOX.lines[UI_TEXTBOX.lineCount].start = lineStart;
UI_TEXTBOX.lines[UI_TEXTBOX.lineCount].count = lineWidth;
UI_TEXTBOX.lineCount++;
}
if(UI_TEXTBOX.lineCount == 0) {
UI_TEXTBOX.pageCount = 1;
} else {
int32_t div = UI_TEXTBOX.lineCount + UI_TEXTBOX.linesPerPage - 1;
UI_TEXTBOX.pageCount = div / UI_TEXTBOX.linesPerPage;
}
}
void uiTextboxSetText(const char_t *text) {
assertNotNull(text, "text must not be NULL");
stringCopy(UI_TEXTBOX.text, text, UI_TEXTBOX_TEXT_MAX);
UI_TEXTBOX.currentPage = 0;
UI_TEXTBOX.scroll = 0;
uiTextboxBuildLayout();
}
errorret_t uiTextboxUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) errorOk();
#endif
if(!uiTextboxPageIsComplete()) {
UI_TEXTBOX.scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK;
}
if(inputPressed(UI_TEXTBOX.advanceAction)) {
if(!uiTextboxPageIsComplete()) {
UI_TEXTBOX.scroll = uiTextboxGetPageCharCount();
} else if(uiTextboxHasNextPage()) {
uiTextboxNextPage();
}
}
errorOk();
}
int32_t uiTextboxGetPageCharCount(void) {
int32_t first = UI_TEXTBOX.currentPage * UI_TEXTBOX.linesPerPage;
int32_t last = first + UI_TEXTBOX.linesPerPage;
if(last > UI_TEXTBOX.lineCount) last = UI_TEXTBOX.lineCount;
int32_t total = 0;
for(int32_t i = first; i < last; i++) {
total += UI_TEXTBOX.lines[i].count;
}
return total;
}
bool_t uiTextboxPageIsComplete(void) {
return UI_TEXTBOX.scroll >= uiTextboxGetPageCharCount();
}
bool_t uiTextboxHasNextPage(void) {
return UI_TEXTBOX.currentPage + 1 < UI_TEXTBOX.pageCount;
}
void uiTextboxNextPage(void) {
if(!uiTextboxHasNextPage()) return;
UI_TEXTBOX.currentPage++;
UI_TEXTBOX.scroll = 0;
}
errorret_t uiTextboxDraw(void) {
errorChain(uiFrameDraw(
&UI_TEXTBOX.frame,
UI_TEXTBOX.x, UI_TEXTBOX.y,
UI_TEXTBOX.width, UI_TEXTBOX.height
));
errorChain(spriteBatchFlush());
if(UI_TEXTBOX.lineCount == 0 || UI_TEXTBOX.text[0] == '\0') errorOk();
errorChain(shaderSetTexture(
&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, UI_TEXTBOX.textTexture
));
#if MESH_ENABLE_COLOR
#else
errorChain(shaderSetColor(
&SHADER_UNLIT, SHADER_UNLIT_COLOR, UI_TEXTBOX.textColor
));
#endif
float_t frameTileW = (float_t)UI_TEXTBOX.frame.tileset.tileWidth;
float_t frameTileH = (float_t)UI_TEXTBOX.frame.tileset.tileHeight;
float_t fontW = (float_t)UI_TEXTBOX.textTileset.tileWidth;
float_t fontH = (float_t)UI_TEXTBOX.textTileset.tileHeight;
float_t contentX = UI_TEXTBOX.x + frameTileW;
float_t contentY = UI_TEXTBOX.y + frameTileH;
int32_t pageFirst = UI_TEXTBOX.currentPage * UI_TEXTBOX.linesPerPage;
int32_t pageLast = pageFirst + UI_TEXTBOX.linesPerPage;
if(pageLast > UI_TEXTBOX.lineCount) pageLast = UI_TEXTBOX.lineCount;
int32_t charsLeft = UI_TEXTBOX.scroll;
for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) {
uitextboxline_t *line = &UI_TEXTBOX.lines[li];
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
float_t lineY = contentY;
lineY += (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
for(int32_t ci = 0; ci < visible; ci++) {
char_t c = UI_TEXTBOX.text[line->start + ci];
if(c == ' ') continue;
errorChain(textDrawChar(
contentX + (float_t)ci * fontW,
lineY,
c,
#if MESH_ENABLE_COLOR
UI_TEXTBOX.textColor,
#endif
&UI_TEXTBOX.textTileset,
UI_TEXTBOX.textTexture
));
}
charsLeft -= visible;
}
errorChain(spriteBatchFlush());
errorOk();
}
void uiTextboxDispose(void) {
uiFrameDispose(&UI_TEXTBOX.frame);
UI_TEXTBOX.textTexture = NULL;
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "uiframe.h"
#include "display/text/text.h"
#include "input/inputaction.h"
#define UI_TEXTBOX_TEXT_MAX 1024
#define UI_TEXTBOX_LINES_MAX 64
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 3
#define UI_TEXTBOX_LINE_SPACING 0.0f
typedef struct {
int32_t start;
int32_t count;
} uitextboxline_t;
typedef struct {
uiframe_t frame;
tileset_t textTileset;
texture_t *textTexture;
color_t textColor;
float_t x, y, width, height;
char_t text[UI_TEXTBOX_TEXT_MAX];
uitextboxline_t lines[UI_TEXTBOX_LINES_MAX];
int32_t lineCount;
int32_t charsPerLine;
int32_t linesPerPage;
int32_t pageCount;
int32_t currentPage;
int32_t scroll;
inputaction_t advanceAction;
} uitextbox_t;
extern uitextbox_t UI_TEXTBOX;
/**
* Initializes UI_TEXTBOX. Sets position and size from current screen
* dimensions and wires up the default font and frame textures.
* Call after displayInit().
*
* @return Any error that occurs.
*/
errorret_t uiTextboxInit(void);
/**
* Rebuilds the word-wrap and page layout from the current text and dimensions.
* Called automatically by uiTextboxSetText.
*/
void uiTextboxBuildLayout(void);
/**
* Copies text into UI_TEXTBOX and rebuilds the word-wrap / page layout.
* Resets currentPage and scroll to 0.
*
* @param text Null-terminated source string.
*/
void uiTextboxSetText(const char_t *text);
/**
* Advances the typewriter scroll by UI_TEXTBOX_SCROLL_CHARS_PER_TICK.
* Skipped on dynamic ticks when DUSK_TIME_DYNAMIC is defined.
*
* @return Any error that occurs.
*/
errorret_t uiTextboxUpdate(void);
/**
* Draws the frame and the currently visible text, including a final flush.
*
* @return Any error that occurs.
*/
errorret_t uiTextboxDraw(void);
/**
* Returns the total char count for the current page.
*
* @return Total chars on current page.
*/
int32_t uiTextboxGetPageCharCount(void);
/**
* Returns true when scroll has fully revealed the current page.
*/
bool_t uiTextboxPageIsComplete(void);
/**
* Returns true when there is at least one more page after the current one.
*/
bool_t uiTextboxHasNextPage(void);
/**
* Advances to the next page and resets scroll to 0.
* Has no effect if already on the last page.
*/
void uiTextboxNextPage(void);
/**
* Disposes of UI_TEXTBOX, nulling out texture pointers.
*/
void uiTextboxDispose(void);