Scene.set() JS lifecycle, UI sprite caching, emulator test scripts
- Add Scene.set(module) so JerryScript can switch scenes via
require()'d {init, update, dispose} modules; wire it into
engineUpdate() and rework overworldscene.js/init.js to the new shape.
- Fix scriptManagerExecFile() never pushing its own file's directory
onto the require() dir stack, breaking relative require() calls from
a top-level entry script (e.g. init.js -> ./overworldscene.js).
- Cache sprite geometry across frames for uislider/uitab/uiframe
(dialogs/textbox/settings) and give uitextbox a per-page glyph cache
instead of one spriteBatchBuffer call per visible character. uiconsole
drops its alloc/free vertex buffer for a fixed-size one. uifps skips
rebuilding its label when the text hasn't changed.
- Add PSP_OPTIMIZATION_PLAN.md and a handful of UI/spritebatch unit
tests covering the new caching logic.
- Add Dolphin/PPSSPP emulator smoke-test scripts (+ Docker variants and
CI jobs) for GameCube/Wii/PSP.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,3 +39,16 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
||||
sprite.uvMax[1] = uv[3];
|
||||
return sprite;
|
||||
}
|
||||
|
||||
spritebatchsprite_t spriteBatchSpriteTranslate(
|
||||
const spritebatchsprite_t *sprite,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
) {
|
||||
spritebatchsprite_t out = *sprite;
|
||||
out.min[0] += x;
|
||||
out.min[1] += y;
|
||||
out.max[0] += x;
|
||||
out.max[1] += y;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -38,3 +38,19 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
||||
const float_t width,
|
||||
const float_t height
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a copy of sprite translated by (x, y). Used to reposition a
|
||||
* sprite cached relative to origin (0,0) at draw time, instead of
|
||||
* re-deriving its geometry from scratch every frame.
|
||||
*
|
||||
* @param sprite The cached sprite, relative to origin (0,0).
|
||||
* @param x X offset to translate by.
|
||||
* @param y Y offset to translate by.
|
||||
* @returns The translated sprite.
|
||||
*/
|
||||
spritebatchsprite_t spriteBatchSpriteTranslate(
|
||||
const spritebatchsprite_t *sprite,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "scene/scene.h"
|
||||
#include "asset/asset.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "script/module/scene/modulescene.h"
|
||||
#include "ui/ui.h"
|
||||
#include "assert/assert.h"
|
||||
#include "network/network.h"
|
||||
@@ -74,7 +75,7 @@ errorret_t engineUpdate(void) {
|
||||
consoleUpdate();
|
||||
|
||||
errorChain(gameUpdate());
|
||||
errorChain(scriptManagerCallGlobal("update"));
|
||||
errorChain(moduleSceneUpdateCurrent());
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
|
||||
@@ -7,11 +7,17 @@
|
||||
|
||||
#include "modulescene.h"
|
||||
#include "script/module/modulebase.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "scene/scene.h"
|
||||
#include "util/string.h"
|
||||
|
||||
scriptproto_t MODULE_SCENE_PROTO;
|
||||
|
||||
// The module last installed via Scene.set(), and the scene it owns.
|
||||
// SCENE_ID_INVALID means no module is currently installed.
|
||||
static jerry_value_t MODULE_SCENE_CURRENT;
|
||||
static sceneid_t MODULE_SCENE_CURRENT_ID;
|
||||
|
||||
moduleBaseFunction(moduleSceneConstructor) {
|
||||
modulescenehandle_t *inst = (modulescenehandle_t *)memoryAllocate(
|
||||
sizeof(modulescenehandle_t)
|
||||
@@ -49,6 +55,31 @@ moduleBaseFunction(moduleSceneGetActiveStatic) {
|
||||
return scriptProtoCreateValue(&MODULE_SCENE_PROTO, &h);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleSceneSetStatic) {
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
|
||||
|
||||
moduleSceneTeardownCurrent();
|
||||
|
||||
sceneid_t newId = sceneCreate();
|
||||
sceneSetActive(newId);
|
||||
MODULE_SCENE_CURRENT = jerry_value_copy(args[0]);
|
||||
MODULE_SCENE_CURRENT_ID = newId;
|
||||
|
||||
jerry_value_t initFn = moduleBaseGetProp(MODULE_SCENE_CURRENT, "init");
|
||||
if(jerry_value_is_function(initFn)) {
|
||||
errorret_t ret = scriptManagerCallValue(
|
||||
MODULE_SCENE_CURRENT, initFn, "Scene module", "init"
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
jerry_value_free(initFn);
|
||||
return moduleBaseThrowError(ret);
|
||||
}
|
||||
}
|
||||
jerry_value_free(initFn);
|
||||
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleSceneToString) {
|
||||
modulescenehandle_t *inst = moduleSceneGet(callInfo);
|
||||
if(!inst) return jerry_string_sz("Scene(?)");
|
||||
@@ -79,9 +110,15 @@ void moduleSceneInit(void) {
|
||||
scriptProtoDefineStaticFunc(
|
||||
&MODULE_SCENE_PROTO, "getActive", moduleSceneGetActiveStatic
|
||||
);
|
||||
scriptProtoDefineStaticFunc(
|
||||
&MODULE_SCENE_PROTO, "set", moduleSceneSetStatic
|
||||
);
|
||||
|
||||
MODULE_SCENE_CURRENT_ID = SCENE_ID_INVALID;
|
||||
}
|
||||
|
||||
void moduleSceneDispose(void) {
|
||||
moduleSceneTeardownCurrent();
|
||||
}
|
||||
|
||||
modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo) {
|
||||
@@ -89,3 +126,37 @@ modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo) {
|
||||
&MODULE_SCENE_PROTO, callInfo->this_value
|
||||
);
|
||||
}
|
||||
|
||||
void moduleSceneTeardownCurrent(void) {
|
||||
if(MODULE_SCENE_CURRENT_ID == SCENE_ID_INVALID) return;
|
||||
|
||||
jerry_value_t disposeFn = moduleBaseGetProp(MODULE_SCENE_CURRENT, "dispose");
|
||||
if(jerry_value_is_function(disposeFn)) {
|
||||
errorret_t ret = scriptManagerCallValue(
|
||||
MODULE_SCENE_CURRENT, disposeFn, "Scene module", "dispose"
|
||||
);
|
||||
errorCatch(errorPrint(ret));
|
||||
}
|
||||
jerry_value_free(disposeFn);
|
||||
|
||||
sceneDestroy(MODULE_SCENE_CURRENT_ID);
|
||||
jerry_value_free(MODULE_SCENE_CURRENT);
|
||||
MODULE_SCENE_CURRENT_ID = SCENE_ID_INVALID;
|
||||
}
|
||||
|
||||
errorret_t moduleSceneUpdateCurrent(void) {
|
||||
if(MODULE_SCENE_CURRENT_ID == SCENE_ID_INVALID) errorOk();
|
||||
|
||||
jerry_value_t updateFn = moduleBaseGetProp(MODULE_SCENE_CURRENT, "update");
|
||||
if(!jerry_value_is_function(updateFn)) {
|
||||
jerry_value_free(updateFn);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t ret = scriptManagerCallValue(
|
||||
MODULE_SCENE_CURRENT, updateFn, "Scene module", "update"
|
||||
);
|
||||
jerry_value_free(updateFn);
|
||||
errorChain(ret);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "script/scriptproto.h"
|
||||
#include "scene/scenebase.h"
|
||||
#include <jerryscript.h>
|
||||
@@ -27,7 +28,8 @@ extern scriptproto_t MODULE_SCENE_PROTO;
|
||||
void moduleSceneInit(void);
|
||||
|
||||
/**
|
||||
* Disposes the Scene class's script resources.
|
||||
* Disposes the Scene class's script resources, including calling
|
||||
* dispose() on and freeing whatever module Scene.set() last installed.
|
||||
*/
|
||||
void moduleSceneDispose(void);
|
||||
|
||||
@@ -39,3 +41,21 @@ void moduleSceneDispose(void);
|
||||
* @return The wrapped handle, or NULL if this_value isn't a Scene.
|
||||
*/
|
||||
modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo);
|
||||
|
||||
/**
|
||||
* Internal. Tears down whatever module Scene.set() currently has
|
||||
* installed, if any: calls its dispose() (errors are logged, not
|
||||
* propagated), destroys the scene it owns, and frees the held
|
||||
* reference. No-op if nothing is installed.
|
||||
*/
|
||||
void moduleSceneTeardownCurrent(void);
|
||||
|
||||
/**
|
||||
* Calls update() on whichever module Scene.set() last installed, if
|
||||
* any, and if it defines one. No-op if Scene.set() has never been
|
||||
* called. Called once per frame by engineUpdate().
|
||||
*
|
||||
* @return The error return value. An error is thrown if update() itself
|
||||
* throws, or if it returns a rejected promise.
|
||||
*/
|
||||
errorret_t moduleSceneUpdateCurrent(void);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "util/string.h"
|
||||
#include "scriptproto.h"
|
||||
#include "script/module/modulelist.h"
|
||||
#include "script/module/require/modulerequire.h"
|
||||
|
||||
scriptmanager_t SCRIPT_MANAGER;
|
||||
|
||||
@@ -82,8 +83,13 @@ errorret_t scriptManagerExecFile(
|
||||
src[size] = '\0';
|
||||
memoryFree(buffer);
|
||||
|
||||
char_t dir[ASSET_FILE_NAME_MAX];
|
||||
moduleRequireDirname(fname, dir, sizeof(dir));
|
||||
moduleRequireDirPush(dir);
|
||||
|
||||
errorret_t ret = scriptManagerExec(src, resultOut);
|
||||
memoryFree(src);
|
||||
moduleRequireDirPop();
|
||||
errorChain(ret);
|
||||
errorOk();
|
||||
}
|
||||
@@ -101,13 +107,24 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
jerry_value_t result = jerry_call(fn, jerry_undefined(), NULL, 0);
|
||||
errorret_t ret = scriptManagerCallValue(
|
||||
jerry_undefined(), fn, "Global function", name
|
||||
);
|
||||
jerry_value_free(fn);
|
||||
errorChain(ret);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t scriptManagerCallValue(
|
||||
const jerry_value_t thisArg,
|
||||
const jerry_value_t fn,
|
||||
const char_t *context,
|
||||
const char_t *name
|
||||
) {
|
||||
jerry_value_t result = jerry_call(fn, thisArg, NULL, 0);
|
||||
|
||||
if(jerry_value_is_exception(result)) {
|
||||
errorret_t err = scriptManagerFormatException(
|
||||
"Global function", name, result
|
||||
);
|
||||
errorret_t err = scriptManagerFormatException(context, name, result);
|
||||
jerry_value_free(result);
|
||||
errorChain(err);
|
||||
}
|
||||
@@ -131,7 +148,7 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
|
||||
if(jerry_promise_state(result) == JERRY_PROMISE_STATE_REJECTED) {
|
||||
jerry_value_t rejectVal = jerry_promise_result(result);
|
||||
errorret_t err = scriptManagerFormatValueError(
|
||||
"Global async function", name, rejectVal
|
||||
context, name, rejectVal
|
||||
);
|
||||
jerry_value_free(rejectVal);
|
||||
jerry_value_free(result);
|
||||
@@ -144,6 +161,7 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
|
||||
}
|
||||
|
||||
errorret_t scriptManagerDispose(void) {
|
||||
moduleListDispose();
|
||||
scriptProtoDisposeAll();
|
||||
|
||||
for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) {
|
||||
|
||||
@@ -50,7 +50,10 @@ errorret_t scriptManagerInit(void);
|
||||
errorret_t scriptManagerExec(const char_t *script, jerry_value_t *result);
|
||||
|
||||
/**
|
||||
* Execute a JS file in the active script context.
|
||||
* Execute a JS file in the active script context. While the file runs,
|
||||
* fname's own directory is pushed as the require() base directory, so a
|
||||
* top-level entry script (e.g. "scripts/init.js") can use relative
|
||||
* require('./foo.js') calls the same way a required file can.
|
||||
*
|
||||
* @param fname The filename of the script to execute.
|
||||
* @param result Optional out-parameter for the script's return value.
|
||||
@@ -81,6 +84,27 @@ errorret_t scriptManagerExecFile(
|
||||
*/
|
||||
errorret_t scriptManagerCallGlobal(const char_t *name);
|
||||
|
||||
/**
|
||||
* Calls an already-resolved JS function value with the given `this`,
|
||||
* handling a thrown exception and draining a returned promise to
|
||||
* completion (same async-await semantics as scriptManagerCallGlobal).
|
||||
* Caller is responsible for checking jerry_value_is_function(fn) first
|
||||
* and for freeing both thisArg and fn afterward.
|
||||
*
|
||||
* @param thisArg The value bound to `this` inside the call.
|
||||
* @param fn The function value to call.
|
||||
* @param context Short label for error messages, e.g. "Scene module".
|
||||
* @param name The name of the function being called, for error messages.
|
||||
* @return The error return value. An error is thrown if the JS function
|
||||
* itself throws, or if its returned promise rejects.
|
||||
*/
|
||||
errorret_t scriptManagerCallValue(
|
||||
const jerry_value_t thisArg,
|
||||
const jerry_value_t fn,
|
||||
const char_t *context,
|
||||
const char_t *name
|
||||
);
|
||||
|
||||
/**
|
||||
* Dispose of the script manager.
|
||||
*
|
||||
|
||||
@@ -6,20 +6,19 @@
|
||||
*/
|
||||
|
||||
#include "uiconsole.h"
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/mesh/quad.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
typedef struct {
|
||||
mesh_t mesh;
|
||||
bool_t built;
|
||||
meshvertex_t *vertices;
|
||||
int32_t capacity;
|
||||
meshvertex_t vertices[UI_CONSOLE_CACHE_VERTEX_MAX];
|
||||
int32_t vertexCount;
|
||||
int32_t cachedScanX;
|
||||
int32_t cachedScanY;
|
||||
@@ -62,22 +61,12 @@ errorret_t uiConsoleRebuild(void) {
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
spriteCount <= UI_CONSOLE_CACHE_GLYPH_MAX,
|
||||
"Console history exceeds fixed cache capacity"
|
||||
);
|
||||
|
||||
const int32_t vertexCount = spriteCount * QUAD_VERTEX_COUNT;
|
||||
if(vertexCount > UI_CONSOLE_CACHE.capacity) {
|
||||
if(UI_CONSOLE_CACHE.built) {
|
||||
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
||||
UI_CONSOLE_CACHE.built = false;
|
||||
}
|
||||
|
||||
if(UI_CONSOLE_CACHE.vertices != NULL) {
|
||||
memoryFree(UI_CONSOLE_CACHE.vertices);
|
||||
}
|
||||
|
||||
UI_CONSOLE_CACHE.vertices = (meshvertex_t *)memoryAllocate(
|
||||
sizeof(meshvertex_t) * vertexCount
|
||||
);
|
||||
UI_CONSOLE_CACHE.capacity = vertexCount;
|
||||
}
|
||||
UI_CONSOLE_CACHE.vertexCount = vertexCount;
|
||||
|
||||
if(vertexCount > 0) {
|
||||
@@ -124,7 +113,7 @@ errorret_t uiConsoleRebuild(void) {
|
||||
errorChain(meshInit(
|
||||
&UI_CONSOLE_CACHE.mesh,
|
||||
QUAD_PRIMITIVE_TYPE,
|
||||
UI_CONSOLE_CACHE.capacity,
|
||||
UI_CONSOLE_CACHE_VERTEX_MAX,
|
||||
UI_CONSOLE_CACHE.vertices
|
||||
));
|
||||
UI_CONSOLE_CACHE.built = true;
|
||||
@@ -144,10 +133,6 @@ errorret_t uiConsoleDispose(void) {
|
||||
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
||||
}
|
||||
|
||||
if(UI_CONSOLE_CACHE.vertices != NULL) {
|
||||
memoryFree(UI_CONSOLE_CACHE.vertices);
|
||||
}
|
||||
|
||||
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "display/mesh/quad.h"
|
||||
|
||||
// Fixed capacity for the console's cached glyph mesh, in glyphs -- the
|
||||
// mesh/vertex buffer is sized to this once and never grown/shrunk, so
|
||||
// history producing more non-space characters than this asserts (see
|
||||
// uiConsoleRebuild) instead of reallocating.
|
||||
#define UI_CONSOLE_CACHE_GLYPH_MAX 512
|
||||
#define UI_CONSOLE_CACHE_VERTEX_MAX (\
|
||||
UI_CONSOLE_CACHE_GLYPH_MAX * QUAD_VERTEX_COUNT \
|
||||
)
|
||||
|
||||
/**
|
||||
* Renders the console history into the scan-safe area, drawing a mesh
|
||||
@@ -22,6 +32,9 @@ errorret_t uiConsoleDraw(void);
|
||||
* Rebuilds the cached mesh for the console's current history and
|
||||
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
|
||||
* needed; only needs calling directly to force an immediate rebuild.
|
||||
* The underlying vertex buffer is a fixed-size array (see
|
||||
* UI_CONSOLE_CACHE_VERTEX_MAX) -- asserts if the history produces more
|
||||
* glyphs than that capacity, rather than growing it.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
|
||||
@@ -59,7 +59,9 @@ errorret_t uiFPSDraw() {
|
||||
}
|
||||
|
||||
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
|
||||
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
|
||||
if(stringCompare(fpsText, UIFPS.fpsLabel.text) != 0) {
|
||||
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
|
||||
}
|
||||
errorChain(uiLabelDraw(
|
||||
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
|
||||
));
|
||||
|
||||
@@ -148,7 +148,7 @@ errorret_t uiSettingsDraw(void) {
|
||||
const float_t y = (float_t)SCREEN.scanY +
|
||||
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(uiFrameDrawCached(&UI_SETTINGS.frameCache, x, y, width, height));
|
||||
|
||||
const float_t contentX = x + UI_FRAME_START_X;
|
||||
const float_t contentY = y + UI_FRAME_START_Y;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "uisettingsdata.h"
|
||||
|
||||
#define UI_SETTINGS_TAB_COUNT 4
|
||||
@@ -29,6 +30,7 @@ typedef struct {
|
||||
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
|
||||
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
|
||||
uisettingsdata_t data;
|
||||
uiframecache_t frameCache;
|
||||
} uisettings_t;
|
||||
|
||||
extern uisettings_t UI_SETTINGS;
|
||||
|
||||
@@ -82,7 +82,7 @@ errorret_t uiConfirmDraw(void) {
|
||||
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(uiFrameDrawCached(&UI_CONFIRM.frameCache, x, y, width, height));
|
||||
|
||||
float_t contentX = x + UI_FRAME_START_X;
|
||||
float_t contentY = y + UI_FRAME_START_Y;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
#include "ui/widget/uilabel.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
||||
#define UI_CONFIRM_INDEX_CONFIRM 0
|
||||
@@ -31,6 +32,7 @@ typedef struct {
|
||||
uiconfirmcallback_t callback;
|
||||
void *user;
|
||||
bool_t result;
|
||||
uiframecache_t frameCache;
|
||||
} uiconfirm_t;
|
||||
|
||||
extern uiconfirm_t UI_CONFIRM;
|
||||
|
||||
@@ -72,6 +72,17 @@ errorret_t uiFrameDraw(
|
||||
}
|
||||
};
|
||||
spritebatchsprite_t sprites[9];
|
||||
uiFrameBuildSprites(sprites, x, y, width, height);
|
||||
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
|
||||
}
|
||||
|
||||
void uiFrameBuildSprites(
|
||||
spritebatchsprite_t sprites[9],
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width,
|
||||
const float_t height
|
||||
) {
|
||||
float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
||||
float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||
|
||||
@@ -112,8 +123,37 @@ errorret_t uiFrameDraw(
|
||||
&UI_FRAME.tileset, 2, 2,
|
||||
x + width - tileW, y + height - tileH, tileW, tileH
|
||||
);
|
||||
}
|
||||
|
||||
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
|
||||
errorret_t uiFrameDrawCached(
|
||||
uiframecache_t *cache,
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width,
|
||||
const float_t height
|
||||
) {
|
||||
assertNotNull(cache, "Frame cache cannot be NULL");
|
||||
|
||||
if(
|
||||
!cache->built ||
|
||||
cache->lastX != x || cache->lastY != y ||
|
||||
cache->lastWidth != width || cache->lastHeight != height
|
||||
) {
|
||||
uiFrameBuildSprites(cache->sprites, x, y, width, height);
|
||||
cache->lastX = x;
|
||||
cache->lastY = y;
|
||||
cache->lastWidth = width;
|
||||
cache->lastHeight = height;
|
||||
cache->built = true;
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = COLOR_WHITE,
|
||||
.texture = &UI_FRAME.texture
|
||||
}
|
||||
};
|
||||
return spriteBatchBuffer(cache->sprites, 9, &SHADER_UNLIT, material);
|
||||
}
|
||||
|
||||
errorret_t uiFrameDispose(void) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "error/error.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/texture/tileset.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
#define UI_FRAME_BORDER_WIDTH 6
|
||||
#define UI_FRAME_BORDER_HEIGHT 6
|
||||
@@ -56,6 +57,63 @@ errorret_t uiFrameDraw(
|
||||
const float_t height
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds the 9 sprites for a 9-slice frame at the given rect. Used
|
||||
* internally by uiFrameDraw and uiFrameDrawCached -- call directly only
|
||||
* if you need the raw sprites instead of buffering them.
|
||||
*
|
||||
* @param sprites Destination array of exactly 9 sprites.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @param width Total width of the frame.
|
||||
* @param height Total height of the frame.
|
||||
*/
|
||||
void uiFrameBuildSprites(
|
||||
spritebatchsprite_t sprites[9],
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width,
|
||||
const float_t height
|
||||
);
|
||||
|
||||
/**
|
||||
* A frame's cached 9-slice sprites, plus the rect they were built for.
|
||||
* Owned by whichever widget/screen draws a frame repeatedly (dialogs,
|
||||
* textboxes, settings panels) so uiFrameDrawCached can skip rebuilding
|
||||
* the sprites when the rect hasn't moved since the last draw.
|
||||
*/
|
||||
typedef struct {
|
||||
spritebatchsprite_t sprites[9];
|
||||
bool_t built;
|
||||
float_t lastX;
|
||||
float_t lastY;
|
||||
float_t lastWidth;
|
||||
float_t lastHeight;
|
||||
} uiframecache_t;
|
||||
|
||||
/**
|
||||
* Draws a 9-slice frame using a caller-owned cache, only rebuilding the
|
||||
* sprites (via uiFrameBuildSprites) when x/y/width/height differ from
|
||||
* the last call -- unlike uiFrameDraw, which rebuilds unconditionally
|
||||
* every time. Prefer this for frames redrawn every frame at a fixed or
|
||||
* rarely-changing rect (dialogs, panels); use plain uiFrameDraw for
|
||||
* genuinely one-off or per-frame-varying rects.
|
||||
*
|
||||
* @param cache Caller-owned cache, zero-initialized before first use.
|
||||
* @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 uiFrameDrawCached(
|
||||
uiframecache_t *cache,
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const float_t width,
|
||||
const float_t height
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the global UI_FRAME, releasing its GPU texture.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,7 @@ void uiSliderInitFloat(
|
||||
slider->step.f = step;
|
||||
slider->value.f = mathClamp(value, min, max);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
void uiSliderInitInt(
|
||||
@@ -64,6 +65,7 @@ void uiSliderInitInt(
|
||||
slider->step.i = step;
|
||||
slider->value.i = mathClamp(value, min, max);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
float_t uiSliderGetFloat(const uislider_t *slider) {
|
||||
@@ -87,6 +89,7 @@ void uiSliderSetFloat(uislider_t *slider, const float_t value) {
|
||||
);
|
||||
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
||||
@@ -96,6 +99,7 @@ void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
||||
);
|
||||
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
void uiSliderStepUp(uislider_t *slider) {
|
||||
@@ -158,67 +162,36 @@ errorret_t uiSliderDraw(
|
||||
) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
|
||||
color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE;
|
||||
|
||||
errorChain(uiWidgetLabelDraw(&slider->label, x, y));
|
||||
|
||||
int32_t labelW, labelH;
|
||||
uiWidgetLabelGetSize(&slider->label, &labelW, &labelH);
|
||||
|
||||
float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP;
|
||||
float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
||||
|
||||
spritebatchsprite_t trackSprite = {
|
||||
.min = { trackX, trackY, 0.0f },
|
||||
.max = { trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
shadermaterial_t trackMaterial = {
|
||||
.unlit = {
|
||||
.color = COLOR_DARK_GRAY,
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial));
|
||||
spritebatchsprite_t track = spriteBatchSpriteTranslate(
|
||||
&slider->cachedTrack, x, y
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&track, 1, &SHADER_UNLIT, trackMaterial));
|
||||
|
||||
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
||||
if(fillWidth > 0.0f) {
|
||||
spritebatchsprite_t fillSprite = {
|
||||
.min = { trackX, trackY, 0.0f },
|
||||
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
if(slider->cachedFillVisible) {
|
||||
shadermaterial_t fillMaterial = {
|
||||
.unlit = {
|
||||
.color = color,
|
||||
.color = slider->highlighted ? COLOR_RED : COLOR_WHITE,
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial));
|
||||
spritebatchsprite_t fill = spriteBatchSpriteTranslate(
|
||||
&slider->cachedFill, x, y
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&fill, 1, &SHADER_UNLIT, fillMaterial));
|
||||
}
|
||||
|
||||
int32_t stepCount = uiSliderGetStepCount(slider);
|
||||
if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
|
||||
if(slider->cachedMarkerCount > 0) {
|
||||
spritebatchsprite_t markers[UI_SLIDER_STEP_MARKERS_MAX];
|
||||
for(int32_t i = 0; i <= stepCount; i++) {
|
||||
float_t markerX = trackX +
|
||||
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
||||
markers[i] = (spritebatchsprite_t){
|
||||
.min = {
|
||||
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||
0.0f
|
||||
},
|
||||
.max = {
|
||||
markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||
trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||
0.0f
|
||||
},
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
for(int32_t i = 0; i < slider->cachedMarkerCount; i++) {
|
||||
markers[i] = spriteBatchSpriteTranslate(&slider->cachedMarkers[i], x, y);
|
||||
}
|
||||
|
||||
shadermaterial_t markerMaterial = {
|
||||
@@ -227,13 +200,15 @@ errorret_t uiSliderDraw(
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
errorChain(
|
||||
spriteBatchBuffer(markers, stepCount + 1, &SHADER_UNLIT, markerMaterial)
|
||||
);
|
||||
errorChain(spriteBatchBuffer(
|
||||
markers, slider->cachedMarkerCount, &SHADER_UNLIT, markerMaterial
|
||||
));
|
||||
}
|
||||
|
||||
errorChain(uiWidgetLabelDraw(
|
||||
&slider->valueLabel, trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y
|
||||
&slider->valueLabel,
|
||||
x + slider->cachedTrack.max[0] + UI_SLIDER_GAP,
|
||||
y
|
||||
));
|
||||
|
||||
errorOk();
|
||||
@@ -252,3 +227,56 @@ void uiSliderRebuildValueLabel(uislider_t *slider) {
|
||||
}
|
||||
uiWidgetLabelSetText(&slider->valueLabel, valueText);
|
||||
}
|
||||
|
||||
void uiSliderRebuildGeometry(uislider_t *slider) {
|
||||
int32_t labelW, labelH;
|
||||
uiWidgetLabelGetSize(&slider->label, &labelW, &labelH);
|
||||
|
||||
float_t trackX = (float_t)labelW + UI_SLIDER_GAP;
|
||||
float_t trackY = ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
||||
|
||||
slider->cachedTrack = (spritebatchsprite_t){
|
||||
.min = { trackX, trackY, 0.0f },
|
||||
.max = {
|
||||
trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f
|
||||
},
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
|
||||
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
||||
slider->cachedFillVisible = fillWidth > 0.0f;
|
||||
if(slider->cachedFillVisible) {
|
||||
slider->cachedFill = (spritebatchsprite_t){
|
||||
.min = { trackX, trackY, 0.0f },
|
||||
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
}
|
||||
|
||||
int32_t stepCount = uiSliderGetStepCount(slider);
|
||||
if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
|
||||
for(int32_t i = 0; i <= stepCount; i++) {
|
||||
float_t markerX = trackX +
|
||||
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
||||
slider->cachedMarkers[i] = (spritebatchsprite_t){
|
||||
.min = {
|
||||
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||
0.0f
|
||||
},
|
||||
.max = {
|
||||
markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||
trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||
0.0f
|
||||
},
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
}
|
||||
slider->cachedMarkerCount = stepCount + 1;
|
||||
} else {
|
||||
slider->cachedMarkerCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uiwidgetlabel.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
#define UI_SLIDER_TRACK_WIDTH 80.0f
|
||||
#define UI_SLIDER_TRACK_HEIGHT 4.0f
|
||||
@@ -42,6 +43,16 @@ typedef struct {
|
||||
uislidervalue_t max;
|
||||
uislidervalue_t step;
|
||||
bool_t highlighted;
|
||||
|
||||
// Track/fill/marker geometry, cached relative to origin (0,0) and only
|
||||
// rebuilt when the value or step configuration changes (see
|
||||
// uiSliderRebuildGeometry) -- uiSliderDraw just translates these into
|
||||
// position instead of re-deriving them every frame.
|
||||
spritebatchsprite_t cachedTrack;
|
||||
spritebatchsprite_t cachedFill;
|
||||
bool_t cachedFillVisible;
|
||||
spritebatchsprite_t cachedMarkers[UI_SLIDER_STEP_MARKERS_MAX];
|
||||
int32_t cachedMarkerCount;
|
||||
} uislider_t;
|
||||
|
||||
/**
|
||||
@@ -192,3 +203,13 @@ errorret_t uiSliderDraw(
|
||||
* @param slider The slider to update.
|
||||
*/
|
||||
void uiSliderRebuildValueLabel(uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Rebuilds the slider's cached track/fill/marker geometry (relative to
|
||||
* origin (0,0)) from its current label size, value, and step
|
||||
* configuration. Called internally on init and whenever the value
|
||||
* changes -- uiSliderDraw never recomputes this itself.
|
||||
*
|
||||
* @param slider The slider to update.
|
||||
*/
|
||||
void uiSliderRebuildGeometry(uislider_t *slider);
|
||||
|
||||
@@ -19,6 +19,7 @@ void uiTabInit(uitab_t *tab, const char_t *label) {
|
||||
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
|
||||
uiWidgetLabelSetText(&tab->label, label);
|
||||
tab->active = false;
|
||||
uiTabRebuildBackground(tab);
|
||||
}
|
||||
|
||||
bool_t uiTabIsActive(const uitab_t *tab) {
|
||||
@@ -38,15 +39,9 @@ errorret_t uiTabDraw(
|
||||
) {
|
||||
assertNotNull(tab, "Tab cannot be NULL");
|
||||
|
||||
int32_t labelW, labelH;
|
||||
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
|
||||
|
||||
spritebatchsprite_t sprite = {
|
||||
.min = { x, y, 0.0f },
|
||||
.max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
spritebatchsprite_t sprite = spriteBatchSpriteTranslate(
|
||||
&tab->cachedBackground, x, y
|
||||
);
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = tab->active ? COLOR_GREEN : COLOR_RED,
|
||||
@@ -59,3 +54,16 @@ errorret_t uiTabDraw(
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTabRebuildBackground(uitab_t *tab) {
|
||||
assertNotNull(tab, "Tab cannot be NULL");
|
||||
|
||||
int32_t labelW, labelH;
|
||||
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
|
||||
tab->cachedBackground = (spritebatchsprite_t){
|
||||
.min = { 0.0f, 0.0f, 0.0f },
|
||||
.max = { (float_t)labelW, (float_t)labelH, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,10 +8,16 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uiwidgetlabel.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
typedef struct {
|
||||
uiwidgetlabel_t label;
|
||||
bool_t active;
|
||||
|
||||
// Background quad, cached relative to origin (0,0) from the label's
|
||||
// size at init -- uiTabDraw only translates this into position, since
|
||||
// active/inactive only changes the material color, not the geometry.
|
||||
spritebatchsprite_t cachedBackground;
|
||||
} uitab_t;
|
||||
|
||||
/**
|
||||
@@ -52,3 +58,13 @@ errorret_t uiTabDraw(
|
||||
const float_t x,
|
||||
const float_t y
|
||||
);
|
||||
|
||||
/**
|
||||
* Rebuilds the tab's cached background quad (relative to origin (0,0))
|
||||
* from its current label size. Called internally by uiTabInit -- the
|
||||
* background's geometry never changes afterwards, since the tab has no
|
||||
* API to change its label text.
|
||||
*
|
||||
* @param tab The tab to update.
|
||||
*/
|
||||
void uiTabRebuildBackground(uitab_t *tab);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "script/scriptmanager.h"
|
||||
|
||||
errorret_t gameInit(void) {
|
||||
errorChain(scriptManagerExecFile("scripts/overworldscene.js", NULL));
|
||||
errorChain(scriptManagerExecFile("scripts/init.js", NULL));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ void uiTextboxInit(
|
||||
box->maxLength = maxLength;
|
||||
box->lines = lines;
|
||||
box->linesMax = linesMax;
|
||||
box->glyphsBuiltForPage = -1;
|
||||
}
|
||||
|
||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||
@@ -43,6 +44,7 @@ void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||
box->scroll = 0;
|
||||
box->layoutWidth = 0.0f;
|
||||
box->layoutHeight = 0.0f;
|
||||
box->glyphsBuiltForPage = -1;
|
||||
}
|
||||
|
||||
void uiTextboxBuildLayout(
|
||||
@@ -52,6 +54,7 @@ void uiTextboxBuildLayout(
|
||||
) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
|
||||
box->glyphsBuiltForPage = -1;
|
||||
box->layoutWidth = width;
|
||||
box->layoutHeight = height;
|
||||
box->lineCount = 0;
|
||||
@@ -169,12 +172,30 @@ errorret_t uiTextboxDraw(
|
||||
uiTextboxBuildLayout(box, contentW, contentH);
|
||||
}
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(uiFrameDrawCached(&box->frameCache, x, y, width, height));
|
||||
|
||||
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;
|
||||
if(box->glyphsBuiltForPage != box->currentPage) {
|
||||
uiTextboxBuildPageGlyphs(box);
|
||||
}
|
||||
|
||||
if(box->glyphCount == 0) errorOk();
|
||||
|
||||
int32_t visibleCount = 0;
|
||||
while(
|
||||
visibleCount < box->glyphCount &&
|
||||
box->glyphs[visibleCount].revealAt <= box->scroll
|
||||
) visibleCount++;
|
||||
|
||||
if(visibleCount == 0) errorOk();
|
||||
|
||||
spritebatchsprite_t scratch[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||
for(int32_t i = 0; i < visibleCount; i++) {
|
||||
scratch[i] = spriteBatchSpriteTranslate(
|
||||
&box->glyphs[i].sprite, contentX, contentY
|
||||
);
|
||||
}
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
@@ -182,34 +203,47 @@ errorret_t uiTextboxDraw(
|
||||
.texture = FONT_DEFAULT.texture
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(scratch, visibleCount, &SHADER_UNLIT, material));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTextboxBuildPageGlyphs(uitextbox_t *box) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
|
||||
box->glyphCount = 0;
|
||||
|
||||
int32_t pageFirst = box->currentPage * box->linesPerPage;
|
||||
int32_t pageLast = pageFirst + box->linesPerPage;
|
||||
if(pageLast > box->lineCount) pageLast = box->lineCount;
|
||||
|
||||
int32_t charsLeft = box->scroll;
|
||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
|
||||
for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) {
|
||||
int32_t consumed = 0;
|
||||
for(int32_t li = pageFirst; li < pageLast; li++) {
|
||||
uitextboxline_t *line = &box->lines[li];
|
||||
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
|
||||
float_t lineY = contentY +
|
||||
float_t lineY =
|
||||
(float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
|
||||
|
||||
for(int32_t ci = 0; ci < visible; ci++) {
|
||||
for(int32_t ci = 0; ci < line->count; ci++) {
|
||||
consumed++;
|
||||
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;
|
||||
assertTrue(
|
||||
box->glyphCount < UI_TEXTBOX_PAGE_GLYPHS_MAX,
|
||||
"Textbox page produces too many glyphs"
|
||||
);
|
||||
uitextboxglyph_t *glyph = &box->glyphs[box->glyphCount++];
|
||||
glyph->sprite = textGetSprite(
|
||||
(vec2){ (float_t)ci * fontW, lineY }, c, &FONT_DEFAULT
|
||||
);
|
||||
glyph->revealAt = consumed;
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
box->glyphsBuiltForPage = box->currentPage;
|
||||
}
|
||||
|
||||
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
|
||||
|
||||
@@ -7,16 +7,34 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
||||
|
||||
// Fixed capacity for a page's cached glyph sprites (see uitextboxglyph_t).
|
||||
// Sized generously above UI_TEXTBOX_LINES_PER_PAGE_MAX worth of glyphs at
|
||||
// typical textbox widths; uiTextboxBuildPageGlyphs asserts if a page
|
||||
// somehow produces more than this.
|
||||
#define UI_TEXTBOX_PAGE_GLYPHS_MAX 512
|
||||
|
||||
typedef struct {
|
||||
int32_t start;
|
||||
int32_t count;
|
||||
} uitextboxline_t;
|
||||
|
||||
// A single visible glyph's cached sprite (relative to the textbox's
|
||||
// content origin, i.e. (0,0)) plus the scroll value at/after which it
|
||||
// becomes visible -- lets uiTextboxDraw turn the typewriter scroll into
|
||||
// a simple prefix-count instead of recomputing glyph geometry every
|
||||
// frame.
|
||||
typedef struct {
|
||||
spritebatchsprite_t sprite;
|
||||
int32_t revealAt;
|
||||
} uitextboxglyph_t;
|
||||
|
||||
typedef struct {
|
||||
char_t *text;
|
||||
uint32_t maxLength;
|
||||
@@ -34,6 +52,15 @@ typedef struct {
|
||||
|
||||
int32_t currentPage;
|
||||
int32_t scroll;
|
||||
|
||||
// Cached glyph sprites for the current page (see uitextboxglyph_t),
|
||||
// rebuilt only when currentPage no longer matches
|
||||
// glyphsBuiltForPage -- not every frame/scroll tick.
|
||||
uitextboxglyph_t glyphs[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||
int32_t glyphCount;
|
||||
int32_t glyphsBuiltForPage;
|
||||
|
||||
uiframecache_t frameCache;
|
||||
} uitextbox_t;
|
||||
|
||||
/**
|
||||
@@ -136,3 +163,13 @@ bool_t uiTextboxHasNextPage(const uitextbox_t *box);
|
||||
* @param box The textbox to advance.
|
||||
*/
|
||||
void uiTextboxNextPage(uitextbox_t *box);
|
||||
|
||||
/**
|
||||
* Rebuilds the cached glyph sprites (see uitextboxglyph_t) for the
|
||||
* current page from its line layout. Called automatically by
|
||||
* uiTextboxDraw whenever currentPage no longer matches the page the
|
||||
* cache was last built for.
|
||||
*
|
||||
* @param box The textbox to rebuild.
|
||||
*/
|
||||
void uiTextboxBuildPageGlyphs(uitextbox_t *box);
|
||||
|
||||
Reference in New Issue
Block a user