Compare commits
4 Commits
93ab7690ba
...
ac2
| Author | SHA1 | Date | |
|---|---|---|---|
| b639bc6c4f | |||
| 7ee04c78cd | |||
| 7b58addf7e | |||
| ae52be591b |
@@ -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
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Gamepad/keyboard-only for now -- no mouse support exists in the input
|
||||
// system yet. Something else (e.g. a future Menu widget) is responsible
|
||||
// for moving `highlighted` between multiple buttons; a Button only knows
|
||||
// how to fire onSelect() when ACCEPT is pressed while it's the highlighted
|
||||
// one.
|
||||
class Button extends UIElement {
|
||||
init() {
|
||||
this.background = new Rectangle();
|
||||
this.background.width = 120;
|
||||
this.background.height = 32;
|
||||
this.background.color = Button.COLOR_NORMAL;
|
||||
this.add(this.background);
|
||||
|
||||
this.label = new Label();
|
||||
this.label.x = 8;
|
||||
this.label.y = 8;
|
||||
this.add(this.label);
|
||||
|
||||
this.highlighted = false;
|
||||
this.onSelect = null;
|
||||
}
|
||||
|
||||
get text() { return this.label.text; }
|
||||
set text(value) { this.label.text = value; }
|
||||
|
||||
setHighlighted(highlighted) {
|
||||
this.highlighted = highlighted;
|
||||
this.background.color = highlighted
|
||||
? Button.COLOR_HIGHLIGHTED
|
||||
: Button.COLOR_NORMAL;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.updateChildren();
|
||||
if(this.highlighted && Input.pressed(InputBind.ACCEPT) && this.onSelect) {
|
||||
this.onSelect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button.COLOR_NORMAL = { r: 255, g: 255, b: 255, a: 255 };
|
||||
Button.COLOR_HIGHLIGHTED = { r: 255, g: 0, b: 0, a: 255 };
|
||||
|
||||
module.exports = Button;
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// A single label showing "Y "/"N " + the caller's text -- no background,
|
||||
// matching the old widget exactly. Toggling itself (checked) is driven
|
||||
// externally, typically by a Menu handing it LEFT/RIGHT via
|
||||
// directionInput() while this checkbox is the highlighted item.
|
||||
class Checkbox extends UIElement {
|
||||
init() {
|
||||
this.label = new Label();
|
||||
this.add(this.label);
|
||||
|
||||
this._text = '';
|
||||
this._checked = false;
|
||||
this.highlighted = false;
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
get text() { return this._text; }
|
||||
set text(value) {
|
||||
this._text = value;
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
get checked() { return this._checked; }
|
||||
set checked(value) {
|
||||
this._checked = value;
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
setHighlighted(highlighted) {
|
||||
this.highlighted = highlighted;
|
||||
this.label.color = highlighted
|
||||
? Checkbox.COLOR_HIGHLIGHTED
|
||||
: Checkbox.COLOR_NORMAL;
|
||||
}
|
||||
|
||||
// Menu direction-input opt-out hook: LEFT/RIGHT toggles and consumes
|
||||
// the input; UP/DOWN falls through to default cursor movement.
|
||||
directionInput(dx, dy) {
|
||||
if(dx === 0) return false;
|
||||
this.toggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.label.text = (this._checked ? 'Y ' : 'N ') + this._text;
|
||||
}
|
||||
}
|
||||
|
||||
Checkbox.COLOR_NORMAL = { r: 255, g: 255, b: 255, a: 255 };
|
||||
Checkbox.COLOR_HIGHLIGHTED = { r: 255, g: 0, b: 0, a: 255 };
|
||||
|
||||
module.exports = Checkbox;
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
var Menu = require('./Menu.js');
|
||||
var Button = require('./Button.js');
|
||||
|
||||
// A Yes/No dialog: dim full-screen backdrop + a flat-color panel (see
|
||||
// Fullbox.js/plan notes on why there's no real 9-slice Frame yet) +
|
||||
// text + a 2-item Menu. Confirm.open(text, callback) lazily creates and
|
||||
// reuses ONE shared instance rather than allocating fresh every time --
|
||||
// the Menu stack already guarantees at most one confirm is ever
|
||||
// meaningfully active, so there's nothing to gain from a fresh instance
|
||||
// per call, and it sidesteps ever needing to dispose one (see Fullbox.js
|
||||
// for why disposing from inside your own callback chain is risky).
|
||||
class Confirm extends UIElement {
|
||||
init() {
|
||||
this.backdrop = new Rectangle();
|
||||
this.backdrop.color = Confirm.COLOR_BACKDROP;
|
||||
this.add(this.backdrop);
|
||||
|
||||
this.panel = new Rectangle();
|
||||
this.panel.width = Confirm.PANEL_WIDTH;
|
||||
this.panel.height = Confirm.PANEL_HEIGHT;
|
||||
this.panel.color = Confirm.COLOR_PANEL;
|
||||
this.add(this.panel);
|
||||
|
||||
this.label = new Label();
|
||||
this.add(this.label);
|
||||
|
||||
this.menu = new Menu();
|
||||
this.menu.cols = 2;
|
||||
this.add(this.menu);
|
||||
|
||||
this.confirmButton = new Button();
|
||||
this.confirmButton.text = 'Confirm';
|
||||
this.menu.addItem(this.confirmButton);
|
||||
|
||||
this.cancelButton = new Button();
|
||||
this.cancelButton.text = 'Cancel';
|
||||
this.menu.addItem(this.cancelButton);
|
||||
|
||||
this.menu.onSelected = (index) => this._finish(index === 0);
|
||||
// CANCEL (the bind, not the button) also closes the menu without
|
||||
// firing onSelected -- treat that the same as picking Cancel.
|
||||
this.menu.onClosed = () => this._finish(false);
|
||||
|
||||
this._callback = null;
|
||||
this._finishing = false;
|
||||
}
|
||||
|
||||
open(text, callback) {
|
||||
this.label.text = text;
|
||||
this._callback = callback;
|
||||
this._finishing = false;
|
||||
|
||||
this._layout();
|
||||
|
||||
UI.add(this);
|
||||
this.menu.open();
|
||||
}
|
||||
|
||||
_finish(result) {
|
||||
// menu.onClosed calling back into _finish (via close() below) would
|
||||
// otherwise recurse into itself.
|
||||
if(this._finishing) return;
|
||||
this._finishing = true;
|
||||
|
||||
this.menu.close();
|
||||
UI.remove(this);
|
||||
|
||||
const callback = this._callback;
|
||||
this._callback = null;
|
||||
if(callback) callback(result);
|
||||
}
|
||||
|
||||
_layout() {
|
||||
this.backdrop.width = Screen.width;
|
||||
this.backdrop.height = Screen.height;
|
||||
|
||||
const panelX = (Screen.width - Confirm.PANEL_WIDTH) / 2;
|
||||
const panelY = (Screen.height - Confirm.PANEL_HEIGHT) / 2;
|
||||
this.panel.x = panelX;
|
||||
this.panel.y = panelY;
|
||||
|
||||
this.label.x = panelX + Confirm.PADDING;
|
||||
this.label.y = panelY + Confirm.PADDING;
|
||||
|
||||
this.confirmButton.x = panelX + Confirm.PADDING;
|
||||
this.confirmButton.y = panelY + Confirm.PANEL_HEIGHT - Confirm.PADDING - 32;
|
||||
|
||||
this.cancelButton.x = this.confirmButton.x + 130;
|
||||
this.cancelButton.y = this.confirmButton.y;
|
||||
}
|
||||
}
|
||||
|
||||
Confirm.PANEL_WIDTH = 300;
|
||||
Confirm.PANEL_HEIGHT = 120;
|
||||
Confirm.PADDING = 16;
|
||||
Confirm.COLOR_BACKDROP = { r: 0, g: 0, b: 0, a: 160 };
|
||||
Confirm.COLOR_PANEL = { r: 40, g: 40, b: 40, a: 255 };
|
||||
|
||||
Confirm.open = function(text, callback) {
|
||||
if(!Confirm._shared) Confirm._shared = new Confirm();
|
||||
Confirm._shared.open(text, callback);
|
||||
return Confirm._shared;
|
||||
};
|
||||
|
||||
module.exports = Confirm;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// One Label per console history slot, refreshed only when
|
||||
// Console.consumeDirty() says the history actually changed -- reuses
|
||||
// the normal Label/spritebatch render path instead of the old
|
||||
// hand-rolled mesh-cache renderer.
|
||||
//
|
||||
// These Labels are NOT add()ed as children -- UIElement.add() caps at 8
|
||||
// children per element, and CONSOLE_HISTORY_MAX (Console.lineCount) is
|
||||
// 16, over that cap. Instead they're driven directly: render()/update()
|
||||
// called explicitly below, and their world position is just their own
|
||||
// x/y (never having a parent means nothing offsets it).
|
||||
class ConsoleOverlay extends UIElement {
|
||||
init() {
|
||||
this.lines = [];
|
||||
for(let i = 0; i < Console.lineCount; i++) {
|
||||
const label = new Label();
|
||||
label.x = 4;
|
||||
label.y = 4 + i * ConsoleOverlay.LINE_HEIGHT;
|
||||
this.lines.push(label);
|
||||
}
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.updateChildren();
|
||||
if(Console.consumeDirty()) this._refresh();
|
||||
for(let i = 0; i < this.lines.length; i++) this.lines[i].update();
|
||||
}
|
||||
|
||||
// Overridden (rather than left to auto-render) so the console can be
|
||||
// hidden entirely without disposing/rebuilding its Labels.
|
||||
render() {
|
||||
this.renderChildren();
|
||||
if(!Console.visible) return;
|
||||
for(let i = 0; i < this.lines.length; i++) this.lines[i].render();
|
||||
}
|
||||
|
||||
// this.lines was never add()ed, so the normal dispose() cascade can't
|
||||
// reach them -- release them explicitly first, or they'd leak their
|
||||
// pool slots.
|
||||
dispose() {
|
||||
for(let i = 0; i < this.lines.length; i++) this.lines[i].dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
for(let i = 0; i < this.lines.length; i++) {
|
||||
this.lines[i].text = Console.getLine(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConsoleOverlay.LINE_HEIGHT = 12;
|
||||
|
||||
module.exports = ConsoleOverlay;
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Up to 4 black letterbox/pillarbox bars sized from Screen.scanX/Y/
|
||||
// scanWidth/scanHeight vs Screen.width/height. Recomputed every tick,
|
||||
// unconditionally -- cheap (four rectangle field writes), and a no-op
|
||||
// when the scan area already covers the full viewport (Rectangle
|
||||
// already skips rendering at width/height <= 0).
|
||||
class Crop extends UIElement {
|
||||
init() {
|
||||
this.top = new Rectangle();
|
||||
this.bottom = new Rectangle();
|
||||
this.left = new Rectangle();
|
||||
this.right = new Rectangle();
|
||||
|
||||
this.top.color = Crop.COLOR_BARS;
|
||||
this.bottom.color = Crop.COLOR_BARS;
|
||||
this.left.color = Crop.COLOR_BARS;
|
||||
this.right.color = Crop.COLOR_BARS;
|
||||
|
||||
this.add(this.top);
|
||||
this.add(this.bottom);
|
||||
this.add(this.left);
|
||||
this.add(this.right);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.updateChildren();
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
const scanX = Screen.scanX;
|
||||
const scanY = Screen.scanY;
|
||||
const scanRight = scanX + Screen.scanWidth;
|
||||
const scanBottom = scanY + Screen.scanHeight;
|
||||
|
||||
this.top.x = 0;
|
||||
this.top.y = 0;
|
||||
this.top.width = Screen.width;
|
||||
this.top.height = scanY;
|
||||
|
||||
this.bottom.x = 0;
|
||||
this.bottom.y = scanBottom;
|
||||
this.bottom.width = Screen.width;
|
||||
this.bottom.height = Screen.height - scanBottom;
|
||||
|
||||
this.left.x = 0;
|
||||
this.left.y = scanY;
|
||||
this.left.width = scanX;
|
||||
this.left.height = Screen.scanHeight;
|
||||
|
||||
this.right.x = scanRight;
|
||||
this.right.y = scanY;
|
||||
this.right.width = Screen.width - scanRight;
|
||||
this.right.height = Screen.scanHeight;
|
||||
}
|
||||
}
|
||||
|
||||
Crop.COLOR_BARS = { r: 0, g: 0, b: 0, a: 255 };
|
||||
|
||||
module.exports = Crop;
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Label + a "< option >"-style value label, cycling a caller-provided
|
||||
// options array. Same directionInput()-consumes-LEFT/RIGHT shape as
|
||||
// Slider, stepping the selected index (with wraparound) instead of a
|
||||
// numeric value.
|
||||
class Dropdown extends UIElement {
|
||||
init() {
|
||||
this.label = new Label();
|
||||
this.add(this.label);
|
||||
|
||||
this.valueLabel = new Label();
|
||||
this.valueLabel.x = 100;
|
||||
this.add(this.valueLabel);
|
||||
|
||||
this.options = [];
|
||||
this._index = 0;
|
||||
this.highlighted = false;
|
||||
this.onChange = null;
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
get text() { return this.label.text; }
|
||||
set text(value) { this.label.text = value; }
|
||||
|
||||
get selectedIndex() { return this._index; }
|
||||
set selectedIndex(index) {
|
||||
if(this.options.length === 0) {
|
||||
this._index = 0;
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const count = this.options.length;
|
||||
this._index = ((index % count) + count) % count;
|
||||
this._refresh();
|
||||
if(this.onChange) this.onChange(this._index, this.options[this._index]);
|
||||
}
|
||||
|
||||
get selected() { return this.options[this._index]; }
|
||||
|
||||
setHighlighted(highlighted) {
|
||||
this.highlighted = highlighted;
|
||||
this.label.color = highlighted
|
||||
? Dropdown.COLOR_HIGHLIGHTED
|
||||
: Dropdown.COLOR_NORMAL;
|
||||
}
|
||||
|
||||
directionInput(dx, dy) {
|
||||
if(dx === 0) return false;
|
||||
this.selectedIndex = this._index + dx;
|
||||
return true;
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
const value = this.options[this._index];
|
||||
this.valueLabel.text = value !== undefined ? ('< ' + value + ' >') : '< >';
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.COLOR_NORMAL = { r: 255, g: 255, b: 255, a: 255 };
|
||||
Dropdown.COLOR_HIGHLIGHTED = { r: 255, g: 0, b: 0, a: 255 };
|
||||
|
||||
module.exports = Dropdown;
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// extends UIElement (not Label) deliberately: only a UIElement/SCRIPTED
|
||||
// instance's render()/update() override actually gets dispatched by the
|
||||
// engine (the native Label/Rectangle callback tables never consult a
|
||||
// subclass's override -- they're not scripted types themselves). So this
|
||||
// composes a child Label instead of subclassing one, same as Button.
|
||||
//
|
||||
// Overrides render() rather than update(): update()/uiUpdate() only run
|
||||
// on the one fixed-timestep tick per real frame that actually crosses a
|
||||
// DUSK_TIME_STEP boundary (see Time.renderDelta's doc), but an FPS
|
||||
// counter needs to sample every real frame -- which is exactly what
|
||||
// render() does.
|
||||
class FpsCounter extends UIElement {
|
||||
init() {
|
||||
this.label = new Label();
|
||||
this.add(this.label);
|
||||
|
||||
this._average = 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
const delta = Time.renderDelta;
|
||||
if(delta > 0) {
|
||||
const fps = 1 / delta;
|
||||
this._average = this._average === 0
|
||||
? fps
|
||||
: this._average + (fps - this._average) * FpsCounter.SMOOTHING;
|
||||
}
|
||||
|
||||
this.label.text = 'FPS: ' + Math.round(this._average);
|
||||
this.renderChildren();
|
||||
}
|
||||
}
|
||||
|
||||
// Exponential-moving-average weight per sample -- lower is smoother/
|
||||
// slower to react, higher tracks instantaneous frame time more closely.
|
||||
FpsCounter.SMOOTHING = 0.1;
|
||||
|
||||
module.exports = FpsCounter;
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// A tweened full-screen color quad -- covers both the old uifullbox
|
||||
// (full-screen fade) and uitransition (fade transition with a
|
||||
// completion callback): same shape, just parameterized, so there's no
|
||||
// separate transition class/global.
|
||||
//
|
||||
// extends UIElement composing a child Rectangle (not extends Rectangle)
|
||||
// for the same reason as FpsCounter: only a UIElement/SCRIPTED
|
||||
// instance's render() override is actually dispatched by the engine.
|
||||
// Overrides render() (not update()) so the tween advances every real
|
||||
// frame, not just on fixed-timestep ticks.
|
||||
//
|
||||
// A finished Fullbox is only removed from the overlay roots, never
|
||||
// disposed -- disposing an element from inside its own render()
|
||||
// callback would free its JS instance registration while a native C
|
||||
// caller further up the same call stack still holds it, risking the
|
||||
// exact kind of delayed JerryScript refcount corruption this project
|
||||
// has already hit once. Fullbox.under()/over() lazily create and cache
|
||||
// one shared, reusable instance each (mirroring the old UNDER/OVER
|
||||
// singletons), so repeated fades cost zero additional pool slots.
|
||||
class Fullbox extends UIElement {
|
||||
init() {
|
||||
this.rect = new Rectangle();
|
||||
this.add(this.rect);
|
||||
|
||||
this._from = Fullbox.COLOR_TRANSPARENT;
|
||||
this._to = Fullbox.COLOR_TRANSPARENT;
|
||||
this._duration = 0;
|
||||
this._t = 0;
|
||||
this._onComplete = null;
|
||||
this._active = false;
|
||||
}
|
||||
|
||||
// Starts (or restarts) this Fullbox tweening rect.color from
|
||||
// options.fromColor to options.toColor over options.duration seconds,
|
||||
// calling options.onComplete() once when done. Adds itself to the
|
||||
// overlay roots if it isn't already there.
|
||||
play(options) {
|
||||
this._from = options.fromColor || Fullbox.COLOR_TRANSPARENT;
|
||||
this._to = options.toColor || Fullbox.COLOR_TRANSPARENT;
|
||||
this._duration = options.duration || 0;
|
||||
this._t = 0;
|
||||
this._onComplete = options.onComplete || null;
|
||||
this._active = true;
|
||||
|
||||
this.rect.width = Screen.width;
|
||||
this.rect.height = Screen.height;
|
||||
this.rect.color = this._from;
|
||||
|
||||
UI.addOverlay(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this._active) {
|
||||
this._t += Time.renderDelta;
|
||||
const ratio = this._duration > 0
|
||||
? Math.min(1, this._t / this._duration)
|
||||
: 1;
|
||||
|
||||
this.rect.color = Fullbox._lerpColor(this._from, this._to, ratio);
|
||||
|
||||
if(ratio >= 1) {
|
||||
this._active = false;
|
||||
UI.removeOverlay(this);
|
||||
if(this._onComplete) this._onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
this.renderChildren();
|
||||
}
|
||||
}
|
||||
|
||||
Fullbox.COLOR_TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 };
|
||||
|
||||
Fullbox._lerpColor = function(from, to, t) {
|
||||
return {
|
||||
r: from.r + (to.r - from.r) * t,
|
||||
g: from.g + (to.g - from.g) * t,
|
||||
b: from.b + (to.b - from.b) * t,
|
||||
a: from.a + (to.a - from.a) * t
|
||||
};
|
||||
};
|
||||
|
||||
Fullbox.under = function() {
|
||||
if(!Fullbox._under) Fullbox._under = new Fullbox();
|
||||
return Fullbox._under;
|
||||
};
|
||||
|
||||
Fullbox.over = function() {
|
||||
if(!Fullbox._over) Fullbox._over = new Fullbox();
|
||||
return Fullbox._over;
|
||||
};
|
||||
|
||||
module.exports = Fullbox;
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Gamepad/keyboard focus-navigation, replacing the old global uifocus_t
|
||||
// stack. A Menu's children still render/update through the normal tree
|
||||
// (add()), but navigation uses its own `items` list -- addItem() calls
|
||||
// add() and also tracks the widget as focusable, so decorative elements
|
||||
// (a title Label, a Rectangle background) just use add() directly and
|
||||
// never enter navigation.
|
||||
//
|
||||
// open()/close() push/pop this Menu on a small global stack -- only the
|
||||
// topmost entry consumes CANCEL/ACCEPT/direction input each tick, so a
|
||||
// Menu opened on top of another (e.g. a Settings sub-page menu, or a
|
||||
// Confirm dialog) doesn't fight it for input; everything underneath
|
||||
// keeps rendering/updating, just doesn't react to input until it's
|
||||
// topmost again.
|
||||
class Menu extends UIElement {
|
||||
init() {
|
||||
this.items = [];
|
||||
this.cols = 1;
|
||||
this.index = 0;
|
||||
|
||||
this.onSelected = null;
|
||||
this.onChanged = null;
|
||||
this.onClosed = null;
|
||||
|
||||
this._heldDir = null;
|
||||
this._heldTime = 0;
|
||||
}
|
||||
|
||||
addItem(widget) {
|
||||
this.add(widget);
|
||||
this.items.push(widget);
|
||||
return widget;
|
||||
}
|
||||
|
||||
open() {
|
||||
if(Menu._stack.indexOf(this) !== -1) return;
|
||||
Menu._stack.push(this);
|
||||
|
||||
const item = this.items[this.index];
|
||||
if(item && item.setHighlighted) item.setHighlighted(true);
|
||||
}
|
||||
|
||||
// Closes this Menu and anything opened on top of it, matching the old
|
||||
// uifocus_t's uiFocusPopItem (popping a mid-stack item also pops
|
||||
// everything above it).
|
||||
close() {
|
||||
const idx = Menu._stack.indexOf(this);
|
||||
if(idx === -1) return;
|
||||
|
||||
while(Menu._stack.length > idx) {
|
||||
Menu._stack.pop()._onClosed();
|
||||
}
|
||||
}
|
||||
|
||||
// Safety net: dispose() has no override-preserving trampoline the way
|
||||
// render()/update() do (see UIElement's dispose() docs), so a bare
|
||||
// this.menu.dispose() would leave a disposed instance stuck at the top
|
||||
// of Menu._stack forever if the caller forgot to close() first --
|
||||
// always close() (a no-op if never opened) before the native cascade.
|
||||
dispose() {
|
||||
this.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_onClosed() {
|
||||
for(let i = 0; i < this.items.length; i++) {
|
||||
const item = this.items[i];
|
||||
if(item.setHighlighted) item.setHighlighted(false);
|
||||
}
|
||||
if(this.onClosed) this.onClosed();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.updateChildren();
|
||||
|
||||
if(Menu.top() !== this) return;
|
||||
|
||||
if(Input.pressed(InputBind.CANCEL)) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if(Input.pressed(InputBind.ACCEPT) && this.onSelected) {
|
||||
this.onSelected(this.index, this.items[this.index]);
|
||||
}
|
||||
|
||||
this._handleDirection();
|
||||
}
|
||||
|
||||
_handleDirection() {
|
||||
const dir = this._currentDirection();
|
||||
|
||||
if(!dir) {
|
||||
this._heldDir = null;
|
||||
this._heldTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(dir !== this._heldDir) {
|
||||
this._heldDir = dir;
|
||||
this._heldTime = 0;
|
||||
this._moveDirection(dir.dx, dir.dy);
|
||||
return;
|
||||
}
|
||||
|
||||
this._heldTime += Time.delta;
|
||||
if(this._heldTime >= Menu.HOLD_DELAY) {
|
||||
this._heldTime -= Menu.HOLD_REPEAT;
|
||||
this._moveDirection(dir.dx, dir.dy);
|
||||
}
|
||||
}
|
||||
|
||||
// Identity-stable direction markers so _handleDirection's `dir !==
|
||||
// this._heldDir` check works without allocating a fresh object (and
|
||||
// therefore never matching) every tick.
|
||||
_currentDirection() {
|
||||
if(Input.isDown(InputBind.LEFT)) return Menu.DIR_LEFT;
|
||||
if(Input.isDown(InputBind.RIGHT)) return Menu.DIR_RIGHT;
|
||||
if(Input.isDown(InputBind.UP)) return Menu.DIR_UP;
|
||||
if(Input.isDown(InputBind.DOWN)) return Menu.DIR_DOWN;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gives the currently-highlighted item first refusal (a Slider/
|
||||
// Dropdown/Checkbox consuming LEFT/RIGHT itself), falling back to
|
||||
// moving the cursor -- mirrors uifocus_t's per-item `direction`
|
||||
// callback.
|
||||
_moveDirection(dx, dy) {
|
||||
if(this.items.length === 0) return;
|
||||
|
||||
const item = this.items[this.index];
|
||||
if(item && typeof item.directionInput === 'function') {
|
||||
if(item.directionInput(dx, dy)) return;
|
||||
}
|
||||
|
||||
this._moveIndex(dx, dy);
|
||||
}
|
||||
|
||||
_moveIndex(dx, dy) {
|
||||
const count = this.items.length;
|
||||
if(count === 0) return;
|
||||
|
||||
const cols = Math.max(1, this.cols);
|
||||
const rows = Math.ceil(count / cols);
|
||||
|
||||
const col = ((this.index % cols) + dx + cols) % cols;
|
||||
const row = ((Math.floor(this.index / cols)) + dy + rows) % rows;
|
||||
|
||||
let next = row * cols + col;
|
||||
if(next >= count) next = count - 1;
|
||||
|
||||
this._setIndex(next);
|
||||
}
|
||||
|
||||
_setIndex(next) {
|
||||
if(next === this.index) return;
|
||||
|
||||
const oldItem = this.items[this.index];
|
||||
if(oldItem && oldItem.setHighlighted) oldItem.setHighlighted(false);
|
||||
|
||||
this.index = next;
|
||||
|
||||
const newItem = this.items[this.index];
|
||||
if(newItem && newItem.setHighlighted) newItem.setHighlighted(true);
|
||||
|
||||
if(this.onChanged) this.onChanged(this.index, newItem);
|
||||
}
|
||||
}
|
||||
|
||||
Menu._stack = [];
|
||||
Menu.top = function() {
|
||||
return Menu._stack.length > 0 ? Menu._stack[Menu._stack.length - 1] : null;
|
||||
};
|
||||
|
||||
Menu.HOLD_DELAY = 0.5;
|
||||
Menu.HOLD_REPEAT = 0.1;
|
||||
|
||||
Menu.DIR_LEFT = { dx: -1, dy: 0 };
|
||||
Menu.DIR_RIGHT = { dx: 1, dy: 0 };
|
||||
Menu.DIR_UP = { dx: 0, dy: -1 };
|
||||
Menu.DIR_DOWN = { dx: 0, dy: 1 };
|
||||
|
||||
module.exports = Menu;
|
||||
@@ -0,0 +1,52 @@
|
||||
var Player = require('./Player.js');
|
||||
var PlayerCamera = require('./PlayerCamera.js');
|
||||
var TestPlane = require('./TestPlane.js');
|
||||
var TestUIElement = require('./TestUIElement.js');
|
||||
var FpsCounter = require('./FpsCounter.js');
|
||||
var ConsoleOverlay = require('./ConsoleOverlay.js');
|
||||
|
||||
class OverworldScene {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.player = new Player();
|
||||
this.plane = new TestPlane();
|
||||
this.camera = new PlayerCamera(this.player);
|
||||
|
||||
this.testUi = new TestUIElement();
|
||||
UI.add(this.testUi);
|
||||
|
||||
this.fpsCounter = new FpsCounter();
|
||||
UI.addOverlay(this.fpsCounter);
|
||||
|
||||
this.consoleOverlay = new ConsoleOverlay();
|
||||
UI.addOverlay(this.consoleOverlay);
|
||||
}
|
||||
|
||||
lateUpdate() {
|
||||
this.camera.update();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.camera.dispose();
|
||||
this.player.dispose();
|
||||
this.plane.dispose();
|
||||
|
||||
// testUi.dispose() cascades to its background/label children and
|
||||
// removes itself from UI's render roots -- no separate UI.remove()
|
||||
// needed.
|
||||
this.testUi.dispose();
|
||||
this.fpsCounter.dispose();
|
||||
this.consoleOverlay.dispose();
|
||||
|
||||
this.camera = null;
|
||||
this.player = null;
|
||||
this.plane = null;
|
||||
this.testUi = null;
|
||||
this.fpsCounter = null;
|
||||
this.consoleOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OverworldScene;
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
var Menu = require('./Menu.js');
|
||||
var Tab = require('./Tab.js');
|
||||
var Confirm = require('./Confirm.js');
|
||||
var SettingsGeneral = require('./SettingsGeneral.js');
|
||||
var SettingsInput = require('./SettingsInput.js');
|
||||
|
||||
// Display/Audio pages are deliberately not here -- neither has any real
|
||||
// engine system behind it, even in the old code (no resolution/
|
||||
// fullscreen system, no audio subsystem at all).
|
||||
var TABS = [
|
||||
{ name: 'General', ctor: SettingsGeneral },
|
||||
{ name: 'Input', ctor: SettingsInput }
|
||||
];
|
||||
|
||||
// Owns a tabs Menu (General/Input) and swaps in the active tab's page
|
||||
// widgets into a second, page-scoped Menu -- both Menus are open on the
|
||||
// navigation stack simultaneously (tabsMenu below, pageMenu on top),
|
||||
// matching the old settings screen's nested-menu behavior. Switching
|
||||
// away from a dirty page opens a Confirm first.
|
||||
class Settings extends UIElement {
|
||||
init() {
|
||||
this.tabsMenu = new Menu();
|
||||
this.tabsMenu.cols = TABS.length;
|
||||
this.add(this.tabsMenu);
|
||||
|
||||
this.tabs = TABS.map((def, index) => {
|
||||
const tab = new Tab();
|
||||
tab.text = def.name;
|
||||
tab.x = index * 90;
|
||||
this.tabsMenu.addItem(tab);
|
||||
return tab;
|
||||
});
|
||||
|
||||
this.tabsMenu.onSelected = (index) => this._activateTab(index);
|
||||
|
||||
this.page = null;
|
||||
this.pageMenu = null;
|
||||
this._activeTabIndex = -1;
|
||||
}
|
||||
|
||||
open() {
|
||||
UI.add(this);
|
||||
this.tabsMenu.open();
|
||||
this._activateTab(0);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.tabsMenu.close();
|
||||
if(this.pageMenu) this.pageMenu.close();
|
||||
UI.remove(this);
|
||||
}
|
||||
|
||||
_activateTab(index) {
|
||||
if(index === this._activeTabIndex) return;
|
||||
|
||||
if(this.page && this.page.hasChanges && this.page.hasChanges()) {
|
||||
Confirm.open('Discard changes?', (confirmed) => {
|
||||
if(confirmed) this._swapTab(index);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._swapTab(index);
|
||||
}
|
||||
|
||||
_swapTab(index) {
|
||||
// pageMenu.dispose() cascades to every widget the outgoing page
|
||||
// handed it via menuItems -- the page itself is a plain object (see
|
||||
// SettingsGeneral.js), nothing else to release.
|
||||
if(this.pageMenu) this.pageMenu.dispose();
|
||||
|
||||
for(let i = 0; i < this.tabs.length; i++) {
|
||||
this.tabs[i].setHighlighted(i === index);
|
||||
}
|
||||
|
||||
const def = TABS[index];
|
||||
this.page = new def.ctor();
|
||||
if(this.page.load) this.page.load();
|
||||
|
||||
this.pageMenu = new Menu();
|
||||
this.pageMenu.y = 40;
|
||||
const items = this.page.menuItems || [];
|
||||
for(let i = 0; i < items.length; i++) this.pageMenu.addItem(items[i]);
|
||||
this.add(this.pageMenu);
|
||||
this.pageMenu.open();
|
||||
|
||||
this._activeTabIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Settings;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
var Dropdown = require('./Dropdown.js');
|
||||
var Button = require('./Button.js');
|
||||
|
||||
// A plain class, not a UIElement -- it owns no visual tree of its own.
|
||||
// Its widgets are handed to Settings.js's pageMenu (via menuItems),
|
||||
// which owns their parenting/rendering/disposal; this only holds
|
||||
// load()/apply()/hasChanges() logic, mirroring the old settings system's
|
||||
// per-tab def struct.
|
||||
class SettingsGeneral {
|
||||
constructor() {
|
||||
this.dropdown = new Dropdown();
|
||||
this.dropdown.text = 'Language';
|
||||
this.dropdown.options = ['en-US', 'ja-JP', 'es-MX'];
|
||||
|
||||
this.applyButton = new Button();
|
||||
this.applyButton.text = 'Apply';
|
||||
this.applyButton.y = 40;
|
||||
this.applyButton.onSelect = () => this.apply();
|
||||
|
||||
this._loadedIndex = 0;
|
||||
}
|
||||
|
||||
get menuItems() {
|
||||
return [this.dropdown, this.applyButton];
|
||||
}
|
||||
|
||||
load() {
|
||||
const index = this.dropdown.options.indexOf(Locale.current);
|
||||
this.dropdown.selectedIndex = index >= 0 ? index : 0;
|
||||
this._loadedIndex = this.dropdown.selectedIndex;
|
||||
}
|
||||
|
||||
apply() {
|
||||
Locale.set(this.dropdown.options[this.dropdown.selectedIndex]);
|
||||
this._loadedIndex = this.dropdown.selectedIndex;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.dropdown.selectedIndex !== this._loadedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SettingsGeneral;
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
var Slider = require('./Slider.js');
|
||||
var Button = require('./Button.js');
|
||||
|
||||
// A plain class, not a UIElement -- see SettingsGeneral.js for why.
|
||||
class SettingsInput {
|
||||
constructor() {
|
||||
this.deadzoneSlider = new Slider();
|
||||
this.deadzoneSlider.text = 'Deadzone';
|
||||
this.deadzoneSlider.min = 0;
|
||||
this.deadzoneSlider.max = 1;
|
||||
this.deadzoneSlider.step = 0.05;
|
||||
|
||||
this.applyButton = new Button();
|
||||
this.applyButton.text = 'Apply';
|
||||
this.applyButton.y = 40;
|
||||
this.applyButton.onSelect = () => this.apply();
|
||||
|
||||
this._loadedValue = 0;
|
||||
}
|
||||
|
||||
get menuItems() {
|
||||
return [this.deadzoneSlider, this.applyButton];
|
||||
}
|
||||
|
||||
load() {
|
||||
this.deadzoneSlider.value = Input.deadzone;
|
||||
this._loadedValue = this.deadzoneSlider.value;
|
||||
}
|
||||
|
||||
apply() {
|
||||
Input.deadzone = this.deadzoneSlider.value;
|
||||
Save.write();
|
||||
this._loadedValue = this.deadzoneSlider.value;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.deadzoneSlider.value !== this._loadedValue;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SettingsInput;
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Name label + value label + a track/fill Rectangle pair. Unlike Button,
|
||||
// a Slider doesn't fire on ACCEPT -- it consumes LEFT/RIGHT itself via
|
||||
// directionInput(), stepping value with wraparound at min/max, which is
|
||||
// how a Menu gives the currently-highlighted item first refusal on
|
||||
// direction input before falling back to moving the cursor.
|
||||
class Slider extends UIElement {
|
||||
init() {
|
||||
this.nameLabel = new Label();
|
||||
this.add(this.nameLabel);
|
||||
|
||||
this.valueLabel = new Label();
|
||||
this.valueLabel.x = 100;
|
||||
this.add(this.valueLabel);
|
||||
|
||||
this.track = new Rectangle();
|
||||
this.track.y = 16;
|
||||
this.track.width = 100;
|
||||
this.track.height = 4;
|
||||
this.track.color = Slider.COLOR_TRACK;
|
||||
this.add(this.track);
|
||||
|
||||
this.fill = new Rectangle();
|
||||
this.fill.y = 16;
|
||||
this.fill.height = 4;
|
||||
this.fill.color = Slider.COLOR_FILL;
|
||||
this.add(this.fill);
|
||||
|
||||
this._min = 0;
|
||||
this._max = 1;
|
||||
this.step = 0.1;
|
||||
this._value = 0;
|
||||
this.highlighted = false;
|
||||
this.onChange = null;
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
get text() { return this.nameLabel.text; }
|
||||
set text(value) { this.nameLabel.text = value; }
|
||||
|
||||
get min() { return this._min; }
|
||||
set min(value) { this._min = value; this._refresh(); }
|
||||
|
||||
get max() { return this._max; }
|
||||
set max(value) { this._max = value; this._refresh(); }
|
||||
|
||||
get value() { return this._value; }
|
||||
set value(v) {
|
||||
this._value = Math.min(this._max, Math.max(this._min, v));
|
||||
this._refresh();
|
||||
if(this.onChange) this.onChange(this._value);
|
||||
}
|
||||
|
||||
setHighlighted(highlighted) {
|
||||
this.highlighted = highlighted;
|
||||
this.nameLabel.color = highlighted
|
||||
? Slider.COLOR_HIGHLIGHTED
|
||||
: Slider.COLOR_NORMAL;
|
||||
}
|
||||
|
||||
directionInput(dx, dy) {
|
||||
if(dx === 0) return false;
|
||||
|
||||
let next = this._value + dx * this.step;
|
||||
const range = this._max - this._min;
|
||||
if(range > 0) {
|
||||
// Wrap within [min, max) rather than clamp, matching the old
|
||||
// slider's step-with-wraparound behavior.
|
||||
next = this._min + (((next - this._min) % range) + range) % range;
|
||||
}
|
||||
this.value = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.valueLabel.text = String(Math.round(this._value * 100) / 100);
|
||||
const ratio = this._max > this._min
|
||||
? (this._value - this._min) / (this._max - this._min)
|
||||
: 0;
|
||||
this.fill.width = this.track.width * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
Slider.COLOR_NORMAL = { r: 255, g: 255, b: 255, a: 255 };
|
||||
Slider.COLOR_HIGHLIGHTED = { r: 255, g: 0, b: 0, a: 255 };
|
||||
Slider.COLOR_TRACK = { r: 80, g: 80, b: 80, a: 255 };
|
||||
Slider.COLOR_FILL = { r: 0, g: 160, b: 220, a: 255 };
|
||||
|
||||
module.exports = Slider;
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// A background Rectangle + Label, structurally a re-skinned Button --
|
||||
// "active" (which content tab is currently shown) plays the same role
|
||||
// Button's "highlighted" plays, so setHighlighted() is how a Menu drives
|
||||
// it: a tab strip's Menu treats "cursor is here" and "this tab's content
|
||||
// is showing" as the same thing, matching the old widget's behavior.
|
||||
class Tab extends UIElement {
|
||||
init() {
|
||||
this.background = new Rectangle();
|
||||
this.background.width = 80;
|
||||
this.background.height = 24;
|
||||
this.add(this.background);
|
||||
|
||||
this.label = new Label();
|
||||
this.label.x = 4;
|
||||
this.label.y = 4;
|
||||
this.add(this.label);
|
||||
|
||||
this.active = false;
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
get text() { return this.label.text; }
|
||||
set text(value) { this.label.text = value; }
|
||||
|
||||
setHighlighted(active) {
|
||||
this.active = active;
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.background.color = this.active
|
||||
? Tab.COLOR_ACTIVE
|
||||
: Tab.COLOR_INACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
Tab.COLOR_ACTIVE = { r: 0, g: 160, b: 0, a: 255 };
|
||||
Tab.COLOR_INACTIVE = { r: 160, g: 0, b: 0, a: 255 };
|
||||
|
||||
module.exports = Tab;
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
var Button = require('./Button.js');
|
||||
var Menu = require('./Menu.js');
|
||||
var Settings = require('./Settings.js');
|
||||
|
||||
// No render() override -- UIElement's base render() auto-renders whatever
|
||||
// was added() here, in add-order: background first, labels, then the
|
||||
// menu (and its two buttons) on top of it.
|
||||
class TestUIElement extends UIElement {
|
||||
init() {
|
||||
this.background = new Rectangle();
|
||||
this.background.width = 220;
|
||||
this.background.height = 128;
|
||||
this.background.color = { r: 255, g: 0, b: 0, a: 255 };
|
||||
this.add(this.background);
|
||||
|
||||
this.label = new Label();
|
||||
this.label.text = 'Hello World!';
|
||||
this.label.x = 8;
|
||||
this.label.y = 8;
|
||||
this.add(this.label);
|
||||
|
||||
this.label2 = new Label();
|
||||
this.label2.text = 'Second label!';
|
||||
this.label2.x = 8;
|
||||
this.label2.y = 24;
|
||||
this.add(this.label2);
|
||||
|
||||
// A navigable 2-item Menu -- UP/DOWN moves the highlighted cursor
|
||||
// between the buttons, ACCEPT fires whichever one is highlighted.
|
||||
this.menu = new Menu();
|
||||
this.add(this.menu);
|
||||
|
||||
this.helloButton = new Button();
|
||||
this.helloButton.text = 'Say hello';
|
||||
this.helloButton.x = 8;
|
||||
this.helloButton.y = 48;
|
||||
this.helloButton.onSelect = () => Console.print('Hello!');
|
||||
this.menu.addItem(this.helloButton);
|
||||
|
||||
this.settingsButton = new Button();
|
||||
this.settingsButton.text = 'Open settings';
|
||||
this.settingsButton.x = 8;
|
||||
this.settingsButton.y = 84;
|
||||
this.settingsButton.onSelect = () => new Settings().open();
|
||||
this.menu.addItem(this.settingsButton);
|
||||
|
||||
this.menu.open();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestUIElement;
|
||||
@@ -1,3 +1,5 @@
|
||||
var OverworldScene = require('./overworldscene.js');
|
||||
require('./input.js');
|
||||
|
||||
var OverworldScene = require('./OverworldScene.js');
|
||||
|
||||
Scene.set(new OverworldScene());
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Default input bindings, ported from the old
|
||||
// src/duskrpg/input/inputbindmap.h (removed) -- one INPUT_BIND_MAP per
|
||||
// compile-time target became one branch here on Platform.current.
|
||||
|
||||
function bindAll(pairs) {
|
||||
for(var i = 0; i < pairs.length; i++) {
|
||||
Input.bind(pairs[i][0], pairs[i][1]);
|
||||
}
|
||||
}
|
||||
|
||||
if(Platform.current === 'linux' || Platform.current === 'knulli') {
|
||||
bindAll([
|
||||
// Keyboard
|
||||
[InputButton.W, InputBind.UP],
|
||||
[InputButton.S, InputBind.DOWN],
|
||||
[InputButton.A, InputBind.LEFT],
|
||||
[InputButton.D, InputBind.RIGHT],
|
||||
[InputButton.LEFT, InputBind.LEFT],
|
||||
[InputButton.RIGHT, InputBind.RIGHT],
|
||||
[InputButton.UP, InputBind.UP],
|
||||
[InputButton.DOWN, InputBind.DOWN],
|
||||
[InputButton.ENTER, InputBind.ACCEPT],
|
||||
[InputButton.E, InputBind.ACCEPT],
|
||||
[InputButton.SPACE, InputBind.ACCEPT],
|
||||
[InputButton.TAB, InputBind.CANCEL],
|
||||
[InputButton.Q, InputBind.CANCEL],
|
||||
[InputButton.ESCAPE, InputBind.RAGEQUIT],
|
||||
[InputButton.ENTER, InputBind.PAUSE],
|
||||
['`', InputBind.CONSOLE],
|
||||
|
||||
// Gamepad
|
||||
[InputButton.GAMEPAD_UP, InputBind.UP],
|
||||
[InputButton.GAMEPAD_DOWN, InputBind.DOWN],
|
||||
[InputButton.GAMEPAD_LEFT, InputBind.LEFT],
|
||||
[InputButton.GAMEPAD_RIGHT, InputBind.RIGHT],
|
||||
[InputButton.GAMEPAD_A, InputBind.ACCEPT],
|
||||
[InputButton.GAMEPAD_B, InputBind.CANCEL],
|
||||
[InputButton.GAMEPAD_BACK, InputBind.RAGEQUIT],
|
||||
[InputButton.GAMEPAD_START, InputBind.PAUSE],
|
||||
[InputButton.GAMEPAD_LSTICK_UP, InputBind.UP],
|
||||
[InputButton.GAMEPAD_LSTICK_DOWN, InputBind.DOWN],
|
||||
[InputButton.GAMEPAD_LSTICK_LEFT, InputBind.LEFT],
|
||||
[InputButton.GAMEPAD_LSTICK_RIGHT, InputBind.RIGHT],
|
||||
|
||||
// Mouse
|
||||
[InputButton.MOUSE_X, InputBind.POINTERX],
|
||||
[InputButton.MOUSE_Y, InputBind.POINTERY],
|
||||
]);
|
||||
} else if(Platform.current === 'psp') {
|
||||
bindAll([
|
||||
[InputButton.UP, InputBind.UP],
|
||||
[InputButton.DOWN, InputBind.DOWN],
|
||||
[InputButton.LEFT, InputBind.LEFT],
|
||||
[InputButton.RIGHT, InputBind.RIGHT],
|
||||
[InputButton.ACCEPT, InputBind.ACCEPT],
|
||||
[InputButton.CANCEL, InputBind.CANCEL],
|
||||
[InputButton.TRIANGLE, InputBind.CONSOLE],
|
||||
[InputButton.SELECT, InputBind.RAGEQUIT],
|
||||
[InputButton.START, InputBind.PAUSE],
|
||||
[InputButton.LSTICK_UP, InputBind.UP],
|
||||
[InputButton.LSTICK_DOWN, InputBind.DOWN],
|
||||
[InputButton.LSTICK_LEFT, InputBind.LEFT],
|
||||
[InputButton.LSTICK_RIGHT, InputBind.RIGHT],
|
||||
]);
|
||||
} else if(Platform.current === 'gamecube' || Platform.current === 'wii') {
|
||||
// GameCube and Wii share the same pad layout for now.
|
||||
// TODO: Wiimote, USB Keyboard, probably more.
|
||||
bindAll([
|
||||
[InputButton.UP, InputBind.UP],
|
||||
[InputButton.DOWN, InputBind.DOWN],
|
||||
[InputButton.LEFT, InputBind.LEFT],
|
||||
[InputButton.RIGHT, InputBind.RIGHT],
|
||||
[InputButton.LSTICK_UP, InputBind.UP],
|
||||
[InputButton.LSTICK_DOWN, InputBind.DOWN],
|
||||
[InputButton.LSTICK_LEFT, InputBind.LEFT],
|
||||
[InputButton.LSTICK_RIGHT, InputBind.RIGHT],
|
||||
[InputButton.A, InputBind.ACCEPT],
|
||||
[InputButton.B, InputBind.CANCEL],
|
||||
[InputButton.Z, InputBind.CONSOLE],
|
||||
[InputButton.START, InputBind.RAGEQUIT],
|
||||
[InputButton.START, InputBind.PAUSE],
|
||||
]);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
var Player = require('./Player.js');
|
||||
var PlayerCamera = require('./PlayerCamera.js');
|
||||
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);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.camera.update();
|
||||
|
||||
this.time += Time.delta;
|
||||
if(this.time > 3.0) {
|
||||
Scene.set(new OverworldScene());
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.camera.dispose();
|
||||
this.player.dispose();
|
||||
this.plane.dispose();
|
||||
|
||||
this.camera = null;
|
||||
this.player = null;
|
||||
this.plane = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OverworldScene;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,3 +14,8 @@ add_subdirectory(require)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(console)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(screen)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(ui)
|
||||
|
||||
@@ -35,9 +35,46 @@ moduleBaseFunction(moduleConsolePrint) {
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleConsoleGetLineCount) {
|
||||
return jerry_number(CONSOLE_HISTORY_MAX);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleConsoleGetLine) {
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
|
||||
|
||||
int32_t index = moduleBaseArgInt(0);
|
||||
if(index < 0 || index >= CONSOLE_HISTORY_MAX) {
|
||||
return moduleBaseThrow("Console.getLine: index out of range");
|
||||
}
|
||||
|
||||
return jerry_string_sz(CONSOLE.line[index]);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleConsoleGetVisible) {
|
||||
return jerry_boolean(CONSOLE.visible);
|
||||
}
|
||||
|
||||
// Returns the current dirty flag and clears it -- mirrors the
|
||||
// check-then-clear contract CONSOLE.dirty already documents for native
|
||||
// consumers, so a script-side console renderer only rebuilds when
|
||||
// something actually changed since the last time it asked.
|
||||
moduleBaseFunction(moduleConsoleConsumeDirty) {
|
||||
bool_t dirty = CONSOLE.dirty;
|
||||
CONSOLE.dirty = false;
|
||||
return jerry_boolean(dirty);
|
||||
}
|
||||
|
||||
void moduleConsoleInit(void) {
|
||||
jerry_value_t consoleObj = jerry_object();
|
||||
moduleBaseDefineMethod(consoleObj, "print", moduleConsolePrint);
|
||||
moduleBaseDefineProperty(
|
||||
consoleObj, "lineCount", moduleConsoleGetLineCount, NULL
|
||||
);
|
||||
moduleBaseDefineMethod(consoleObj, "getLine", moduleConsoleGetLine);
|
||||
moduleBaseDefineProperty(
|
||||
consoleObj, "visible", moduleConsoleGetVisible, NULL
|
||||
);
|
||||
moduleBaseDefineMethod(consoleObj, "consumeDirty", moduleConsoleConsumeDirty);
|
||||
moduleBaseSetValue("Console", consoleObj);
|
||||
jerry_value_free(consoleObj);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
* Registers the global `Console` object, exposing `Console.print(...)`,
|
||||
* which stringifies and space-joins its arguments (like console.log) and
|
||||
* forwards the result to the engine's consolePrint() (see
|
||||
* console/console.h).
|
||||
* console/console.h). Also exposes read access to the console's fixed
|
||||
* CONSOLE_HISTORY_MAX-line ring buffer for a script-side renderer:
|
||||
* `Console.lineCount` (always CONSOLE_HISTORY_MAX), `Console.getLine(i)`,
|
||||
* `Console.visible`, and `Console.consumeDirty()` (returns whether the
|
||||
* history changed since the last call, clearing the flag).
|
||||
*/
|
||||
void moduleConsoleInit(void);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user