Rebuild UI as a scriptable element tree, archive the old system

The old UI system (X-macro static element list, hand-authored C
screens/widgets/focus stack) is archived under archive/ rather than
deleted, since it's a useful reference during the rewrite.

New native UI element pool (src/dusk/ui): a flat UI_ELEMENTS[128] pool
of tagged-union elements (Label, Rectangle, Scripted), a real
persistent parent/child tree (children[8] per element, cycle- and
capacity-checked uiElementSetParent), always-fresh worldX/worldY
(cheap enough to recompute on every read, no dirty-flag cache needed),
and cascading dispose. Rendering stays manual/immediate: a scripted
element with no render() override auto-renders its children by
default, but overriding render() takes full control (an override must
call renderChildren() itself to opt back in) -- this is deliberately
preserved end to end via a render()-shadow trampoline so overriding
render() always keeps working the same way regardless of how a node
is reached.

New scripting layer (src/dusk/script/module/ui): UIElement/Label/
Rectangle JS classes (Label/Rectangle share UIElement's prototype via
manual chaining, not JS `extends`), exposing x/y/worldX/worldY/parent/
add()/remove()/render()/renderChildren()/dispose(). UI.add()/
UI.remove() manage top-level render roots, mutually exclusive with
being someone's child.

Also: Scene gains a lateUpdate() hook (called once per frame after
every other update, for things like camera-follow that need to react
to where everything else ended up); several duskrpg call sites
(cutscene items, entityinteractable, entityplayer) that depended on
the now-archived RPG textbox are stubbed to console output instead of
a dialogue box, pending the new UI reaching that far.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 11:13:23 -05:00
parent 7b58addf7e
commit 7ee04c78cd
118 changed files with 2502 additions and 391 deletions
+18
View File
@@ -0,0 +1,18 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(debug)
add_subdirectory(frame)
add_subdirectory(focus)
add_subdirectory(overlay)
add_subdirectory(transition)
add_subdirectory(widget)
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
ui.c
uielement.c
)
+62
View File
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "ui.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "ui/uielement.h"
#include "ui/focus/uifocus.h"
ui_t UI;
errorret_t uiInit(void) {
memoryZero(&UI, sizeof(ui_t));
uiFocusInit();
uiElementsSort();
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementInit(element));
element++;
}
errorOk();
}
errorret_t uiUpdate(void) {
uiFocusUpdate();
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementUpdate(element));
element++;
}
errorOk();
}
errorret_t uiRender(void) {
const uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementDraw(element));
element++;
}
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiDispose(void) {
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementDispose(element));
element++;
}
errorOk();
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
void *nothing;
} ui_t;
extern ui_t UI;
/**
* Initializes the UI system.
*/
errorret_t uiInit(void);
/**
* Updates the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiUpdate(void);
/**
* Renders the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiRender(void);
/**
* Disposes of the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiDispose(void);
+86
View File
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uielement.h"
#include "assert/assert.h"
#include "util/sort.h"
#include "ui/frame/uiframe.h"
#include "ui/debug/uifps.h"
#include "engine/engine.h"
#include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h"
#include "ui/overlay/uicrop.h"
#include "ui/transition/uitransition.h"
#include "ui/debug/uiconsole.h"
#include "ui/frame/uiconfirm.h"
// Priming pass: X does nothing here, so this only exists to process any
// #include directives nested in uielementlist.h/ui/uielementgame.h at
// file scope (each such header's own #pragma once makes it a no-op on
// the real pass below, which happens inside UI_ELEMENTS[]'s braces,
// where a raw #include of a declaration would be invalid).
#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing
#include "uielementlist.h"
#undef X
uielement_t UI_ELEMENTS[] = {
#define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \
{ \
.init = initFn, .update = updateFn, .draw = drawFn, \
.dispose = disposeFn, .order = elementOrder \
},
#include "uielementlist.h"
#undef X
{ 0 } // Null terminator
};
bool_t uiElementIsNull(const uielement_t *element) {
return element->init == NULL &&
element->update == NULL &&
element->draw == NULL &&
element->dispose == NULL;
}
int_t uiElementCompareOrder(const void *a, const void *b) {
const uielement_t *elementA = (const uielement_t *)a;
const uielement_t *elementB = (const uielement_t *)b;
if(elementA->order < elementB->order) return -1;
if(elementA->order > elementB->order) return 1;
return 0;
}
void uiElementsSort(void) {
// The trailing null terminator is always the last static entry -- sort
// everything before it, and leave it in place.
const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1;
sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder);
}
errorret_t uiElementInit(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->init != NULL) errorChain(element->init());
errorOk();
}
errorret_t uiElementUpdate(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->update != NULL) errorChain(element->update());
errorOk();
}
errorret_t uiElementDraw(const uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->draw != NULL) errorChain(element->draw());
errorOk();
}
errorret_t uiElementDispose(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->dispose != NULL) errorChain(element->dispose());
errorOk();
}
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
// Built-in order tiers. Lower values update/render first; ties preserve
// their relative UI_ELEMENTS declaration order (uiElementsSort uses a
// stable sort). Game-specific elements can use any int32_t value -- these
// are just the ones the engine itself relies on.
#define UI_ELEMENT_ORDER_DEFAULT 0
#define UI_ELEMENT_ORDER_DEBUG 1000
typedef struct {
errorret_t (*init)();
errorret_t (*update)();
errorret_t (*draw)();
errorret_t (*dispose)();
int32_t order;
} uielement_t;
extern uielement_t UI_ELEMENTS[];
/**
* Returns true when all four callbacks on the element are NULL,
* which marks the end of the UI_ELEMENTS array.
*
* @param element The element to test.
* @returns True if the element is the null terminator.
*/
bool_t uiElementIsNull(const uielement_t *element);
/**
* Compares two elements by their .order field, ascending. Matches
* sortcompare_t, for use with the project's sort utilities.
*
* @param a First uielement_t to compare.
* @param b Second uielement_t to compare.
* @return Negative if a < b, zero if a == b, positive if a > b.
*/
int_t uiElementCompareOrder(const void *a, const void *b);
/**
* Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing
* null terminator is never moved. Called once by uiInit -- element order
* is static after that, so there's no need to re-sort every frame.
*/
void uiElementsSort(void);
/**
* Initializes a UI element, invoking its init callback if set.
*
* @param element The element to initialize.
* @return Any error that occurs.
*/
errorret_t uiElementInit(uielement_t *element);
/**
* Updates a UI element, calling its update callback if set.
*
* @param element The element to update.
* @return Any error that occurs.
*/
errorret_t uiElementUpdate(uielement_t *element);
/**
* Draws a UI element, calling its draw callback if set.
*
* @param element The element to render.
* @return Any error that occurs.
*/
errorret_t uiElementDraw(const uielement_t *element);
/**
* Disposes of a UI element, invoking its dispose callback if set.
*
* @param element The element to dispose.
* @return Any error that occurs.
*/
errorret_t uiElementDispose(uielement_t *element);
+17
View File
@@ -0,0 +1,17 @@
# 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
uibutton.c
uicheckbox.c
uitab.c
uislider.c
uidropdown.c
uiscrolling.c
uimenu.c
uilabel.c
uiwidgetlabel.c
)
+70
View File
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uilabel.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h"
void uiLabelInit(uilabel_t *label, font_t *font) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
memoryZero(label, sizeof(uilabel_t));
label->font = font;
label->color = COLOR_WHITE;
}
void uiLabelSetText(uilabel_t *label, const char_t *text) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(text, "Text cannot be NULL");
assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long");
stringCopy(label->text, text, UI_LABEL_TEXT_MAX);
label->spriteCount = textBuildSpriteCache(
label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX,
&label->width, &label->height
);
}
void uiLabelSetColor(uilabel_t *label, const color_t color) {
assertNotNull(label, "Label cannot be NULL");
label->color = color;
}
void uiLabelGetSize(
const uilabel_t *label,
int32_t *outWidth,
int32_t *outHeight
) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(outWidth, "Output width cannot be NULL");
assertNotNull(outHeight, "Output height cannot be NULL");
*outWidth = label->width;
*outHeight = label->height;
}
errorret_t uiLabelDraw(
const uilabel_t *label,
const float_t x,
const float_t y
) {
assertNotNull(label, "Label cannot be NULL");
if(label->spriteCount == 0) errorOk();
// Cached sprites are relative to (0,0); textDrawSpriteCache translates
// into the requested screen position here instead of in
// uiLabelSetText, so a label can be repositioned every frame without
// rebuilding the (much more expensive) glyph/UV cache.
spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX];
errorChain(textDrawSpriteCache(
label->sprites, label->spriteCount, scratch, x, y, label->color,
label->font->texture
));
errorOk();
}
+85
View File
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "display/text/font.h"
#include "display/spritebatch/spritebatchsprite.h"
#include "display/color.h"
#define UI_LABEL_TEXT_MAX 256
#define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX
typedef struct {
char_t text[UI_LABEL_TEXT_MAX];
color_t color;
font_t *font;
spritebatchsprite_t sprites[UI_LABEL_SPRITE_COUNT_MAX];
uint32_t spriteCount;
int32_t width;
int32_t height;
} uilabel_t;
/**
* Initializes a label, defaulting to white text and no text set.
*
* @param label The label to initialize.
* @param font Font to use for rendering. Must outlive the label.
*/
void uiLabelInit(uilabel_t *label, font_t *font);
/**
* Sets the label's text, rebuilding its cached sprites (glyph lookup, UV,
* and layout) immediately. This is the only way the sprite cache gets
* rebuilt -- uiLabelDraw never recomputes it, so call this whenever the
* text changes rather than once up front and expecting it to stay in sync.
*
* @param label The label to update.
* @param text Null-terminated string to display. Must be shorter than
* UI_LABEL_TEXT_MAX.
*/
void uiLabelSetText(uilabel_t *label, const char_t *text);
/**
* Sets the label's tint color. Cheap -- doesn't touch the sprite cache,
* since color is applied via the draw material, not baked into sprites.
*
* @param label The label to update.
* @param color The new tint color.
*/
void uiLabelSetColor(uilabel_t *label, const color_t color);
/**
* Gets the measured size (in pixels) of the label's current text, cached
* from the last uiLabelSetText call.
*
* @param label The label to query.
* @param outWidth Pointer to store the width.
* @param outHeight Pointer to store the height.
*/
void uiLabelGetSize(
const uilabel_t *label,
int32_t *outWidth,
int32_t *outHeight
);
/**
* Draws the label's cached sprites at the given screen position in a
* single batched buffer call. Does not recompute glyph layout -- call
* uiLabelSetText first whenever the text changes.
*
* @param label The label to draw.
* @param x Screen x position.
* @param y Screen y position.
* @return Any error that occurs.
*/
errorret_t uiLabelDraw(
const uilabel_t *label,
const float_t x,
const float_t y
);
+18 -7
View File
@@ -4,22 +4,26 @@ var TestPlane = require('./TestPlane.js');
class OverworldScene {
constructor() {
this.time = 0;
}
init() {
this.player = new Player();
this.plane = new TestPlane();
this.camera = new PlayerCamera(this.player);
this.rect = new Rectangle();
this.rect.color = { r: 255, g: 0, b: 0, a: 255 };
this.rect.width = 100;
this.rect.height = 100;
UI.add(this.rect);
this.label = new Label();
this.label.text = 'Hello World!';
this.rect.add(this.label);
}
update() {
lateUpdate() {
this.camera.update();
this.time += Time.delta;
if(this.time > 3.0) {
Scene.set(new OverworldScene());
}
}
dispose() {
@@ -27,9 +31,16 @@ class OverworldScene {
this.player.dispose();
this.plane.dispose();
// rect.dispose() cascades to label (added as its child) and removes
// itself from UI's render roots -- no separate UI.remove()/dispose()
// needed for either.
this.rect.dispose();
this.camera = null;
this.player = null;
this.plane = null;
this.label = null;
this.rect = null;
}
}
+1
View File
@@ -80,6 +80,7 @@ errorret_t displayUpdate(void) {
);
errorChain(sceneRender());
errorChain(uiRender());
// Finish up
screenUnbind();
+1
View File
@@ -90,6 +90,7 @@ errorret_t engineUpdate(void) {
errorChain(sceneUpdate());
errorChain(assetUpdate());
errorChain(uiUpdate());
errorChain(moduleSceneLateUpdateCurrent());
}
// Render
-36
View File
@@ -14,7 +14,6 @@
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entityrenderable.h"
#include "physics/triggersystem.h"
#include "ui/ui.h"
#include "console/console.h"
scenemanager_t SCENE_MANAGER;
@@ -148,41 +147,6 @@ errorret_t sceneRender(void) {
}
}
// Screen-space matrices for UI rendering.
mat4 screenIdentity;
mat4 screenProj;
mat4 screenView;
glm_mat4_identity(screenIdentity);
glm_ortho(
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
0.1f, 100.0f,
screenProj
);
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
screenView
);
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_MODEL, screenIdentity
));
errorChain(shaderSetMatrix(
&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, screenProj
));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, screenView));
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
errorChain(uiRender());
errorOk();
}
+9
View File
@@ -16,6 +16,9 @@
#include "script/module/scene/modulescene.h"
#include "script/module/console/moduleconsole.h"
#include "script/module/input/moduleinput.h"
#include "script/module/ui/moduleuielement.h"
#include "script/module/ui/modulelabel.h"
#include "script/module/ui/modulerectangle.h"
#include "script/module/ui/moduleui.h"
void moduleListInit(void) {
@@ -25,6 +28,9 @@ void moduleListInit(void) {
moduleTimeInit();
moduleConsoleInit();
moduleInputInit();
moduleUiElementInit();
moduleLabelInit();
moduleRectangleInit();
moduleUiInit();
moduleComponentInit();
moduleComponentListInit();
@@ -38,6 +44,9 @@ void moduleListDispose(void) {
moduleComponentListDispose();
moduleComponentDispose();
moduleUiDispose();
moduleRectangleDispose();
moduleLabelDispose();
moduleUiElementDispose();
moduleInputDispose();
moduleConsoleDispose();
moduleTimeDispose();
+3 -2
View File
@@ -9,8 +9,9 @@
/**
* Registers every script module (Component + its typed per-type
* wrappers, Entity, Scene, require(), Mesh, Time, Console, Input, UI,
* the platform globals). Called once by scriptManagerInit().
* wrappers, Entity, Scene, require(), Mesh, Time, Console, Input,
* UIElement + Label/Rectangle + UI, the platform globals). Called once
* by scriptManagerInit().
*/
void moduleListInit(void);
@@ -160,3 +160,22 @@ errorret_t moduleSceneUpdateCurrent(void) {
errorChain(ret);
errorOk();
}
errorret_t moduleSceneLateUpdateCurrent(void) {
if(MODULE_SCENE_CURRENT_ID == SCENE_ID_INVALID) errorOk();
jerry_value_t lateUpdateFn = moduleBaseGetProp(
MODULE_SCENE_CURRENT, "lateUpdate"
);
if(!jerry_value_is_function(lateUpdateFn)) {
jerry_value_free(lateUpdateFn);
errorOk();
}
errorret_t ret = scriptManagerCallValue(
MODULE_SCENE_CURRENT, lateUpdateFn, "Scene module", "lateUpdate"
);
jerry_value_free(lateUpdateFn);
errorChain(ret);
errorOk();
}
@@ -59,3 +59,16 @@ void moduleSceneTeardownCurrent(void);
* throws, or if it returns a rejected promise.
*/
errorret_t moduleSceneUpdateCurrent(void);
/**
* Calls lateUpdate() 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(), after every other
* update (entities, physics, UI, etc.) has already run -- useful for
* things like camera follow that need to react to where everything
* else ended up this frame, rather than where it was at the start.
*
* @return The error return value. An error is thrown if lateUpdate()
* itself throws, or if it returns a rejected promise.
*/
errorret_t moduleSceneLateUpdateCurrent(void);
+3
View File
@@ -5,5 +5,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
moduleuielement.c
modulelabel.c
modulerectangle.c
moduleui.c
)
+55
View File
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulelabel.h"
#include "moduleuielement.h"
#include "script/module/modulebase.h"
#include "ui/widget/uilabel.h"
static scriptproto_t MODULE_LABEL_PROTO;
moduleBaseFunction(moduleLabelConstructor) {
errorret_t ret = moduleUiElementConstructShared(
callInfo->this_value, UI_ELEMENT_TYPE_LABEL
);
if(errorIsNotOk(ret)) return moduleBaseThrowError(ret);
return jerry_undefined();
}
moduleBaseFunction(moduleLabelGetText) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_string_sz(UI_ELEMENTS[h->id].label.text);
}
moduleBaseFunction(moduleLabelSetText) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleBaseRequireArgs(1); moduleBaseRequireString(0);
char_t buffer[UI_LABEL_TEXT_MAX];
moduleBaseToString(args[0], buffer, sizeof(buffer));
uiLabelSetText(h->id, buffer);
return jerry_undefined();
}
void moduleLabelInit(void) {
scriptProtoInit(
&MODULE_LABEL_PROTO,
"Label",
sizeof(moduleuielementhandle_t),
moduleLabelConstructor
);
jerry_object_set_proto(
MODULE_LABEL_PROTO.prototype, MODULE_UI_ELEMENT_PROTO.prototype
);
scriptProtoDefineProp(
&MODULE_LABEL_PROTO, "text", moduleLabelGetText, moduleLabelSetText
);
}
void moduleLabelDispose(void) {
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
/**
* Registers the Label class: `new Label()` allocates a
* UI_ELEMENT_TYPE_LABEL pool element. Its prototype chains to
* UIElement.prototype (see moduleuielement.h), so x/y/worldX/worldY/
* parent/render/dispose all work on a Label unmodified; Label itself
* only adds a `text` property (read/write).
*/
void moduleLabelInit(void);
/**
* Disposes the Label class's script resources.
*/
void moduleLabelDispose(void);
+120
View File
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulerectangle.h"
#include "moduleuielement.h"
#include "script/module/modulebase.h"
#include "ui/widget/uirectangle.h"
#include "display/color.h"
static scriptproto_t MODULE_RECTANGLE_PROTO;
static jerry_value_t moduleRectangleColorToObject(const color_t c) {
jerry_value_t obj = jerry_object();
moduleBaseObjectSetNumber(obj, "r", c.r);
moduleBaseObjectSetNumber(obj, "g", c.g);
moduleBaseObjectSetNumber(obj, "b", c.b);
moduleBaseObjectSetNumber(obj, "a", c.a);
return obj;
}
static color_t moduleRectangleColorFromObject(const jerry_value_t obj) {
color_t c = COLOR_WHITE;
jerry_value_t rVal = moduleBaseGetProp(obj, "r");
jerry_value_t gVal = moduleBaseGetProp(obj, "g");
jerry_value_t bVal = moduleBaseGetProp(obj, "b");
jerry_value_t aVal = moduleBaseGetProp(obj, "a");
if(jerry_value_is_number(rVal)) c.r = (colorchannel8_t)moduleBaseValueInt(rVal);
if(jerry_value_is_number(gVal)) c.g = (colorchannel8_t)moduleBaseValueInt(gVal);
if(jerry_value_is_number(bVal)) c.b = (colorchannel8_t)moduleBaseValueInt(bVal);
if(jerry_value_is_number(aVal)) c.a = (colorchannel8_t)moduleBaseValueInt(aVal);
jerry_value_free(rVal);
jerry_value_free(gVal);
jerry_value_free(bVal);
jerry_value_free(aVal);
return c;
}
moduleBaseFunction(moduleRectangleConstructor) {
errorret_t ret = moduleUiElementConstructShared(
callInfo->this_value, UI_ELEMENT_TYPE_RECTANGLE
);
if(errorIsNotOk(ret)) return moduleBaseThrowError(ret);
return jerry_undefined();
}
moduleBaseFunction(moduleRectangleGetWidth) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_number(UI_ELEMENTS[h->id].rectangle.width);
}
moduleBaseFunction(moduleRectangleSetWidth) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
uirectangle_t *rect = &UI_ELEMENTS[h->id].rectangle;
uiRectangleSetSize(h->id, moduleBaseArgFloat(0), rect->height);
return jerry_undefined();
}
moduleBaseFunction(moduleRectangleGetHeight) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_number(UI_ELEMENTS[h->id].rectangle.height);
}
moduleBaseFunction(moduleRectangleSetHeight) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
uirectangle_t *rect = &UI_ELEMENTS[h->id].rectangle;
uiRectangleSetSize(h->id, rect->width, moduleBaseArgFloat(0));
return jerry_undefined();
}
moduleBaseFunction(moduleRectangleGetColor) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return moduleRectangleColorToObject(UI_ELEMENTS[h->id].rectangle.color);
}
moduleBaseFunction(moduleRectangleSetColor) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
uiRectangleSetColor(h->id, moduleRectangleColorFromObject(args[0]));
return jerry_undefined();
}
void moduleRectangleInit(void) {
scriptProtoInit(
&MODULE_RECTANGLE_PROTO,
"Rectangle",
sizeof(moduleuielementhandle_t),
moduleRectangleConstructor
);
jerry_object_set_proto(
MODULE_RECTANGLE_PROTO.prototype, MODULE_UI_ELEMENT_PROTO.prototype
);
scriptProtoDefineProp(
&MODULE_RECTANGLE_PROTO, "width",
moduleRectangleGetWidth, moduleRectangleSetWidth
);
scriptProtoDefineProp(
&MODULE_RECTANGLE_PROTO, "height",
moduleRectangleGetHeight, moduleRectangleSetHeight
);
scriptProtoDefineProp(
&MODULE_RECTANGLE_PROTO, "color",
moduleRectangleGetColor, moduleRectangleSetColor
);
}
void moduleRectangleDispose(void) {
}
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
/**
* Registers the Rectangle class: `new Rectangle()` allocates a
* UI_ELEMENT_TYPE_RECTANGLE pool element. Its prototype chains to
* UIElement.prototype (see moduleuielement.h), so x/y/worldX/worldY/
* parent/render/dispose all work on a Rectangle unmodified; Rectangle
* itself adds width/height (read/write, in screen space) and color
* (read/write, a plain {r,g,b,a} object, 0-255 per channel).
*/
void moduleRectangleInit(void);
/**
* Disposes the Rectangle class's script resources.
*/
void moduleRectangleDispose(void);
+17 -35
View File
@@ -6,50 +6,32 @@
*/
#include "moduleui.h"
#include "moduleuielement.h"
#include "script/module/modulebase.h"
#include "console/console.h"
#include "ui/ui.h"
// Stub: stringify "<name> <args...>" (like Console.print) and forward to
// consolePrint(), so UI.add/UI.remove are visibly wired up before there's
// any real UI system behind them.
static jerry_value_t moduleUiLogCall(
const char_t *name,
const jerry_value_t args[],
const jerry_length_t argc
) {
char_t buffer[CONSOLE_LINE_MAX];
size_t pos = strlen(name);
if(pos >= CONSOLE_LINE_MAX) pos = CONSOLE_LINE_MAX - 1;
memoryCopy(buffer, name, pos);
moduleBaseFunction(moduleUiAddFn) {
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
for(jerry_length_t i = 0; i < argc; i++) {
jerry_value_t strVal = jerry_value_to_string(args[i]);
if(jerry_value_is_exception(strVal)) return strVal;
moduleuielementhandle_t *h = moduleUiElementGetFromValue(args[0]);
if(!h) return moduleBaseThrow("Expected a UIElement");
if(pos + 1 < CONSOLE_LINE_MAX) buffer[pos++] = ' ';
jerry_size_t written = jerry_string_to_buffer(
strVal, JERRY_ENCODING_UTF8,
(jerry_char_t *)(buffer + pos), (jerry_size_t)(CONSOLE_LINE_MAX - pos - 1)
);
jerry_value_free(strVal);
pos += written;
}
buffer[pos] = '\0';
// Fixed "%s" format -- buffer is arbitrary JS-provided text and must
// never be used as the format string itself.
consolePrint("%s", buffer);
// A root and a child are mutually exclusive -- promoting an existing
// child to a root detaches it from its parent first.
uiElementSetParent(h->id, UI_ELEMENT_ID_INVALID);
if(!uiRootAdd(h->id)) return moduleBaseThrow("UI root capacity exceeded");
return jerry_undefined();
}
moduleBaseFunction(moduleUiAddFn) {
return moduleUiLogCall("UI.add", args, argc);
}
moduleBaseFunction(moduleUiRemoveFn) {
return moduleUiLogCall("UI.remove", args, argc);
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
moduleuielementhandle_t *h = moduleUiElementGetFromValue(args[0]);
if(!h) return moduleBaseThrow("Expected a UIElement");
uiRootRemove(h->id);
return jerry_undefined();
}
void moduleUiInit(void) {
+7 -4
View File
@@ -8,10 +8,13 @@
#pragma once
/**
* Registers the global `UI` object, exposing `UI.add(...)` and
* `UI.remove(...)`. Both are stubs for now -- they stringify and
* space-join their arguments (like Console.print) and forward the
* result to consolePrint(), prefixed with the called method's name.
* Registers the global `UI` object, exposing `UI.add(element)` and
* `UI.remove(element)`. Both take a UIElement (or Label/Rectangle/etc.)
* instance and add/remove it from the engine's render roots (UI.root[]
* in ui/ui.h) -- root elements get their render() called once per frame
* by uiRender(); everything else only renders as part of a root's
* children (see UIElement.add()). A root and a child are mutually
* exclusive: UI.add() detaches the element from any parent first.
*/
void moduleUiInit(void);
+361
View File
@@ -0,0 +1,361 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleuielement.h"
#include "script/module/modulebase.h"
#include "script/scriptmanager.h"
#include "util/memory.h"
scriptproto_t MODULE_UI_ELEMENT_PROTO;
// Registered JS instance per pool slot, so the SCRIPTED render callback
// and the parent property can call back into whatever JS object owns a
// given element id. Indices with no registered instance hold undefined.
static jerry_value_t UI_ELEMENT_JS_INSTANCES[UI_ELEMENT_COUNT_MAX];
// Own (instance, not prototype) property that a construction-time
// override wrap stashes the (sub)class's real render() under -- see
// moduleUiElementConstructShared and moduleUiElementRenderScripted.
#define MODULE_UI_ELEMENT_USER_RENDER_PROP "__uiElementUserRender"
static bool_t moduleUiElementStrictEqual(
const jerry_value_t a,
const jerry_value_t b
) {
jerry_value_t result = jerry_binary_op(JERRY_BIN_OP_STRICT_EQUAL, a, b);
bool_t equal = jerry_value_is_true(result);
jerry_value_free(result);
return equal;
}
static jerry_value_t moduleUiElementRenderMethod(
const jerry_call_info_t *callInfo,
const jerry_value_t args[],
const jerry_length_t argc
);
// UI_ELEMENT_TYPE_SCRIPTED's render callback: calls the (sub)class's own
// render(), stashed at construction time under
// MODULE_UI_ELEMENT_USER_RENDER_PROP -- see moduleUiElementConstructShared.
// If the instance never had one stashed (it never overrode
// UIElement.prototype's own (native) render in the first place), the
// default behavior is to auto-render whatever was added() -- an override
// takes full manual control and must call renderChildren() itself if it
// still wants added children drawn.
static void moduleUiElementRenderScripted(uielement_t *element) {
jerry_value_t instance = UI_ELEMENT_JS_INSTANCES[element->id];
jerry_value_t renderFn = jerry_value_is_object(instance)
? moduleBaseGetProp(instance, MODULE_UI_ELEMENT_USER_RENDER_PROP)
: jerry_undefined();
if(jerry_value_is_function(renderFn)) {
errorret_t ret = scriptManagerCallValue(
instance, renderFn, "UIElement", "render"
);
errorCatch(errorPrint(ret));
} else {
uiElementRenderChildren(element);
}
jerry_value_free(renderFn);
}
// Calling instance.render() (JS method-call syntax) only reaches native
// code if the property lookup actually resolves to
// UIElement.prototype.render -- if a (sub)class overrides render(), that
// override shadows it and callers bypass uiElementRender() entirely,
// which is what actually tracks parent/worldX/worldY. So if this
// instance's resolved render is anything other than the base one, stash
// the override under MODULE_UI_ELEMENT_USER_RENDER_PROP and shadow
// render itself (as an own property, just on this instance) with the
// native trampoline -- moduleUiElementRenderScripted then calls the
// stashed override from inside uiElementRender(), fully wired up.
static void moduleUiElementWrapUserRenderIfOverridden(
const jerry_value_t thisValue
) {
jerry_value_t resolvedRender = moduleBaseGetProp(thisValue, "render");
jerry_value_t baseRender = moduleBaseGetProp(
MODULE_UI_ELEMENT_PROTO.prototype, "render"
);
if(
jerry_value_is_function(resolvedRender) &&
!moduleUiElementStrictEqual(resolvedRender, baseRender)
) {
jerry_value_t userRenderKey = jerry_string_sz(
MODULE_UI_ELEMENT_USER_RENDER_PROP
);
jerry_object_set(thisValue, userRenderKey, resolvedRender);
jerry_value_free(userRenderKey);
jerry_value_t trampoline = jerry_function_external(
moduleUiElementRenderMethod
);
jerry_value_t renderKey = jerry_string_sz("render");
jerry_object_set(thisValue, renderKey, trampoline);
jerry_value_free(renderKey);
jerry_value_free(trampoline);
}
jerry_value_free(baseRender);
jerry_value_free(resolvedRender);
}
errorret_t moduleUiElementConstructShared(
const jerry_value_t thisValue,
const uielementtype_t type
) {
moduleuielementhandle_t *inst = (moduleuielementhandle_t *)memoryAllocate(
sizeof(moduleuielementhandle_t)
);
inst->id = uiElementGetAvailable();
uiElementInit(inst->id, type);
jerry_object_set_native_ptr(
thisValue, &MODULE_UI_ELEMENT_PROTO.info, inst
);
UI_ELEMENT_JS_INSTANCES[inst->id] = jerry_value_copy(thisValue);
moduleUiElementWrapUserRenderIfOverridden(thisValue);
jerry_value_t initFn = moduleBaseGetProp(thisValue, "init");
if(jerry_value_is_function(initFn)) {
errorret_t ret = scriptManagerCallValue(
thisValue, initFn, "UIElement", "init"
);
jerry_value_free(initFn);
errorChain(ret);
errorOk();
}
jerry_value_free(initFn);
errorOk();
}
moduleBaseFunction(moduleUiElementConstructor) {
errorret_t ret = moduleUiElementConstructShared(
callInfo->this_value, UI_ELEMENT_TYPE_SCRIPTED
);
if(errorIsNotOk(ret)) return moduleBaseThrowError(ret);
return jerry_undefined();
}
moduleuielementhandle_t *moduleUiElementGetFromValue(const jerry_value_t val) {
return (moduleuielementhandle_t *)scriptProtoGetValue(
&MODULE_UI_ELEMENT_PROTO, val
);
}
moduleuielementhandle_t *moduleUiElementGet(
const jerry_call_info_t *callInfo
) {
return moduleUiElementGetFromValue(callInfo->this_value);
}
jerry_value_t moduleUiElementGetInstance(const uielementid_t elementId) {
return UI_ELEMENT_JS_INSTANCES[elementId];
}
moduleBaseFunction(moduleUiElementGetId) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_number(h->id);
}
moduleBaseFunction(moduleUiElementGetX) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_number(UI_ELEMENTS[h->id].x);
}
moduleBaseFunction(moduleUiElementSetX) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
UI_ELEMENTS[h->id].x = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleUiElementGetY) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_number(UI_ELEMENTS[h->id].y);
}
moduleBaseFunction(moduleUiElementSetY) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
UI_ELEMENTS[h->id].y = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleUiElementGetWorldX) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
uiElementUpdateWorld(&UI_ELEMENTS[h->id]);
return jerry_number(UI_ELEMENTS[h->id].worldX);
}
moduleBaseFunction(moduleUiElementGetWorldY) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
uiElementUpdateWorld(&UI_ELEMENTS[h->id]);
return jerry_number(UI_ELEMENTS[h->id].worldY);
}
moduleBaseFunction(moduleUiElementGetParent) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
uielementid_t parentId = UI_ELEMENTS[h->id].parent;
if(parentId == UI_ELEMENT_ID_INVALID) return jerry_undefined();
jerry_value_t parentInstance = moduleUiElementGetInstance(parentId);
if(!jerry_value_is_object(parentInstance)) return jerry_undefined();
return jerry_value_copy(parentInstance);
}
moduleBaseFunction(moduleUiElementGetChildCount) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
return jerry_number(UI_ELEMENTS[h->id].childCount);
}
moduleBaseFunction(moduleUiElementAddMethod) {
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleuielementhandle_t *childH = moduleUiElementGetFromValue(args[0]);
if(!childH) return moduleBaseThrow("UIElement.add: expected a UIElement");
if(childH->id == h->id) {
return moduleBaseThrow("UIElement.add: cannot add an element to itself");
}
if(uiElementIsAncestorOf(childH->id, h->id)) {
return moduleBaseThrow("UIElement.add: would create a cycle");
}
if(!uiElementSetParent(childH->id, h->id)) {
return moduleBaseThrow("UIElement.add: child capacity exceeded");
}
return jerry_value_copy(args[0]);
}
moduleBaseFunction(moduleUiElementRemoveMethod) {
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
moduleuielementhandle_t *childH = moduleUiElementGetFromValue(args[0]);
if(!childH) return moduleBaseThrow("UIElement.remove: expected a UIElement");
if(UI_ELEMENTS[childH->id].parent == h->id) {
uiElementSetParent(childH->id, UI_ELEMENT_ID_INVALID);
}
return jerry_undefined();
}
moduleBaseFunction(moduleUiElementRenderChildrenMethod) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
uiElementRenderChildren(&UI_ELEMENTS[h->id]);
return jerry_undefined();
}
moduleBaseFunction(moduleUiElementRenderMethod) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
uiElementRender(h->id);
return jerry_undefined();
}
// Frees the registered JS instance for elementId and every descendant
// (walked via the still-intact children[]/childCount), so the registry
// never points at an about-to-be-disposed id. Must run BEFORE
// uiElementDisposeDeep, which zeroes each struct as it goes.
static void moduleUiElementReleaseInstanceTree(const uielementid_t elementId) {
uielement_t *element = &UI_ELEMENTS[elementId];
jerry_value_t instance = UI_ELEMENT_JS_INSTANCES[elementId];
if(jerry_value_is_object(instance)) jerry_value_free(instance);
UI_ELEMENT_JS_INSTANCES[elementId] = jerry_undefined();
for(uint8_t i = 0; i < element->childCount; i++) {
moduleUiElementReleaseInstanceTree(element->children[i]);
}
}
moduleBaseFunction(moduleUiElementDisposeMethod) {
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
// Idempotency: this id's registry slot must still be this exact
// instance. If it's not (already disposed, or the pool slot was
// reused by something else entirely), there's nothing to do -- this
// is what makes cascading dispose safe to call redundantly, e.g. a
// script that still manually disposes a child after its parent
// already cascaded to it.
if(
!moduleUiElementStrictEqual(
UI_ELEMENT_JS_INSTANCES[h->id], callInfo->this_value
)
) {
return jerry_undefined();
}
moduleUiElementReleaseInstanceTree(h->id);
uiElementDisposeDeep(h->id);
return jerry_undefined();
}
void moduleUiElementInit(void) {
for(uielementid_t i = 0; i < UI_ELEMENT_COUNT_MAX; i++) {
UI_ELEMENT_JS_INSTANCES[i] = jerry_undefined();
}
scriptProtoInit(
&MODULE_UI_ELEMENT_PROTO,
"UIElement",
sizeof(moduleuielementhandle_t),
moduleUiElementConstructor
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "id", moduleUiElementGetId, NULL
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "x", moduleUiElementGetX, moduleUiElementSetX
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "y", moduleUiElementGetY, moduleUiElementSetY
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "worldX", moduleUiElementGetWorldX, NULL
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "worldY", moduleUiElementGetWorldY, NULL
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "parent", moduleUiElementGetParent, NULL
);
scriptProtoDefineProp(
&MODULE_UI_ELEMENT_PROTO, "childCount", moduleUiElementGetChildCount, NULL
);
scriptProtoDefineFunc(
&MODULE_UI_ELEMENT_PROTO, "add", moduleUiElementAddMethod
);
scriptProtoDefineFunc(
&MODULE_UI_ELEMENT_PROTO, "remove", moduleUiElementRemoveMethod
);
scriptProtoDefineFunc(
&MODULE_UI_ELEMENT_PROTO, "renderChildren", moduleUiElementRenderChildrenMethod
);
scriptProtoDefineFunc(
&MODULE_UI_ELEMENT_PROTO, "render", moduleUiElementRenderMethod
);
scriptProtoDefineFunc(
&MODULE_UI_ELEMENT_PROTO, "dispose", moduleUiElementDisposeMethod
);
UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_SCRIPTED] = (uilelementcallbacks_t){
.render = moduleUiElementRenderScripted
};
}
void moduleUiElementDispose(void) {
for(uielementid_t i = 0; i < UI_ELEMENT_COUNT_MAX; i++) {
if(jerry_value_is_object(UI_ELEMENT_JS_INSTANCES[i])) {
jerry_value_free(UI_ELEMENT_JS_INSTANCES[i]);
UI_ELEMENT_JS_INSTANCES[i] = jerry_undefined();
}
}
}
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "script/scriptproto.h"
#include "ui/uielement.h"
#include <jerryscript.h>
/** Native data wrapped by a JS UIElement instance (and every class built
* on top of it -- Label, Rectangle, etc. share this exact handle shape
* and store it under MODULE_UI_ELEMENT_PROTO's own native-pointer tag,
* not one of their own, so UIElement's x/y/worldX/worldY/parent/render
* accessors work on them unmodified via the prototype chain. */
typedef struct {
uielementid_t id;
} moduleuielementhandle_t;
extern scriptproto_t MODULE_UI_ELEMENT_PROTO;
/**
* Registers the UIElement class: `new UIElement()` allocates a
* UI_ELEMENT_TYPE_SCRIPTED pool element and, if the (sub)class defines
* an init() method, calls it immediately. Exposes id (read-only), x/y
* (read/write), worldX/worldY (read-only, always current -- no dirty-flag
* cache, just cheap to recompute), parent (read-only, the UIElement this
* one was add()ed to, or undefined), childCount (read-only), add(child)/
* remove(child) (persistent parent/child links, up to
* UI_ELEMENT_CHILDREN_MAX per element), render(), renderChildren()
* (renders whatever was added, in add-order -- called automatically by
* render() unless a subclass overrides render(), in which case the
* override has full manual control and must call this itself if it
* still wants added children drawn), and dispose() (cascades to every
* descendant).
*/
void moduleUiElementInit(void);
/**
* Disposes the UIElement class's script resources.
*/
void moduleUiElementDispose(void);
/**
* Internal. Gets the native handle wrapped by a UIElement (or
* Label/Rectangle/etc.) instance's `this` value.
*
* @param callInfo The JS call info, whose this_value is the instance.
* @return The wrapped handle, or NULL if this_value isn't a UIElement.
*/
moduleuielementhandle_t *moduleUiElementGet(const jerry_call_info_t *callInfo);
/**
* Internal. Same as moduleUiElementGet, but for a plain JS value rather
* than a call's this_value -- e.g. an argument passed to UI.add().
*
* @param val The JS value to inspect.
* @return The wrapped handle, or NULL if val isn't a UIElement.
*/
moduleuielementhandle_t *moduleUiElementGetFromValue(const jerry_value_t val);
/**
* Internal. Shared construction logic for UIElement and every class
* built on top of it (Label, Rectangle, ...): allocates a pool element
* of the given type, attaches the native handle to thisValue under
* MODULE_UI_ELEMENT_PROTO's tag, registers thisValue as the element's
* JS instance (see moduleUiElementGetInstance), and calls init() on
* thisValue if one is defined.
*
* @param thisValue The new instance (a call's this_value).
* @param type The UI element type to allocate.
* @return The error return value; an error is thrown if init() itself
* throws, or if it returns a rejected promise.
*/
errorret_t moduleUiElementConstructShared(
const jerry_value_t thisValue,
const uielementtype_t type
);
/**
* Internal. Gets the JS instance registered for a UI element id by
* moduleUiElementConstructShared, e.g. so a parent-lookup or the
* UI_ELEMENT_TYPE_SCRIPTED render callback can call back into it.
*
* @param elementId The element id to look up.
* @return The JS instance, or undefined if none is registered (e.g. a
* native-only element that was never constructed from script).
*/
jerry_value_t moduleUiElementGetInstance(const uielementid_t elementId);
+2 -7
View File
@@ -1,13 +1,8 @@
# Copyright (c) 2026 Dominic Masters
#
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(debug)
add_subdirectory(frame)
add_subdirectory(focus)
add_subdirectory(overlay)
add_subdirectory(transition)
add_subdirectory(widget)
# Sources
@@ -15,4 +10,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
ui.c
uielement.c
)
)
+75 -29
View File
@@ -1,51 +1,102 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "ui.h"
#include "time/time.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "display/spritebatch/spritebatch.h"
#include "display/display.h"
#include "display/displaystate.h"
#include "display/screen/screen.h"
#include "ui/uielement.h"
#include "ui/focus/uifocus.h"
#include "display/shader/shaderunlit.h"
#include "display/spritebatch/spritebatch.h"
#include "console/console.h"
ui_t UI;
errorret_t uiInit(void) {
memoryZero(&UI, sizeof(ui_t));
uiFocusInit();
uiElementsSort();
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementInit(element));
element++;
bool_t uiRootAdd(const uielementid_t elementId) {
for(uint8_t i = 0; i < UI_ROOT_MAX; i++) {
if(UI.root[i] != UI_ELEMENT_ID_INVALID) continue;
UI.root[i] = elementId;
return true;
}
return false;
}
void uiRootRemove(const uielementid_t elementId) {
for(uint8_t i = 0; i < UI_ROOT_MAX; i++) {
if(UI.root[i] != elementId) continue;
UI.root[i] = UI_ELEMENT_ID_INVALID;
return;
}
}
errorret_t uiInit(void) {
memoryZero(UI_ELEMENTS, sizeof(UI_ELEMENTS));
memoryZero(&UI, sizeof(UI));
for(uint8_t i = 0; i < UI_ROOT_MAX; i++) UI.root[i] = UI_ELEMENT_ID_INVALID;
consolePrint(
"UI elements size: %zu bytes (%.2f KB), %zu bytes/element, "
"%zu bytes/element for children[%d]",
sizeof(UI_ELEMENTS), sizeof(UI_ELEMENTS) / 1024.0f,
sizeof(uielement_t),
sizeof(uielementid_t) * UI_ELEMENT_CHILDREN_MAX, UI_ELEMENT_CHILDREN_MAX
);
errorOk();
}
errorret_t uiUpdate(void) {
uiFocusUpdate();
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) errorOk();
#endif
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementUpdate(element));
element++;
}
errorOk();
}
errorret_t uiRender(void) {
const uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementDraw(element));
element++;
// UI draws in screen space -- bind an orthographic projection looking
// straight at the screen, independent of whatever 3D projection/view
// the scene left bound.
mat4 screenIdentity;
mat4 screenProj;
mat4 screenView;
glm_mat4_identity(screenIdentity);
glm_ortho(
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
0.1f, 100.0f,
screenProj
);
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
screenView
);
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, screenIdentity));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, screenProj));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, screenView));
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
for(uint8_t i = 0; i < UI_ROOT_MAX; i++) {
if(UI.root[i] == UI_ELEMENT_ID_INVALID) continue;
uiElementRender(UI.root[i]);
}
errorChain(spriteBatchFlush());
@@ -53,10 +104,5 @@ errorret_t uiRender(void) {
}
errorret_t uiDispose(void) {
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementDispose(element));
element++;
}
errorOk();
}
}
+27 -5
View File
@@ -1,21 +1,42 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "uielement.h"
#define UI_ROOT_MAX 32
typedef struct {
void *nothing;
uielementid_t root[UI_ROOT_MAX];
} ui_t;
extern ui_t UI;
/**
* Adds an element to UI's render roots, in the first free slot.
*
* @param elementId The element id to add as a root.
* @return True on success, false if UI.root[] is already full.
*/
bool_t uiRootAdd(const uielementid_t elementId);
/**
* Removes an element from UI's render roots, if present. No-op if
* elementId isn't currently a root.
*
* @param elementId The element id to remove.
*/
void uiRootRemove(const uielementid_t elementId);
/**
* Initializes the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiInit(void);
@@ -27,8 +48,9 @@ errorret_t uiInit(void);
errorret_t uiUpdate(void);
/**
* Renders the UI system.
*
* Renders the UI system. Unlike uiUpdate(), this runs every real frame
* unconditionally, regardless of the fixed-timestep cadence.
*
* @return Any error that occurs.
*/
errorret_t uiRender(void);
@@ -38,4 +60,4 @@ errorret_t uiRender(void);
*
* @return Any error that occurs.
*/
errorret_t uiDispose(void);
errorret_t uiDispose(void);
+13
View File
@@ -0,0 +1,13 @@
/**
* 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_ELEMENT_ID_INVALID 0xFFFF
typedef uint_fast16_t uielementid_t;
typedef struct uielement_s uielement_t;
+220 -59
View File
@@ -1,3 +1,10 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/**
* Copyright (c) 2026 Dominic Masters
*
@@ -6,81 +13,235 @@
*/
#include "uielement.h"
#include "ui.h"
#include "assert/assert.h"
#include "util/sort.h"
#include "ui/frame/uiframe.h"
#include "ui/debug/uifps.h"
#include "engine/engine.h"
#include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h"
#include "ui/overlay/uicrop.h"
#include "ui/transition/uitransition.h"
#include "ui/debug/uiconsole.h"
#include "ui/frame/uiconfirm.h"
#include "util/memory.h"
// Priming pass: X does nothing here, so this only exists to process any
// #include directives nested in uielementlist.h/ui/uielementgame.h at
// file scope (each such header's own #pragma once makes it a no-op on
// the real pass below, which happens inside UI_ELEMENTS[]'s braces,
// where a raw #include of a declaration would be invalid).
#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing
#include "uielementlist.h"
#undef X
uielement_t UI_ELEMENTS[UI_ELEMENT_COUNT_MAX];
uielement_t UI_ELEMENTS[] = {
#define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \
{ \
.init = initFn, .update = updateFn, .draw = drawFn, \
.dispose = disposeFn, .order = elementOrder \
},
#include "uielementlist.h"
#undef X
// Belt-and-braces guard against runaway recursion in uiElementRender()/
// uiElementUpdateWorld() -- cycles are already rejected at
// uiElementSetParent() time, so this should never actually trip.
#define UI_ELEMENT_RENDER_DEPTH_MAX 16
static uint8_t UI_ELEMENT_RENDER_DEPTH = 0;
{ 0 } // Null terminator
uilelementcallbacks_t UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_COUNT] = {
[UI_ELEMENT_TYPE_NULL] = {0},
[UI_ELEMENT_TYPE_LABEL] = {
.init = uiLabelInit,
.update = uiLabelUpdate,
.render = uiLabelRender,
.dispose = uiLabelDispose
},
[UI_ELEMENT_TYPE_RECTANGLE] = {
.init = uiRectangleInit,
.update = uiRectangleUpdate,
.render = uiRectangleRender,
.dispose = uiRectangleDispose
},
[UI_ELEMENT_TYPE_SCRIPTED] = {0}
};
bool_t uiElementIsNull(const uielement_t *element) {
return element->init == NULL &&
element->update == NULL &&
element->draw == NULL &&
element->dispose == NULL;
void uiElementInit(
const uielementid_t elementId,
const uielementtype_t type
) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
assertTrue(type < UI_ELEMENT_TYPE_COUNT, "Invalid type");
assertTrue(type != UI_ELEMENT_TYPE_NULL, "Cannot initialize null type");
assertTrue(
UI_ELEMENTS[elementId].type == UI_ELEMENT_TYPE_NULL,
"Element already initialized?"
);
uielement_t *element = &UI_ELEMENTS[elementId];
memoryZero(element, sizeof(uielement_t));
element->id = elementId;
element->type = type;
element->parent = UI_ELEMENT_ID_INVALID;
element->childCount = 0;
if(UI_ELEMENT_CALLBACKS[type].init) {
UI_ELEMENT_CALLBACKS[type].init(element);
}
}
int_t uiElementCompareOrder(const void *a, const void *b) {
const uielement_t *elementA = (const uielement_t *)a;
const uielement_t *elementB = (const uielement_t *)b;
if(elementA->order < elementB->order) return -1;
if(elementA->order > elementB->order) return 1;
return 0;
void uiElementUpdate(const uielementid_t elementId) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
uielement_t *element = &UI_ELEMENTS[elementId];
assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type");
if(UI_ELEMENT_CALLBACKS[element->type].update) {
UI_ELEMENT_CALLBACKS[element->type].update(element);
}
}
void uiElementsSort(void) {
// The trailing null terminator is always the last static entry -- sort
// everything before it, and leave it in place.
const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1;
sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder);
void uiElementRender(const uielementid_t elementId) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
uielement_t *element = &UI_ELEMENTS[elementId];
assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type");
uiElementUpdateWorld(element);
assertTrue(
UI_ELEMENT_RENDER_DEPTH < UI_ELEMENT_RENDER_DEPTH_MAX,
"UI render depth exceeded -- elements nested too deep"
);
UI_ELEMENT_RENDER_DEPTH++;
if(UI_ELEMENT_CALLBACKS[element->type].render) {
UI_ELEMENT_CALLBACKS[element->type].render(element);
}
UI_ELEMENT_RENDER_DEPTH--;
}
errorret_t uiElementInit(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->init != NULL) errorChain(element->init());
errorOk();
void uiElementUpdateWorld(uielement_t *element) {
assertNotNull(element, "Element is null");
assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type");
element->worldX = element->x;
element->worldY = element->y;
if(element->parent != UI_ELEMENT_ID_INVALID) {
uielement_t *parent = &UI_ELEMENTS[element->parent];
uiElementUpdateWorld(parent);
element->worldX += parent->worldX;
element->worldY += parent->worldY;
}
}
errorret_t uiElementUpdate(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->update != NULL) errorChain(element->update());
errorOk();
void uiElementRenderChildren(uielement_t *element) {
assertNotNull(element, "Element is null");
for(uint8_t i = 0; i < element->childCount; i++) {
uiElementRender(element->children[i]);
}
}
errorret_t uiElementDraw(const uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->draw != NULL) errorChain(element->draw());
errorOk();
bool_t uiElementIsAncestorOf(
const uielementid_t elementId,
const uielementid_t otherId
) {
uielementid_t current = otherId;
for(uielementid_t i = 0; i < UI_ELEMENT_COUNT_MAX; i++) {
if(current == elementId) return true;
if(current == UI_ELEMENT_ID_INVALID) return false;
current = UI_ELEMENTS[current].parent;
}
return false;
}
errorret_t uiElementDispose(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->dispose != NULL) errorChain(element->dispose());
errorOk();
bool_t uiElementSetParent(
const uielementid_t elementId,
const uielementid_t parentElementId
) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
assertTrue(
UI_ELEMENTS[elementId].type != UI_ELEMENT_TYPE_NULL,
"Element is null type"
);
if(parentElementId != UI_ELEMENT_ID_INVALID) {
assertTrue(parentElementId < UI_ELEMENT_COUNT_MAX, "Invalid parent ID");
assertTrue(
UI_ELEMENTS[parentElementId].type != UI_ELEMENT_TYPE_NULL,
"Parent element is null type"
);
assertTrue(
elementId != parentElementId, "Cannot parent an element to itself"
);
if(uiElementIsAncestorOf(elementId, parentElementId)) return false;
if(UI_ELEMENTS[parentElementId].childCount >= UI_ELEMENT_CHILDREN_MAX) {
return false;
}
}
uielement_t *element = &UI_ELEMENTS[elementId];
if(element->parent != UI_ELEMENT_ID_INVALID) {
uielement_t *oldParent = &UI_ELEMENTS[element->parent];
for(uint8_t i = 0; i < oldParent->childCount; i++) {
if(oldParent->children[i] != elementId) continue;
for(uint8_t j = i; j < oldParent->childCount - 1; j++) {
oldParent->children[j] = oldParent->children[j + 1];
}
oldParent->childCount--;
break;
}
}
element->parent = parentElementId;
if(parentElementId != UI_ELEMENT_ID_INVALID) {
uielement_t *parent = &UI_ELEMENTS[parentElementId];
parent->children[parent->childCount++] = elementId;
uiRootRemove(elementId);
}
return true;
}
uielementid_t uiElementGetParent(const uielementid_t elementId) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
return UI_ELEMENTS[elementId].parent;
}
void uiElementDispose(const uielementid_t elementId) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
uielement_t *element = &UI_ELEMENTS[elementId];
assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type");
uiElementSetParent(elementId, UI_ELEMENT_ID_INVALID);
uiRootRemove(elementId);
for(uint8_t i = 0; i < element->childCount; i++) {
UI_ELEMENTS[element->children[i]].parent = UI_ELEMENT_ID_INVALID;
}
if(UI_ELEMENT_CALLBACKS[element->type].dispose) {
UI_ELEMENT_CALLBACKS[element->type].dispose(element);
}
memoryZero(element, sizeof(uielement_t));
}
void uiElementDisposeDeep(const uielementid_t elementId) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
uielement_t *element = &UI_ELEMENTS[elementId];
assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type");
uint8_t childCount = element->childCount;
uielementid_t children[UI_ELEMENT_CHILDREN_MAX];
if(childCount > 0) {
memoryCopy(
children, element->children, sizeof(uielementid_t) * childCount
);
}
uiElementDispose(elementId);
for(uint8_t i = 0; i < childCount; i++) {
uiElementDisposeDeep(children[i]);
}
}
uielementid_t uiElementGetAvailable() {
uielementid_t id;
for(id = 0; id < UI_ELEMENT_COUNT_MAX; id++) {
if(UI_ELEMENTS[id].type == UI_ELEMENT_TYPE_NULL) {
return id;
}
}
assertUnreachable("No available UI element IDs");
return UI_ELEMENT_COUNT_MAX;
}
+133 -47
View File
@@ -7,78 +7,164 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#include "ui/widget/uirectangle.h"
// Built-in order tiers. Lower values update/render first; ties preserve
// their relative UI_ELEMENTS declaration order (uiElementsSort uses a
// stable sort). Game-specific elements can use any int32_t value -- these
// are just the ones the engine itself relies on.
#define UI_ELEMENT_ORDER_DEFAULT 0
#define UI_ELEMENT_ORDER_DEBUG 1000
#define UI_ELEMENT_COUNT_MAX 128
#define UI_ELEMENT_CHILDREN_MAX 8
typedef struct {
errorret_t (*init)();
errorret_t (*update)();
errorret_t (*draw)();
errorret_t (*dispose)();
int32_t order;
#define UI_ELEMENT_STATE_ACTIVE (1 << 0)
typedef enum {
UI_ELEMENT_TYPE_NULL,
UI_ELEMENT_TYPE_LABEL,
UI_ELEMENT_TYPE_RECTANGLE,
UI_ELEMENT_TYPE_SCRIPTED,
UI_ELEMENT_TYPE_COUNT
} uielementtype_t;
typedef struct uielement_s {
uielementid_t id;
uint8_t state;
uielementtype_t type;
float_t x, y;
float_t worldX, worldY;
/**
* Persistent parent link, or UI_ELEMENT_ID_INVALID if this element is
* unparented. Set only via uiElementSetParent() -- add()/remove()/
* UI.add() all route through it -- and kept symmetric with the
* parent's own children[]/childCount.
*/
uielementid_t parent;
uint8_t childCount;
uielementid_t children[UI_ELEMENT_CHILDREN_MAX];
union {
uilabel_t label;
uirectangle_t rectangle;
// uiscripted_t scripted;
};
} uielement_t;
extern uielement_t UI_ELEMENTS[];
typedef struct {
void (*init)(uielement_t *element);
void (*update)(uielement_t *element);
void (*render)(uielement_t *element);
void (*dispose)(uielement_t *element);
} uilelementcallbacks_t;
extern uielement_t UI_ELEMENTS[UI_ELEMENT_COUNT_MAX];
extern uilelementcallbacks_t UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_COUNT];
/**
* Returns true when all four callbacks on the element are NULL,
* which marks the end of the UI_ELEMENTS array.
* Initializes a UI element with the given ID.
*
* @param elementId The ID of the UI element to initialize.
*/
void uiElementInit(
const uielementid_t elementId,
const uielementtype_t type
);
/**
* Updates a UI element with the given ID.
*
* @param elementId The ID of the UI element to update.
*/
void uiElementUpdate(const uielementid_t elementId);
/**
* Renders a UI element with the given ID.
*
* @param elementId The ID of the UI element to render.
*/
void uiElementRender(const uielementid_t elementId);
/**
* Recomputes worldX/worldY by walking the persistent parent chain.
* Always recomputes unconditionally (no dirty-flag cache) -- the
* underlying math is two float adds, cheap enough that caching it isn't
* worth the complexity. Safe to call any time, not just during render.
*
* @param element The element to test.
* @returns True if the element is the null terminator.
* @param element The UI element to update.
*/
bool_t uiElementIsNull(const uielement_t *element);
void uiElementUpdateWorld(uielement_t *element);
/**
* Compares two elements by their .order field, ascending. Matches
* sortcompare_t, for use with the project's sort utilities.
* Renders every child of element, in child-array (add-order) order.
* Called unconditionally by native leaf render callbacks (uiLabelRender,
* uiRectangleRender) since they have no override concept; called by
* UI_ELEMENT_TYPE_SCRIPTED's render callback only when the instance did
* NOT override render() -- an override takes full manual control and
* must call this itself (see UIElement.prototype.renderChildren) if it
* still wants added children drawn.
*
* @param a First uielement_t to compare.
* @param b Second uielement_t to compare.
* @return Negative if a < b, zero if a == b, positive if a > b.
* @param element The UI element whose children to render.
*/
int_t uiElementCompareOrder(const void *a, const void *b);
void uiElementRenderChildren(uielement_t *element);
/**
* Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing
* null terminator is never moved. Called once by uiInit -- element order
* is static after that, so there's no need to re-sort every frame.
*/
void uiElementsSort(void);
/**
* Initializes a UI element, invoking its init callback if set.
* Sets the persistent parent of a UI element, detaching it from any
* current parent first. Pass UI_ELEMENT_ID_INVALID to detach only.
* Rejects parenting an element to itself or to one of its own
* descendants (a cycle would hang uiElementRender()/uiElementUpdateWorld
* forever). Also removes elementId from UI.root[] when attaching to a
* real parent -- an element is either a render root or somebody's
* child, never both.
*
* @param element The element to initialize.
* @return Any error that occurs.
* @param elementId The child element id.
* @param parentElementId The new parent id, or UI_ELEMENT_ID_INVALID.
* @return True on success, false if parentElementId's children[] is
* already full (UI_ELEMENT_CHILDREN_MAX) or the reparent would create
* a cycle -- in both failure cases nothing is changed.
*/
errorret_t uiElementInit(uielement_t *element);
bool_t uiElementSetParent(
const uielementid_t elementId,
const uielementid_t parentElementId
);
/**
* Updates a UI element, calling its update callback if set.
* Gets the persistent parent of a UI element.
*
* @param element The element to update.
* @return Any error that occurs.
* @param elementId The element id to query.
* @return The parent id, or UI_ELEMENT_ID_INVALID if unparented.
*/
errorret_t uiElementUpdate(uielement_t *element);
uielementid_t uiElementGetParent(const uielementid_t elementId);
/**
* Draws a UI element, calling its draw callback if set.
* Internal. True if elementId is otherId itself or an ancestor of
* otherId, walking otherId's parent chain. Used by uiElementSetParent
* to reject cycles before they're created.
*
* @param element The element to render.
* @return Any error that occurs.
* @param elementId The potential ancestor.
* @param otherId The element to walk up from.
*/
errorret_t uiElementDraw(const uielement_t *element);
bool_t uiElementIsAncestorOf(
const uielementid_t elementId,
const uielementid_t otherId
);
/**
* Disposes of a UI element, invoking its dispose callback if set.
* Disposes of a UI element with the given ID.
*
* @param element The element to dispose.
* @return Any error that occurs.
* @param elementId The ID of the UI element to dispose of.
*/
errorret_t uiElementDispose(uielement_t *element);
void uiElementDispose(const uielementid_t elementId);
/**
* Disposes of a UI element and every descendant, depth-first. Detaches
* from any parent and removes from UI.root[] first.
*
* @param elementId The ID of the UI element to dispose of.
*/
void uiElementDisposeDeep(const uielementid_t elementId);
/**
* Gets an available UI element ID from the pool.
*
* @return An available UI element ID.
*/
uielementid_t uiElementGetAvailable();
+1 -8
View File
@@ -5,13 +5,6 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibutton.c
uicheckbox.c
uitab.c
uislider.c
uidropdown.c
uiscrolling.c
uimenu.c
uilabel.c
uiwidgetlabel.c
uirectangle.c
)
+31 -41
View File
@@ -5,66 +5,56 @@
* https://opensource.org/licenses/MIT
*/
#include "uilabel.h"
#include "ui/uielement.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h"
#include "display/color.h"
void uiLabelInit(uilabel_t *label, font_t *font) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
void uiLabelInit(uielement_t *element) {
assertNotNull(element, "Element cannot be NULL");
memoryZero(label, sizeof(uilabel_t));
label->font = font;
uilabel_t *label = &element->label;
label->font = &FONT_DEFAULT;
label->color = COLOR_WHITE;
}
void uiLabelSetText(uilabel_t *label, const char_t *text) {
assertNotNull(label, "Label cannot be NULL");
void uiLabelSetText(const uielementid_t elementId, const char_t *text) {
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
assertNotNull(text, "Text cannot be NULL");
assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long");
uielement_t *element = &UI_ELEMENTS[elementId];
assertTrue(element->type == UI_ELEMENT_TYPE_LABEL, "Element is not a label");
uilabel_t *label = &element->label;
stringCopy(label->text, text, UI_LABEL_TEXT_MAX);
label->spriteCount = textBuildSpriteCache(
label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX,
&label->width, &label->height
label->text, label->font, label->sprites,
UI_LABEL_SPRITE_COUNT_MAX, &label->width, &label->height
);
}
void uiLabelSetColor(uilabel_t *label, const color_t color) {
assertNotNull(label, "Label cannot be NULL");
label->color = color;
void uiLabelUpdate(uielement_t *element) {
assertNotNull(element, "Element cannot be NULL");
}
void uiLabelGetSize(
const uilabel_t *label,
int32_t *outWidth,
int32_t *outHeight
) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(outWidth, "Output width cannot be NULL");
assertNotNull(outHeight, "Output height cannot be NULL");
*outWidth = label->width;
*outHeight = label->height;
void uiLabelRender(uielement_t *element) {
assertNotNull(element, "Element cannot be NULL");
uilabel_t *label = &element->label;
if(label->spriteCount > 0) {
spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX];
errorCatch(errorPrint(textDrawSpriteCache(
label->sprites, label->spriteCount, scratch,
element->worldX, element->worldY, label->color, label->font->texture
)));
}
uiElementRenderChildren(element);
}
errorret_t uiLabelDraw(
const uilabel_t *label,
const float_t x,
const float_t y
) {
assertNotNull(label, "Label cannot be NULL");
if(label->spriteCount == 0) errorOk();
// Cached sprites are relative to (0,0); textDrawSpriteCache translates
// into the requested screen position here instead of in
// uiLabelSetText, so a label can be repositioned every frame without
// rebuilding the (much more expensive) glyph/UV cache.
spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX];
errorChain(textDrawSpriteCache(
label->sprites, label->spriteCount, scratch, x, y, label->color,
label->font->texture
));
errorOk();
void uiLabelDispose(uielement_t *element) {
assertNotNull(element, "Element cannot be NULL");
}

Some files were not shown because too many files have changed in this diff Show More