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:
@@ -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
|
||||
)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ errorret_t displayUpdate(void) {
|
||||
);
|
||||
|
||||
errorChain(sceneRender());
|
||||
errorChain(uiRender());
|
||||
|
||||
// Finish up
|
||||
screenUnbind();
|
||||
|
||||
@@ -90,6 +90,7 @@ errorret_t engineUpdate(void) {
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
errorChain(moduleSceneLateUpdateCurrent());
|
||||
}
|
||||
|
||||
// Render
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
moduleuielement.c
|
||||
modulelabel.c
|
||||
modulerectangle.c
|
||||
moduleui.c
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user