From 3f35d56be4b2b9072e81918341e8b7c967ea08fe Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 25 Jun 2026 20:58:44 -0500 Subject: [PATCH] UI Frame finally doing things --- src/dusk/engine/engine.c | 2 +- src/dusk/rpg/entity/entityinteract.c | 4 +- src/dusk/ui/CMakeLists.txt | 1 + src/dusk/ui/focus/uifocus.c | 36 ++-- src/dusk/ui/focus/uifocus.h | 1 + src/dusk/ui/frame/uiframe.c | 30 +-- src/dusk/ui/frame/uiframe.h | 12 +- src/dusk/ui/frame/uisettings.c | 8 +- src/dusk/ui/rpg/CMakeLists.txt | 10 + src/dusk/ui/rpg/uitextbox.c | 242 +++++++++++++++++++++++ src/dusk/ui/rpg/uitextbox.h | 127 ++++++++++++ src/dusk/ui/rpg/uitextboxmain.c | 85 ++++++++ src/dusk/ui/rpg/uitextboxmain.h | 86 +++++++++ src/dusk/ui/uielement.c | 16 +- src/dusk/ui/uitextbox.c | 278 --------------------------- src/dusk/ui/uitextbox.h | 121 ------------ 16 files changed, 608 insertions(+), 451 deletions(-) create mode 100644 src/dusk/ui/rpg/CMakeLists.txt create mode 100644 src/dusk/ui/rpg/uitextbox.c create mode 100644 src/dusk/ui/rpg/uitextbox.h create mode 100644 src/dusk/ui/rpg/uitextboxmain.c create mode 100644 src/dusk/ui/rpg/uitextboxmain.h delete mode 100644 src/dusk/ui/uitextbox.c delete mode 100644 src/dusk/ui/uitextbox.h diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 9e110efb..04c234ad 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -61,10 +61,10 @@ errorret_t engineUpdate(void) { inputUpdate(); consoleUpdate(); errorChain(rpgUpdate()); - errorChain(uiUpdate()); errorChain(cutsceneUpdate()); errorChain(sceneUpdate()); errorChain(assetUpdate()); + errorChain(uiUpdate()); // Render errorChain(displayUpdate()); diff --git a/src/dusk/rpg/entity/entityinteract.c b/src/dusk/rpg/entity/entityinteract.c index d3b3ca7f..c618ab0c 100644 --- a/src/dusk/rpg/entity/entityinteract.c +++ b/src/dusk/rpg/entity/entityinteract.c @@ -7,8 +7,8 @@ #include "entity.h" #include "assert/assert.h" -#include "console/console.h" #include "rpg/cutscene/cutscenesystem.h" +#include "ui/rpg/uitextboxmain.h" void entityInteractWith(entity_t *player, entity_t *target) { assertNotNull(player, "Player entity pointer cannot be NULL"); @@ -24,7 +24,7 @@ void entityInteractWith(entity_t *player, entity_t *target) { return; case ENTITY_INTERACT_PRINT: - consolePrint(target->interact.data.message); + uiTextboxMainSetText(target->interact.data.message); return; case ENTITY_INTERACT_NULL: diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index 0c3b196e..d6a0d06f 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(debug) add_subdirectory(frame) add_subdirectory(focus) add_subdirectory(overlay) +add_subdirectory(rpg) add_subdirectory(widget) # Sources diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c index 1b6103e8..49d86349 100644 --- a/src/dusk/ui/focus/uifocus.c +++ b/src/dusk/ui/focus/uifocus.c @@ -49,13 +49,14 @@ uifocusitem_t * uiFocusPush( item->closed = closed; item->user = user; UI_FOCUS.count++; + UI_FOCUS.pushedThisTick = true; if(item->changed != NULL) item->changed(item); return item; } void uiFocusPop(void) { assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow"); - + uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; if(item->closed != NULL) item->closed(item); UI_FOCUS.count--; @@ -120,36 +121,39 @@ void uiFocusMoveDirection( void uiFocusUpdate(void) { if(UI_FOCUS.count == 0) return; - // Current item + #ifdef DUSK_TIME_DYNAMIC + if(TIME.dynamicUpdate) return; + #endif + + if(UI_FOCUS.pushedThisTick) { + UI_FOCUS.pushedThisTick = false; + return; + } + uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; - // Accept - if(inputPressedIfDynamic(INPUT_ACTION_ACCEPT)) { + if(inputPressed(INPUT_ACTION_ACCEPT)) { if(item->selected != NULL) item->selected(item); return; } - // Return - if(inputPressedIfDynamic(INPUT_ACTION_CANCEL)) { + if(inputPressed(INPUT_ACTION_CANCEL)) { uiFocusPop(); return; } - // Handle pressed directions for( const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; m->action != INPUT_ACTION_NULL; m++ ) { - if(!inputPressedIfDynamic(m->action)) continue; - + if(!inputPressed(m->action)) continue; UI_FOCUS.direction = m->direction; UI_FOCUS.timeHeld = 0.0f; uiFocusMoveDirection(item, m->direction); return; } - // Handle held directions bool_t held = false; for( const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; @@ -157,26 +161,18 @@ void uiFocusUpdate(void) { m++ ) { if(m->direction != UI_FOCUS.direction) continue; - - held = inputDownIfDynamic(m->action); + held = inputIsDown(m->action); break; } - // Are we still holding? if(!held) { UI_FOCUS.direction = UI_FOCUS_DIRECTION_NONE; UI_FOCUS.timeHeld = 0.0f; return; } + UI_FOCUS.timeHeld += TIME.delta; - #ifdef DUSK_TIME_DYNAMIC - UI_FOCUS.timeHeld += TIME.dynamicDelta; - #else - UI_FOCUS.timeHeld += TIME.delta; - #endif - - // Can tick? if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return; if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return; UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY; diff --git a/src/dusk/ui/focus/uifocus.h b/src/dusk/ui/focus/uifocus.h index 9369e097..09379831 100644 --- a/src/dusk/ui/focus/uifocus.h +++ b/src/dusk/ui/focus/uifocus.h @@ -53,6 +53,7 @@ typedef struct { uint8_t count; uifocusdirection_t direction; float_t timeHeld; + bool_t pushedThisTick; } uifocus_t; extern uifocus_t UI_FOCUS; diff --git a/src/dusk/ui/frame/uiframe.c b/src/dusk/ui/frame/uiframe.c index 3097fcbe..0406265a 100644 --- a/src/dusk/ui/frame/uiframe.c +++ b/src/dusk/ui/frame/uiframe.c @@ -22,33 +22,39 @@ errorret_t uiFrameInit(void) { color_t border = color4b(0, 100, 220, 255); color_t center = color4b(0, 100, 220, 200); - for(uint8_t y = 0; y < UIFRAME_BORDER_HEIGHT; y++) { - for(uint8_t x = 0; x < UIFRAME_BORDER_WIDTH; x++) { + for(uint8_t y = 0; y < UI_FRAME_TEXTURE_HEIGHT_POW2; y++) { + for(uint8_t x = 0; x < UI_FRAME_TEXTURE_WIDTH_POW2; x++) { color_t c; - if(x >= 12 || y >= 12) {// Because power of two is required. + if(x >= UI_FRAME_TEXTURE_WIDTH || y >= UI_FRAME_TEXTURE_HEIGHT) { c = COLOR_TRANSPARENT; - } else if(y < 4 || y >= 8 || x < 4 || x >= 8) { + } else if( + y < UI_FRAME_TILE_HEIGHT || y >= UI_FRAME_TILE_HEIGHT * 2 || + x < UI_FRAME_TILE_WIDTH || x >= UI_FRAME_TILE_WIDTH * 2 + ) { c = border; } else { c = center; } - UI_FRAME.pixels[y * UIFRAME_BORDER_WIDTH + x] = c; + UI_FRAME.pixels[y * UI_FRAME_TEXTURE_WIDTH_POW2 + x] = c; } } errorChain(textureInit( - &UI_FRAME.texture, UIFRAME_BORDER_WIDTH, UIFRAME_BORDER_HEIGHT, + &UI_FRAME.texture, + UI_FRAME_TEXTURE_WIDTH_POW2, UI_FRAME_TEXTURE_HEIGHT_POW2, TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = UI_FRAME.pixels } )); - UI_FRAME.tileset.tileWidth = 4; - UI_FRAME.tileset.tileHeight = 4; + UI_FRAME.tileset.tileWidth = UI_FRAME_TILE_WIDTH; + UI_FRAME.tileset.tileHeight = UI_FRAME_TILE_HEIGHT; UI_FRAME.tileset.columns = 3; UI_FRAME.tileset.rows = 3; UI_FRAME.tileset.tileCount = 9; - UI_FRAME.tileset.uv[0] = 4.0f / UIFRAME_BORDER_WIDTH; - UI_FRAME.tileset.uv[1] = 4.0f / UIFRAME_BORDER_HEIGHT; + UI_FRAME.tileset.uv[0] = + (float_t)UI_FRAME_TILE_WIDTH / UI_FRAME_TEXTURE_WIDTH_POW2; + UI_FRAME.tileset.uv[1] = + (float_t)UI_FRAME_TILE_HEIGHT / UI_FRAME_TEXTURE_HEIGHT_POW2; errorOk(); } @@ -66,8 +72,8 @@ errorret_t uiFrameDraw( } }; spritebatchsprite_t sprites[9]; - float_t tileW = (float_t)UI_FRAME.tileset.tileWidth; - float_t tileH = (float_t)UI_FRAME.tileset.tileHeight; + float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH; + float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT; sprites[0] = spriteBatchSpriteTilesetPosition( &UI_FRAME.tileset, 0, 0, diff --git a/src/dusk/ui/frame/uiframe.h b/src/dusk/ui/frame/uiframe.h index 20511a2e..9fef7257 100644 --- a/src/dusk/ui/frame/uiframe.h +++ b/src/dusk/ui/frame/uiframe.h @@ -10,13 +10,19 @@ #include "display/texture/texture.h" #include "display/texture/tileset.h" -#define UIFRAME_BORDER_WIDTH 16 -#define UIFRAME_BORDER_HEIGHT 16 +#define UI_FRAME_BORDER_WIDTH 6 +#define UI_FRAME_BORDER_HEIGHT 6 +#define UI_FRAME_TILE_WIDTH 1 +#define UI_FRAME_TILE_HEIGHT 1 +#define UI_FRAME_TEXTURE_WIDTH (UI_FRAME_TILE_WIDTH * 3) +#define UI_FRAME_TEXTURE_HEIGHT (UI_FRAME_TILE_HEIGHT * 3) +#define UI_FRAME_TEXTURE_WIDTH_POW2 4 +#define UI_FRAME_TEXTURE_HEIGHT_POW2 4 typedef struct { tileset_t tileset; texture_t texture; - color_t pixels[UIFRAME_BORDER_WIDTH * UIFRAME_BORDER_HEIGHT]; + color_t pixels[UI_FRAME_BORDER_WIDTH * UI_FRAME_BORDER_HEIGHT]; } uiframe_t; extern uiframe_t UI_FRAME; diff --git a/src/dusk/ui/frame/uisettings.c b/src/dusk/ui/frame/uisettings.c index 768ed5bf..14ff6cd6 100644 --- a/src/dusk/ui/frame/uisettings.c +++ b/src/dusk/ui/frame/uisettings.c @@ -79,10 +79,10 @@ errorret_t uiSettingsDraw(void) { errorChain(uiFrameDraw(0.0f, 0.0f, 300.0f, 300)); errorChain(uiMenuDraw( &UI_SETTINGS.menu, - UIFRAME_BORDER_WIDTH, - UIFRAME_BORDER_HEIGHT, - 300.0f - (UIFRAME_BORDER_WIDTH * 2), - 300.0f - (UIFRAME_BORDER_HEIGHT * 2) + UI_FRAME_BORDER_WIDTH, + UI_FRAME_BORDER_HEIGHT, + 300.0f - (UI_FRAME_BORDER_WIDTH * 2), + 300.0f - (UI_FRAME_BORDER_HEIGHT * 2) )); errorChain(spriteBatchFlush()); diff --git a/src/dusk/ui/rpg/CMakeLists.txt b/src/dusk/ui/rpg/CMakeLists.txt new file mode 100644 index 00000000..e248e4fe --- /dev/null +++ b/src/dusk/ui/rpg/CMakeLists.txt @@ -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 +) diff --git a/src/dusk/ui/rpg/uitextbox.c b/src/dusk/ui/rpg/uitextbox.c new file mode 100644 index 00000000..a08636fe --- /dev/null +++ b/src/dusk/ui/rpg/uitextbox.c @@ -0,0 +1,242 @@ +/** + * 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) { + assertNotNull(box, "Textbox cannot be NULL"); + memoryZero(box, sizeof(uitextbox_t)); +} + +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, UI_TEXTBOX_TEXT_MAX); + box->currentPage = 0; + box->scroll = 0; + box->layoutWidth = 0.0f; + box->layoutHeight = 0.0f; +} + +void uiTextboxBuildLayout( + uitextbox_t *box, + const float_t width, + const float_t height +) { + assertNotNull(box, "Textbox cannot be NULL"); + + 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 < UI_TEXTBOX_LINES_MAX) { + 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 < UI_TEXTBOX_LINES_MAX) { + 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 borderW = (float_t)UI_FRAME_BORDER_WIDTH; + float_t borderH = (float_t)UI_FRAME_BORDER_HEIGHT; + float_t contentX = x + borderW; + float_t contentY = y + borderH; + float_t contentW = width - 2.0f * borderW; + float_t contentH = height - 2.0f * borderH; + + if(contentW != box->layoutWidth || contentH != box->layoutHeight) { + uiTextboxBuildLayout(box, contentW, contentH); + } + + errorChain(uiFrameDraw(x, y, width, height)); + errorChain(spriteBatchFlush()); + + if(box->lineCount == 0 || box->text[0] == '\0') errorOk(); + + float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; + float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; + + shadermaterial_t material = { + .unlit = { + .color = COLOR_WHITE, + .texture = FONT_DEFAULT.texture + } + }; + + int32_t pageFirst = box->currentPage * box->linesPerPage; + int32_t pageLast = pageFirst + box->linesPerPage; + if(pageLast > box->lineCount) pageLast = box->lineCount; + + int32_t charsLeft = box->scroll; + + for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) { + uitextboxline_t *line = &box->lines[li]; + int32_t visible = line->count < charsLeft ? line->count : charsLeft; + float_t lineY = contentY + + (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING); + + for(int32_t ci = 0; ci < visible; ci++) { + char_t c = box->text[line->start + ci]; + if(c == ' ') continue; + spritebatchsprite_t sprite = textGetSprite( + (vec2){ contentX + (float_t)ci * fontW, lineY }, + c, + &FONT_DEFAULT + ); + errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); + } + + charsLeft -= visible; + } + + if(uiTextboxPageIsComplete(box)) { + spritebatchsprite_t caret = textGetSprite( + (vec2){ + contentX + contentW - fontW, + contentY + contentH - fontH + }, + 'v', + &FONT_DEFAULT + ); + errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material)); + } + + errorChain(spriteBatchFlush()); + errorOk(); +} + +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; +} diff --git a/src/dusk/ui/rpg/uitextbox.h b/src/dusk/ui/rpg/uitextbox.h new file mode 100644 index 00000000..2132a764 --- /dev/null +++ b/src/dusk/ui/rpg/uitextbox.h @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +#define UI_TEXTBOX_TEXT_MAX 1024 +#define UI_TEXTBOX_LINES_MAX 64 +#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4 +#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1 +#define UI_TEXTBOX_LINE_SPACING 0.0f + +typedef struct { + int32_t start; + int32_t count; +} uitextboxline_t; + +typedef struct { + 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; + + // last dimensions used for layout; rebuild triggers when these change + float_t layoutWidth; + float_t layoutHeight; + + int32_t currentPage; + int32_t scroll; +} uitextbox_t; + +/** + * Initializes a textbox, zeroing all state. + * + * @param box The textbox to initialize. + */ +void uiTextboxInit(uitextbox_t *box); + +/** + * 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); diff --git a/src/dusk/ui/rpg/uitextboxmain.c b/src/dusk/ui/rpg/uitextboxmain.c new file mode 100644 index 00000000..263f2d85 --- /dev/null +++ b/src/dusk/ui/rpg/uitextboxmain.c @@ -0,0 +1,85 @@ +/** + * 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 "ui/frame/uiframe.h" + +uitextbox_t UI_TEXTBOX_MAIN; +static uifocusitem_t *focusItem = NULL; + +errorret_t uiTextboxMainInit(void) { + uiTextboxInit(&UI_TEXTBOX_MAIN); + errorOk(); +} + +void uiTextboxMainSetText(const char_t *text) { + uiTextboxSetText(&UI_TEXTBOX_MAIN, text); + if(focusItem != NULL) return; + focusItem = uiFocusPush( + 1, 1, + uiTextboxMainFocusSelected, + NULL, + uiTextboxMainFocusClosed, + NULL + ); +} + +errorret_t uiTextboxMainUpdate(void) { + if(focusItem == NULL) errorOk(); + return uiTextboxUpdate(&UI_TEXTBOX_MAIN); +} + +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_BORDER_HEIGHT; + float_t w = (float_t)SCREEN.scanWidth; + float_t x = (float_t)SCREEN.scanX; + float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h; + return uiTextboxDraw(&UI_TEXTBOX_MAIN, x, y, w, h); +} + +bool_t uiTextboxMainPageIsComplete(void) { + return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN); +} + +bool_t uiTextboxMainHasNextPage(void) { + return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN); +} + +void uiTextboxMainNextPage(void) { + uiTextboxNextPage(&UI_TEXTBOX_MAIN); +} + +bool_t uiTextboxMainIsActive(void) { + return focusItem != NULL; +} + +bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) { + if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN)) { + UI_TEXTBOX_MAIN.scroll = uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN); + return true; + } + + if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN)) { + uiTextboxNextPage(&UI_TEXTBOX_MAIN); + return true; + } + + uiFocusPopItem(focusItem); + return true; +} + +bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) { + focusItem = NULL; + return true; +} diff --git a/src/dusk/ui/rpg/uitextboxmain.h b/src/dusk/ui/rpg/uitextboxmain.h new file mode 100644 index 00000000..04284ab0 --- /dev/null +++ b/src/dusk/ui/rpg/uitextboxmain.h @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "ui/rpg/uitextbox.h" +#include "ui/focus/uifocusitem.h" + +#define UI_TEXTBOX_MAIN_LINES 4 + +extern uitextbox_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); diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 577bcf9e..5c73e0bd 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -10,14 +10,13 @@ #include "ui/frame/uiframe.h" #include "ui/debug/uifps.h" #include "engine/engine.h" -#include "ui/uitextbox.h" #include "ui/overlay/uifullbox.h" #include "ui/overlay/uiloading.h" #include "ui/debug/uiplayerpos.h" #include "ui/overlay/uicrop.h" #include "ui/debug/uiconsole.h" -#include "ui/frame/uiframe.h" #include "ui/frame/uisettings.h" +#include "ui/rpg/uitextboxmain.h" uielement_t UI_ELEMENTS[] = { { @@ -39,14 +38,11 @@ uielement_t UI_ELEMENTS[] = { .dispose = uiSettingsDispose }, - // { .type = UI_ELEMENT_TYPE_SCRIPT, .script = { .script = "ui/test.js" } }, - - // { - // .init = uiTextboxInit, - // .update = uiTextboxUpdate, - // .draw = uiTextboxDraw, - // .dispose = uiTextboxDispose - // }, + { + .init = uiTextboxMainInit, + .update = uiTextboxMainUpdate, + .draw = uiTextboxMainDraw + }, // Fullbox over: above absolutely everything. { diff --git a/src/dusk/ui/uitextbox.c b/src/dusk/ui/uitextbox.c deleted file mode 100644 index fc1231f2..00000000 --- a/src/dusk/ui/uitextbox.c +++ /dev/null @@ -1,278 +0,0 @@ -// // 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 "event/event.h" -// #include "display/screen/screen.h" -// #include "display/texture/texture.h" -// #include "display/text/text.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; - -// UI_TEXTBOX.font = &FONT_DEFAULT; - -// float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; -// float_t fontH = (float_t)FONT_DEFAULT.tileset->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 = (float_t)SCREEN.scanX + 10.0f; -// UI_TEXTBOX.y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - -// tbHeight - 10.0f; -// UI_TEXTBOX.width = (float_t)SCREEN.scanWidth - 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_DEFAULT.tileset->tileWidth; -// UI_TEXTBOX.frame.tileset.tileHeight = FONT_DEFAULT.tileset->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; - -// eventInit( -// &UI_TEXTBOX.onPageComplete, -// UI_TEXTBOX.onPageCompleteCallbacks, -// UI_TEXTBOX.onPageCompleteUsers, -// 4 -// ); -// eventInit( -// &UI_TEXTBOX.onLastPage, -// UI_TEXTBOX.onLastPageCallbacks, -// UI_TEXTBOX.onLastPageUsers, -// 4 -// ); - -// 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.font->tileset->tileWidth; -// float_t fontH = (float_t)UI_TEXTBOX.font->tileset->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; - -// 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 - -// bool_t wasComplete = uiTextboxPageIsComplete(); - -// if(!wasComplete) { -// UI_TEXTBOX.scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK; -// } - -// if(inputPressed(UI_TEXTBOX.advanceAction)) { -// if(!uiTextboxPageIsComplete()) { -// UI_TEXTBOX.scroll = uiTextboxGetPageCharCount(); -// } else if(uiTextboxHasNextPage()) { -// uiTextboxNextPage(); -// } -// } - -// if(!wasComplete && uiTextboxPageIsComplete()) { -// eventInvoke(&UI_TEXTBOX.onPageComplete, &UI_TEXTBOX); -// if(!uiTextboxHasNextPage()) { -// eventInvoke(&UI_TEXTBOX.onLastPage, &UI_TEXTBOX); -// } -// } - -// 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) { -// if(UI_TEXTBOX.lineCount == 0 || UI_TEXTBOX.text[0] == '\0') errorOk(); - -// errorChain(uiFrameDraw( -// &UI_TEXTBOX.frame, -// UI_TEXTBOX.x, UI_TEXTBOX.y, -// UI_TEXTBOX.width, UI_TEXTBOX.height -// )); -// errorChain(spriteBatchFlush()); - -// shadermaterial_t textMaterial = { -// .unlit = { -// .color = UI_TEXTBOX.textColor, -// .texture = UI_TEXTBOX.font->texture -// } -// }; - -// 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.font->tileset->tileWidth; -// float_t fontH = (float_t)UI_TEXTBOX.font->tileset->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 + -// (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; -// spritebatchsprite_t sprite = textGetSprite( -// (vec2){ contentX + (float_t)ci * fontW, lineY }, -// c, -// UI_TEXTBOX.font -// ); -// errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, textMaterial)); -// } - -// charsLeft -= visible; -// } - -// errorChain(spriteBatchFlush()); -// errorOk(); -// } - -// errorret_t uiTextboxDispose(void) { -// uiFrameDispose(&UI_TEXTBOX.frame); -// UI_TEXTBOX.font = NULL; -// errorOk(); -// } diff --git a/src/dusk/ui/uitextbox.h b/src/dusk/ui/uitextbox.h deleted file mode 100644 index 29606c20..00000000 --- a/src/dusk/ui/uitextbox.h +++ /dev/null @@ -1,121 +0,0 @@ -// /** -// * Copyright (c) 2026 Dominic Masters -// * -// * This software is released under the MIT License. -// * https://opensource.org/licenses/MIT -// */ - -// #pragma once -// #include "ui/frame/uiframe.h" -// #include "display/text/text.h" -// #include "input/inputaction.h" -// #include "event/event.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; - -// font_t *font; -// 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; - -// eventcallback_t onPageCompleteCallbacks[4]; -// void *onPageCompleteUsers[4]; -// event_t onPageComplete; -// eventcallback_t onLastPageCallbacks[4]; -// void *onLastPageUsers[4]; -// event_t onLastPage; -// } 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. -// * -// * @return Any error that occurs. -// */ -// errorret_t uiTextboxDispose(void);