From d8fe0f692354e47be60a91ee3ba91e3cc73935f3 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Wed, 6 May 2026 22:42:28 -0500 Subject: [PATCH] textbox --- assets/entities/CubeEntity.js | 2 +- assets/scenes/cube.js | 8 + src/dusk/engine/engine.c | 7 +- src/dusk/engine/engine.h | 3 +- src/dusk/script/module/module.h | 2 + src/dusk/script/module/ui/moduletextbox.h | 122 ++++++++++ src/dusk/ui/CMakeLists.txt | 2 + src/dusk/ui/uielement.c | 3 + src/dusk/ui/uiframe.c | 125 +++++++++++ src/dusk/ui/uiframe.h | 50 +++++ src/dusk/ui/uitextbox.c | 260 ++++++++++++++++++++++ src/dusk/ui/uitextbox.h | 112 ++++++++++ 12 files changed, 693 insertions(+), 3 deletions(-) create mode 100644 src/dusk/script/module/ui/moduletextbox.h create mode 100644 src/dusk/ui/uiframe.c create mode 100644 src/dusk/ui/uiframe.h create mode 100644 src/dusk/ui/uitextbox.c create mode 100644 src/dusk/ui/uitextbox.h diff --git a/assets/entities/CubeEntity.js b/assets/entities/CubeEntity.js index d9aa5ca1..a86f994f 100644 --- a/assets/entities/CubeEntity.js +++ b/assets/entities/CubeEntity.js @@ -19,7 +19,7 @@ CubeEntity.prototype.update = function() { 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.z += move.y * speed * TIME.delta; - this.material.setColor(Color.rainbow()); + this.material.color = Color.rainbow(); }; module = CubeEntity; diff --git a/assets/scenes/cube.js b/assets/scenes/cube.js index ff3a02ea..54c216c4 100644 --- a/assets/scenes/cube.js +++ b/assets/scenes/cube.js @@ -21,6 +21,14 @@ function CubeScene() { Cutscene.play(new MoveCubeCutscene({ cube: this.cube })).then(function() { 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); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index c8013b11..ce528bf1 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -15,6 +15,7 @@ #include "cutscene/cutscene.h" #include "asset/asset.h" #include "ui/ui.h" +#include "ui/uitextbox.h" #include "script/scriptmanager.h" #include "assert/assert.h" #include "entity/entitymanager.h" @@ -53,6 +54,8 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(scriptManagerInit()); errorChain(displayInit()); errorChain(uiInit()); + errorChain(uiTextboxInit()); + errorChain(cutsceneInit()); errorChain(sceneInit()); entityManagerInit(); @@ -74,6 +77,7 @@ errorret_t engineUpdate(void) { inputUpdate(); consoleUpdate(); uiUpdate(); + errorChain(uiTextboxUpdate()); physicsManagerUpdate(); errorChain(displayUpdate()); @@ -92,6 +96,7 @@ void engineExit(void) { } errorret_t engineDispose(void) { + uiTextboxDispose(); cutsceneDispose(); sceneDispose(); errorChain(networkDispose()); @@ -101,6 +106,6 @@ errorret_t engineDispose(void) { consoleDispose(); errorChain(displayDispose()); errorChain(assetDispose()); - + errorOk(); } diff --git a/src/dusk/engine/engine.h b/src/dusk/engine/engine.h index 6394fddd..c519ecf3 100644 --- a/src/dusk/engine/engine.h +++ b/src/dusk/engine/engine.h @@ -35,4 +35,5 @@ errorret_t engineUpdate(void); /** * Shuts down the engine. */ -errorret_t engineDispose(void); \ No newline at end of file +errorret_t engineDispose(void); + diff --git a/src/dusk/script/module/module.h b/src/dusk/script/module/module.h index 8f6273bb..a1929593 100644 --- a/src/dusk/script/module/module.h +++ b/src/dusk/script/module/module.h @@ -25,6 +25,7 @@ #include "script/module/engine/moduleengine.h" #include "script/module/item/moduleitem.h" #include "script/module/story/modulestory.h" +#include "script/module/ui/moduletextbox.h" static void moduleRegister(void) { moduleInclude(); @@ -46,4 +47,5 @@ static void moduleRegister(void) { moduleEngine(); moduleItem(); moduleStory(); + moduleTextbox(); } diff --git a/src/dusk/script/module/ui/moduletextbox.h b/src/dusk/script/module/ui/moduletextbox.h new file mode 100644 index 00000000..697c8eaa --- /dev/null +++ b/src/dusk/script/module/ui/moduletextbox.h @@ -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 + ); +} diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index 071692b8..9ef01edc 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -9,4 +9,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} ui.c uifps.c uielement.c + uiframe.c + uitextbox.c ) \ No newline at end of file diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index a31d3ddd..34c17027 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -10,10 +10,13 @@ #include "script/scriptmanager.h" #include "console/console.h" #include "ui/uifps.h" +#include "engine/engine.h" +#include "ui/uitextbox.h" uielement_t UI_ELEMENTS[] = { { .type = UI_ELEMENT_TYPE_NATIVE, .native = { .draw = consoleDraw } }, { .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_NULL }, diff --git a/src/dusk/ui/uiframe.c b/src/dusk/ui/uiframe.c new file mode 100644 index 00000000..60c60196 --- /dev/null +++ b/src/dusk/ui/uiframe.c @@ -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; +} diff --git a/src/dusk/ui/uiframe.h b/src/dusk/ui/uiframe.h new file mode 100644 index 00000000..c6c4b3be --- /dev/null +++ b/src/dusk/ui/uiframe.h @@ -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); diff --git a/src/dusk/ui/uitextbox.c b/src/dusk/ui/uitextbox.c new file mode 100644 index 00000000..c0231ea5 --- /dev/null +++ b/src/dusk/ui/uitextbox.c @@ -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; +} diff --git a/src/dusk/ui/uitextbox.h b/src/dusk/ui/uitextbox.h new file mode 100644 index 00000000..5aa58389 --- /dev/null +++ b/src/dusk/ui/uitextbox.h @@ -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);