diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c index fe6e6119..e0acabba 100644 --- a/src/dusk/console/console.c +++ b/src/dusk/console/console.c @@ -17,7 +17,7 @@ console_t CONSOLE; void consoleInit(void) { memoryZero(&CONSOLE, sizeof(console_t)); - CONSOLE.visible = true; + CONSOLE.visible = false; #ifdef DUSK_CONSOLE_POSIX threadMutexInit(&CONSOLE.printMutex); diff --git a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c b/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c index c23a868e..69c27136 100644 --- a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c +++ b/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c @@ -7,7 +7,7 @@ #include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/item/itemgive.h" -#include "ui/rpg/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxmain.h" void cutsceneItemGiveStart( const cutsceneitem_t *item, diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetext.c b/src/dusk/rpg/cutscene/item/ui/cutscenetext.c index 38f8cd62..9ad60c75 100644 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetext.c +++ b/src/dusk/rpg/cutscene/item/ui/cutscenetext.c @@ -6,7 +6,7 @@ */ #include "rpg/cutscene/item/cutsceneitem.h" -#include "ui/rpg/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxmain.h" void cutsceneTextStart( const cutsceneitem_t *item, diff --git a/src/dusk/rpg/entity/interact/entityinteract.c b/src/dusk/rpg/entity/interact/entityinteract.c index 6601b411..d7a8b224 100644 --- a/src/dusk/rpg/entity/interact/entityinteract.c +++ b/src/dusk/rpg/entity/interact/entityinteract.c @@ -8,7 +8,7 @@ #include "rpg/entity/entity.h" #include "assert/assert.h" #include "rpg/cutscene/cutscenesystem.h" -#include "ui/rpg/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxmain.h" void entityInteractWith(entity_t *player, entity_t *target) { assertNotNull(player, "Player entity pointer cannot be NULL"); diff --git a/src/dusk/rpg/entity/item/entityitem.c b/src/dusk/rpg/entity/item/entityitem.c index dfcbec9a..eccfd0f6 100644 --- a/src/dusk/rpg/entity/item/entityitem.c +++ b/src/dusk/rpg/entity/item/entityitem.c @@ -9,7 +9,7 @@ #include "rpg/entity/entity.h" #include "assert/assert.h" #include "rpg/item/itemgive.h" -#include "ui/rpg/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxmain.h" void entityItemInit(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); diff --git a/src/dusk/rpg/item/itemgive.c b/src/dusk/rpg/item/itemgive.c index 8872fff7..b133ab49 100644 --- a/src/dusk/rpg/item/itemgive.c +++ b/src/dusk/rpg/item/itemgive.c @@ -7,7 +7,7 @@ #include "itemgive.h" #include "rpg/item/backpack.h" -#include "ui/rpg/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxmain.h" #include "util/string.h" #include "error/error.h" diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index 26d7c6a1..73f63e1b 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -15,6 +15,7 @@ #include "rpg/cutscene/scene/testcutscene.h" #include "rpg/item/backpack.h" #include "rpg/battle/party.h" +#include "ui/rpg/textbox/uitextboxminilist.h" #include "time/time.h" #include "rpgcamera.h" #include "util/memory.h" @@ -64,6 +65,16 @@ errorret_t rpgInit(void) { backpackAdd(ITEM_ID_POTATO, 3); backpackAdd(ITEM_ID_APPLE, 8); + // TEST: Show a mini textbox at the world origin. + uiTextboxMiniShow( + &UI_TEXTBOX_MINI_LIST[0], + "Hello, World!", + (vec3){ 0.0f, 0.0f, 0.0f }, + 100.0f, + NULL, + NULL + ); + // TEST: Create a test map area. uint8_t areaIndex = mapAreaAdd( (worldpos_t){ 11, 3, 0 }, diff --git a/src/dusk/rpg/rpgcamera.c b/src/dusk/rpg/rpgcamera.c index e9014840..f717ac9d 100644 --- a/src/dusk/rpg/rpgcamera.c +++ b/src/dusk/rpg/rpgcamera.c @@ -56,6 +56,39 @@ void rpgCameraUpdateProjection(void) { ); } +void rpgCameraUpdateEye(void) { + float_t fov = glm_rad(RPG_CAMERA_FOV); + float_t pixelsPerUnit = TILE_SIZE_PIXELS; + float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit; + float_t z = (worldH * 0.5f) / tanf(fov * 0.5f); + float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS); + + vec3 target; + rpgCameraGetPosition(target); + glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target); + + glm_lookat( + (vec3){ target[0], target[1] + offset, target[2] + z }, + target, + (vec3){ 0, 1, 0 }, // up + RPG_CAMERA.eye + ); +} + +void rpgCameraToScreen(vec3 worldPos, vec2 out) { + mat4 viewProj; + glm_mat4_mul(RPG_CAMERA.projection, RPG_CAMERA.eye, viewProj); + + vec4 viewport = { + 0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height + }; + vec3 window; + glm_project(worldPos, viewProj, viewport, window); + + out[0] = window[0]; + out[1] = (float_t)SCREEN.height - window[1]; +} + errorret_t rpgCameraUpdate(void) { if(!mapIsLoaded()) errorOk(); diff --git a/src/dusk/rpg/rpgcamera.h b/src/dusk/rpg/rpgcamera.h index ba2e9da8..056cdb55 100644 --- a/src/dusk/rpg/rpgcamera.h +++ b/src/dusk/rpg/rpgcamera.h @@ -59,4 +59,21 @@ errorret_t rpgCameraUpdate(void); * matrix is computed once and cached; on dynamic-display platforms it * is recomputed every call. */ -void rpgCameraUpdateProjection(void); \ No newline at end of file +void rpgCameraUpdateProjection(void); + +/** + * Recomputes the camera eye/view matrix from the camera's current mode + * and position, and stores it in RPG_CAMERA.eye. Unlike the projection + * matrix this is never cached, since the camera position can change + * every frame. + */ +void rpgCameraUpdateEye(void); + +/** + * Converts a world-space position to screen-space pixel coordinates, + * using the camera's current eye and projection matrices. + * + * @param worldPos The world-space position to convert. + * @param out Output vec2 filled with the screen-space pixel position. + */ +void rpgCameraToScreen(vec3 worldPos, vec2 out); \ No newline at end of file diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index f84506e0..e20b83da 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -44,7 +44,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL })); - mat4 model, eye; + mat4 model; errorChain(shaderBind(&SHADER_UNLIT)); @@ -59,27 +59,10 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { )); // Camera Eye - float_t fov = glm_rad(RPG_CAMERA_FOV); - float_t pixelsPerUnit = TILE_SIZE_PIXELS; - float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit; - float_t z = (worldH * 0.5f) / tanf(fov * 0.5f); - vec3 worldPosVec; - rpgCameraGetPosition(worldPosVec); - float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS); - - glm_vec3_add(worldPosVec, (vec3){ 0.5f, 0.5f, 0.5f }, worldPosVec); - - glm_lookat( - (vec3){ - worldPosVec[0], - worldPosVec[1] + offset, - worldPosVec[2] + z - }, - worldPosVec, - (vec3){ 0, 1, 0 }, // up - eye - ); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye)); + rpgCameraUpdateEye(); + errorChain(shaderSetMatrix( + &SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye + )); // Base terrain meshes, drawn with normal depth testing. errorChain(sceneOverworldDrawChunksBase()); diff --git a/src/dusk/ui/frame/uiframe.h b/src/dusk/ui/frame/uiframe.h index 25fdc277..626afd64 100644 --- a/src/dusk/ui/frame/uiframe.h +++ b/src/dusk/ui/frame/uiframe.h @@ -12,8 +12,8 @@ #define UI_FRAME_BORDER_WIDTH 6 #define UI_FRAME_BORDER_HEIGHT 6 -#define UI_FRAME_PADDING_X 4 -#define UI_FRAME_PADDING_Y 4 +#define UI_FRAME_PADDING_X 2 +#define UI_FRAME_PADDING_Y 2 #define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X) #define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y) #define UI_FRAME_TILE_WIDTH 1 diff --git a/src/dusk/ui/rpg/CMakeLists.txt b/src/dusk/ui/rpg/CMakeLists.txt index e248e4fe..a8ae223b 100644 --- a/src/dusk/ui/rpg/CMakeLists.txt +++ b/src/dusk/ui/rpg/CMakeLists.txt @@ -3,8 +3,4 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uitextbox.c - uitextboxmain.c -) +add_subdirectory(textbox) diff --git a/src/dusk/ui/rpg/textbox/CMakeLists.txt b/src/dusk/ui/rpg/textbox/CMakeLists.txt new file mode 100644 index 00000000..e7787f2d --- /dev/null +++ b/src/dusk/ui/rpg/textbox/CMakeLists.txt @@ -0,0 +1,12 @@ +# 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 + uitextboxmini.c + uitextboxminilist.c +) diff --git a/src/dusk/ui/rpg/uitextbox.c b/src/dusk/ui/rpg/textbox/uitextbox.c similarity index 90% rename from src/dusk/ui/rpg/uitextbox.c rename to src/dusk/ui/rpg/textbox/uitextbox.c index 088ab652..47bdbdf6 100644 --- a/src/dusk/ui/rpg/uitextbox.c +++ b/src/dusk/ui/rpg/textbox/uitextbox.c @@ -16,15 +16,29 @@ #include "display/shader/shaderunlit.h" #include "ui/frame/uiframe.h" -void uiTextboxInit(uitextbox_t *box) { +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; } 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); + stringCopy(box->text, text, box->maxLength); box->currentPage = 0; box->scroll = 0; box->layoutWidth = 0.0f; @@ -60,12 +74,12 @@ void uiTextboxBuildLayout( char_t *src = box->text; int32_t i = 0; - while(src[i] != '\0' && box->lineCount < UI_TEXTBOX_LINES_MAX) { + 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 < UI_TEXTBOX_LINES_MAX) { + while(pad > 0 && box->lineCount < (int32_t)box->linesMax) { box->lines[box->lineCount].start = i; box->lines[box->lineCount].count = 0; box->lineCount++; @@ -196,18 +210,6 @@ errorret_t uiTextboxDraw( 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(); } diff --git a/src/dusk/ui/rpg/uitextbox.h b/src/dusk/ui/rpg/textbox/uitextbox.h similarity index 83% rename from src/dusk/ui/rpg/uitextbox.h rename to src/dusk/ui/rpg/textbox/uitextbox.h index 2132a764..047e7e1f 100644 --- a/src/dusk/ui/rpg/uitextbox.h +++ b/src/dusk/ui/rpg/textbox/uitextbox.h @@ -8,8 +8,6 @@ #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 @@ -20,9 +18,11 @@ typedef struct { } uitextboxline_t; typedef struct { - char_t text[UI_TEXTBOX_TEXT_MAX]; + char_t *text; + uint32_t maxLength; - uitextboxline_t lines[UI_TEXTBOX_LINES_MAX]; + uitextboxline_t *lines; + uint32_t linesMax; int32_t lineCount; int32_t charsPerLine; int32_t linesPerPage; @@ -37,11 +37,22 @@ typedef struct { } uitextbox_t; /** - * Initializes a textbox, zeroing all state. + * 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); +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. diff --git a/src/dusk/ui/rpg/textbox/uitextboxmain.c b/src/dusk/ui/rpg/textbox/uitextboxmain.c new file mode 100644 index 00000000..e65892ea --- /dev/null +++ b/src/dusk/ui/rpg/textbox/uitextboxmain.c @@ -0,0 +1,122 @@ +/** + * 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)); + errorChain(spriteBatchFlush()); + + 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; +} diff --git a/src/dusk/ui/rpg/uitextboxmain.h b/src/dusk/ui/rpg/textbox/uitextboxmain.h similarity index 86% rename from src/dusk/ui/rpg/uitextboxmain.h rename to src/dusk/ui/rpg/textbox/uitextboxmain.h index 04284ab0..e7eb83f6 100644 --- a/src/dusk/ui/rpg/uitextboxmain.h +++ b/src/dusk/ui/rpg/textbox/uitextboxmain.h @@ -6,12 +6,20 @@ */ #pragma once -#include "ui/rpg/uitextbox.h" +#include "ui/rpg/textbox/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 -extern uitextbox_t UI_TEXTBOX_MAIN; +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. diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.c b/src/dusk/ui/rpg/textbox/uitextboxmini.c new file mode 100644 index 00000000..7e4b43b2 --- /dev/null +++ b/src/dusk/ui/rpg/textbox/uitextboxmini.c @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uitextboxmini.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "time/time.h" +#include "rpg/rpgcamera.h" +#include "display/text/text.h" +#include "display/screen/screen.h" +#include "ui/frame/uiframe.h" + +void uiTextboxMiniInit(uitextboxmini_t *mini) { + assertNotNull(mini, "Mini textbox cannot be NULL"); + memoryZero(mini, sizeof(uitextboxmini_t)); + uiTextboxInit( + &mini->box, + mini->text, UI_TEXTBOX_MINI_TEXT_MAX, + mini->lines, UI_TEXTBOX_MINI_LINES_MAX + ); +} + +void uiTextboxMiniShow( + uitextboxmini_t *mini, + const char_t *text, + vec3 position, + const float_t duration, + uitextboxminiclosedcallback_t closed, + void *user +) { + assertNotNull(mini, "Mini textbox cannot be NULL"); + assertNotNull(text, "Text cannot be NULL"); + uiTextboxSetText(&mini->box, text); + glm_vec3_copy(position, mini->position); + + int32_t textWidth, textHeight; + textMeasure(text, &FONT_DEFAULT, &textWidth, &textHeight); + float_t width = (float_t)textWidth + 2.0f * (float_t)UI_FRAME_START_X; + mini->width = width < UI_TEXTBOX_MINI_WIDTH_MAX + ? width : UI_TEXTBOX_MINI_WIDTH_MAX; + + float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; + float_t maxHeight = (float_t)UI_TEXTBOX_MINI_LINES_MAX * fontH + + (float_t)(UI_TEXTBOX_MINI_LINES_MAX - 1) * UI_TEXTBOX_LINE_SPACING + + 2.0f * (float_t)UI_FRAME_START_Y; + uiTextboxBuildLayout( + &mini->box, + mini->width - 2.0f * (float_t)UI_FRAME_START_X, + maxHeight - 2.0f * (float_t)UI_FRAME_START_Y + ); + int32_t lineCount = mini->box.lineCount > 0 ? mini->box.lineCount : 1; + mini->height = (float_t)lineCount * fontH + + (float_t)(lineCount - 1) * UI_TEXTBOX_LINE_SPACING + + 2.0f * (float_t)UI_FRAME_START_Y; + + mini->active = true; + mini->timer = duration; + mini->closed = closed; + mini->user = user; +} + +errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini) { + assertNotNull(mini, "Mini textbox cannot be NULL"); + if(!mini->active) errorOk(); + + errorChain(uiTextboxUpdate(&mini->box)); + + mini->timer -= TIME.delta; + if(mini->timer <= 0.0f) uiTextboxMiniClose(mini); + + errorOk(); +} + +errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini) { + assertNotNull(mini, "Mini textbox cannot be NULL"); + if(!mini->active) errorOk(); + + vec2 screenPos; + rpgCameraToScreen(mini->position, screenPos); + + if( + mini->clamp == UI_TEXTBOX_MINI_CLAMP_X || + mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH + ) { + float_t minX = (float_t)SCREEN.scanX; + float_t maxX = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - mini->width; + if(screenPos[0] < minX) screenPos[0] = minX; + if(screenPos[0] > maxX) screenPos[0] = maxX; + } + + if( + mini->clamp == UI_TEXTBOX_MINI_CLAMP_Y || + mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH + ) { + float_t minY = (float_t)SCREEN.scanY; + float_t maxY = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - mini->height; + if(screenPos[1] < minY) screenPos[1] = minY; + if(screenPos[1] > maxY) screenPos[1] = maxY; + } + + return uiTextboxDraw( + &mini->box, screenPos[0], screenPos[1], mini->width, mini->height + ); +} + +bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini) { + assertNotNull(mini, "Mini textbox cannot be NULL"); + return mini->active; +} + +void uiTextboxMiniClose(uitextboxmini_t *mini) { + assertNotNull(mini, "Mini textbox cannot be NULL"); + if(!mini->active) return; + mini->active = false; + if(mini->closed != NULL) mini->closed(mini); +} diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.h b/src/dusk/ui/rpg/textbox/uitextboxmini.h new file mode 100644 index 00000000..eb61608d --- /dev/null +++ b/src/dusk/ui/rpg/textbox/uitextboxmini.h @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "ui/rpg/textbox/uitextbox.h" + +#define UI_TEXTBOX_MINI_TEXT_MAX 128 +#define UI_TEXTBOX_MINI_LINES_MAX 2 +#define UI_TEXTBOX_MINI_WIDTH_MAX 160.0f + +typedef struct uitextboxmini_s uitextboxmini_t; +typedef void (*uitextboxminiclosedcallback_t)(const uitextboxmini_t *mini); + +/** + * Determines whether a mini textbox's screen position is clamped to stay + * fully on screen. Defaults to UI_TEXTBOX_MINI_CLAMP_NONE. + */ +typedef enum { + UI_TEXTBOX_MINI_CLAMP_NONE, + UI_TEXTBOX_MINI_CLAMP_X, + UI_TEXTBOX_MINI_CLAMP_Y, + UI_TEXTBOX_MINI_CLAMP_BOTH +} uitextboxminiclamp_t; + +typedef struct uitextboxmini_s { + uitextbox_t box; + char_t text[UI_TEXTBOX_MINI_TEXT_MAX]; + uitextboxline_t lines[UI_TEXTBOX_MINI_LINES_MAX]; + + vec3 position; + float_t width; + float_t height; + uitextboxminiclamp_t clamp; + bool_t active; + float_t timer; + + uitextboxminiclosedcallback_t closed; + void *user; +} uitextboxmini_t; + +/** + * Initializes a mini textbox, zeroing all state and binding its internal + * uitextbox_t to its own fixed-size text and line storage. + * + * @param mini The mini textbox to initialize. + */ +void uiTextboxMiniInit(uitextboxmini_t *mini); + +/** + * Shows a mini textbox with the given text at the given world position for + * the given duration. Resets the typewriter scroll, measures the text to + * size the box (width capped at UI_TEXTBOX_MINI_WIDTH_MAX, height derived + * from the resulting wrapped line count), and starts the visibility timer. + * + * @param mini The mini textbox to show. + * @param text Null-terminated source string. + * @param position World-space position the box is anchored to on screen. + * @param duration How long the mini textbox stays visible, in seconds. + * @param closed Called once the timer elapses and the box closes. May be + * NULL. + * @param user Opaque pointer passed back through the closed callback. + */ +void uiTextboxMiniShow( + uitextboxmini_t *mini, + const char_t *text, + vec3 position, + const float_t duration, + uitextboxminiclosedcallback_t closed, + void *user +); + +/** + * Advances the typewriter scroll and counts down the visibility timer. + * Closes the mini textbox and fires its closed callback once the timer + * elapses. Has no effect if not active. + * + * @param mini The mini textbox to update. + * @returns Any error that occurs. + */ +errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini); + +/** + * Draws the mini textbox at its text-measured size, anchored on screen to + * its world-space position via the RPG camera. Has no effect if not + * active. + * + * @param mini The mini textbox to draw. + * @returns Any error that occurs. + */ +errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini); + +/** + * Returns true when the mini textbox is currently visible. + * + * @param mini The mini textbox to query. + * @returns True if active. + */ +bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini); + +/** + * Immediately closes the mini textbox and fires its closed callback. + * Has no effect if not active. + * + * @param mini The mini textbox to close. + */ +void uiTextboxMiniClose(uitextboxmini_t *mini); diff --git a/src/dusk/ui/rpg/textbox/uitextboxminilist.c b/src/dusk/ui/rpg/textbox/uitextboxminilist.c new file mode 100644 index 00000000..3e066418 --- /dev/null +++ b/src/dusk/ui/rpg/textbox/uitextboxminilist.c @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uitextboxminilist.h" + +uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT]; + +errorret_t uiTextboxMiniListInit(void) { + for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) { + uiTextboxMiniInit(&UI_TEXTBOX_MINI_LIST[i]); + } + errorOk(); +} + +errorret_t uiTextboxMiniListUpdate(void) { + for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) { + errorChain(uiTextboxMiniUpdate(&UI_TEXTBOX_MINI_LIST[i])); + } + errorOk(); +} + +errorret_t uiTextboxMiniListDraw(void) { + for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) { + errorChain(uiTextboxMiniDraw(&UI_TEXTBOX_MINI_LIST[i])); + } + errorOk(); +} diff --git a/src/dusk/ui/rpg/textbox/uitextboxminilist.h b/src/dusk/ui/rpg/textbox/uitextboxminilist.h new file mode 100644 index 00000000..9b4167b2 --- /dev/null +++ b/src/dusk/ui/rpg/textbox/uitextboxminilist.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "ui/rpg/textbox/uitextboxmini.h" + +#define UI_TEXTBOX_MINI_LIST_COUNT 4 + +extern uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT]; + +/** + * Initializes all UI_TEXTBOX_MINI_LIST slots. + * + * @returns Any error that occurs. + */ +errorret_t uiTextboxMiniListInit(void); + +/** + * Updates all UI_TEXTBOX_MINI_LIST slots. + * + * @returns Any error that occurs. + */ +errorret_t uiTextboxMiniListUpdate(void); + +/** + * Draws all active UI_TEXTBOX_MINI_LIST slots. + * + * @returns Any error that occurs. + */ +errorret_t uiTextboxMiniListDraw(void); diff --git a/src/dusk/ui/rpg/uitextboxmain.c b/src/dusk/ui/rpg/uitextboxmain.c deleted file mode 100644 index 65c69644..00000000 --- a/src/dusk/ui/rpg/uitextboxmain.c +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 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, - 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_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; - 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/uielement.c b/src/dusk/ui/uielement.c index 14e75a78..877251c3 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -21,7 +21,8 @@ #include "ui/frame/battle/uibattlemenu.h" #include "ui/frame/backpack/uibackpack.h" #include "ui/frame/uiconfirm.h" -#include "ui/rpg/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxmain.h" +#include "ui/rpg/textbox/uitextboxminilist.h" uielement_t UI_ELEMENTS[] = { { @@ -74,6 +75,12 @@ uielement_t UI_ELEMENTS[] = { .draw = uiTextboxMainDraw }, + { + .init = uiTextboxMiniListInit, + .update = uiTextboxMiniListUpdate, + .draw = uiTextboxMiniListDraw + }, + { .init = uiTransitionInit, .update = uiTransitionUpdate,