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 <[email protected]>
This commit is contained in:
1 parent
7b58addf7e
commit
7ee04c78cd
118 files changed
+2502
-391
No files matched your search
@@ -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
|
||||
)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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);
|
||||
File renamed without changes.
@@ -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
|
||||
)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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");
|
||||
}
|
||||
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user