Reimplement the archived UI widget stack on the new element-tree system
The old widget system (archive/dusk/ui/) depended on infrastructure this rewrite deleted -- a static X-macro element list, a 9-slice UI_FRAME, and a global gamepad/keyboard focus stack -- so everything below is rebuilt on top of the new native uielement_t pool + parent/child tree instead, gamepad/keyboard-only (no mouse support exists in the input system). New JS widgets (assets/scripts/), all pure composites of Label/Rectangle following the Button.js pattern: Checkbox, Tab, Slider, Dropdown. Slider/ Dropdown expose a directionInput(dx, dy) hook so a menu can hand them LEFT/RIGHT before falling back to cursor movement. New Menu.js replaces the old global uifocus_t stack: a small push/pop stack of Menu instances where only the topmost consumes CANCEL/ACCEPT/ direction input each tick (needed for nested modals -- a Settings sub-page menu plus a "discard changes?" Confirm can be open at once). Held-direction repeat timing matches the old 0.5s delay / 0.1s repeat. New overlay composites (added via a new UI.addOverlay()/UI.removeOverlay(), always drawn/updated after normal roots): FpsCounter, ConsoleOverlay (one Label per console history line, driven directly rather than via add() since History (16) exceeds the 8-children-per-element cap), Crop (letterbox/pillarbox bars), and Fullbox (a reusable tweened full-screen fade covering both the old fullbox and transition effects -- reuses shared instances rather than allocate-and-dispose, since disposing an element from inside its own render() callback risks the same JerryScript refcount corruption hit earlier in this rewrite). New Confirm.js (Yes/No dialog) and Settings.js (+ SettingsGeneral/ SettingsInput sub-pages) built on Menu. Display/Audio settings pages are intentionally not ported -- neither had a real backing engine system even in the old code. Frame is a flat Rectangle for now, not real 9-slice (a genuinely bigger native lift, deliberately deferred). Small native binding additions needed by the above, each with its own unit test: Time.renderDelta, Console.lineCount/getLine/visible/ consumeDirty, a new Screen module, a new Locale module, Input.deadzone, a new Save module, and Label.color (was missing entirely). Also fixes a real gap from the UI.addOverlay() work: uiElementDispose() wasn't removing disposed elements from the new overlay root array, which would have left dangling ids behind. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
@@ -1,6 +1,9 @@
|
||||
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() {
|
||||
@@ -11,15 +14,14 @@ class OverworldScene {
|
||||
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);
|
||||
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() {
|
||||
@@ -31,16 +33,19 @@ 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();
|
||||
// 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.camera = null;
|
||||
this.player = null;
|
||||
this.plane = null;
|
||||
this.label = null;
|
||||
this.rect = null;
|
||||
this.testUi = null;
|
||||
this.fpsCounter = null;
|
||||
this.consoleOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -15,4 +15,7 @@ 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);
|
||||
|
||||
|
||||
@@ -54,6 +54,16 @@ moduleBaseFunction(moduleInputReleasedFn) {
|
||||
return jerry_boolean(inputReleased((inputbind_t)moduleBaseArgInt(0)));
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleInputGetDeadzone) {
|
||||
return jerry_number(INPUT.deadzone);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleInputSetDeadzone) {
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
|
||||
INPUT.deadzone = moduleBaseArgFloat(0);
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
void moduleInputInit(void) {
|
||||
jerry_value_t inputObj = jerry_object();
|
||||
moduleBaseDefineMethod(inputObj, "bind", moduleInputBindFn);
|
||||
@@ -63,6 +73,9 @@ void moduleInputInit(void) {
|
||||
moduleBaseDefineMethod(inputObj, "wasDown", moduleInputWasDownFn);
|
||||
moduleBaseDefineMethod(inputObj, "pressed", moduleInputPressedFn);
|
||||
moduleBaseDefineMethod(inputObj, "released", moduleInputReleasedFn);
|
||||
moduleBaseDefineProperty(
|
||||
inputObj, "deadzone", moduleInputGetDeadzone, moduleInputSetDeadzone
|
||||
);
|
||||
moduleBaseSetValue("Input", inputObj);
|
||||
jerry_value_free(inputObj);
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
/**
|
||||
* Registers the global `Input` object (bind/value/lastValue/isDown/wasDown/
|
||||
* pressed/released), plus two constant lookup objects: `InputBind` (the
|
||||
* pressed/released, plus a read/write `deadzone` property over
|
||||
* INPUT.deadzone), plus two constant lookup objects: `InputBind` (the
|
||||
* inputbind_t action enum, e.g. InputBind.UP) and `InputButton` (every
|
||||
* named physical button in the compiled platform's INPUT_BUTTON_DATA
|
||||
* table, e.g. InputButton.SELECT -- the exact set varies per platform).
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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
|
||||
modulelocale.c
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "modulelocale.h"
|
||||
#include "script/module/modulebase.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "locale/localeinfo.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define MODULE_LOCALE_NAME_MAX 32
|
||||
|
||||
static const localeinfo_t *MODULE_LOCALE_KNOWN[] = {
|
||||
&LOCALE_EN_US,
|
||||
&LOCALE_JP_JP,
|
||||
&LOCALE_ES_MX,
|
||||
NULL
|
||||
};
|
||||
|
||||
moduleBaseFunction(moduleLocaleGetCurrent) {
|
||||
if(!LOCALE.locale) return jerry_string_sz("");
|
||||
return jerry_string_sz(LOCALE.locale->name);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleLocaleSet) {
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireString(0);
|
||||
|
||||
char_t name[MODULE_LOCALE_NAME_MAX];
|
||||
moduleBaseToString(args[0], name, sizeof(name));
|
||||
|
||||
for(const localeinfo_t **info = MODULE_LOCALE_KNOWN; *info; info++) {
|
||||
if(stringCompare((*info)->name, name) != 0) continue;
|
||||
|
||||
errorret_t ret = localeManagerSetLocale(*info);
|
||||
if(errorIsNotOk(ret)) return moduleBaseThrowError(ret);
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
return moduleBaseThrow("Locale.set: unknown locale name");
|
||||
}
|
||||
|
||||
void moduleLocaleInit(void) {
|
||||
jerry_value_t localeObj = jerry_object();
|
||||
moduleBaseDefineProperty(
|
||||
localeObj, "current", moduleLocaleGetCurrent, NULL
|
||||
);
|
||||
moduleBaseDefineMethod(localeObj, "set", moduleLocaleSet);
|
||||
moduleBaseSetValue("Locale", localeObj);
|
||||
jerry_value_free(localeObj);
|
||||
}
|
||||
|
||||
void moduleLocaleDispose(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 global `Locale` object, exposing read-only `current`
|
||||
* (the active locale's name, e.g. "en-US") and `set(name)` (switches the
|
||||
* active locale by name -- throws if name doesn't match a known
|
||||
* localeinfo_t) over the engine's locale system (see
|
||||
* locale/localemanager.h).
|
||||
*/
|
||||
void moduleLocaleInit(void);
|
||||
|
||||
/**
|
||||
* Disposes the locale module's script resources.
|
||||
*/
|
||||
void moduleLocaleDispose(void);
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/color.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -460,3 +461,39 @@ static inline jerry_value_t moduleBaseVec3ToObject(const vec3 v) {
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a color_t as a plain JS {r, g, b, a} object.
|
||||
*/
|
||||
static inline jerry_value_t moduleBaseColorToObject(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a plain JS {r, g, b, a} object into a color_t. Any missing
|
||||
* channel keeps its COLOR_WHITE default.
|
||||
*/
|
||||
static inline color_t moduleBaseColorFromObject(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;
|
||||
}
|
||||
|
||||
@@ -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/screen/modulescreen.h"
|
||||
#include "script/module/locale/modulelocale.h"
|
||||
#include "script/module/save/modulesave.h"
|
||||
#include "script/module/ui/moduleuielement.h"
|
||||
#include "script/module/ui/modulelabel.h"
|
||||
#include "script/module/ui/modulerectangle.h"
|
||||
@@ -28,6 +31,9 @@ void moduleListInit(void) {
|
||||
moduleTimeInit();
|
||||
moduleConsoleInit();
|
||||
moduleInputInit();
|
||||
moduleScreenInit();
|
||||
moduleLocaleInit();
|
||||
moduleSaveInit();
|
||||
moduleUiElementInit();
|
||||
moduleLabelInit();
|
||||
moduleRectangleInit();
|
||||
@@ -47,6 +53,9 @@ void moduleListDispose(void) {
|
||||
moduleRectangleDispose();
|
||||
moduleLabelDispose();
|
||||
moduleUiElementDispose();
|
||||
moduleSaveDispose();
|
||||
moduleLocaleDispose();
|
||||
moduleScreenDispose();
|
||||
moduleInputDispose();
|
||||
moduleConsoleDispose();
|
||||
moduleTimeDispose();
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
/**
|
||||
* Registers every script module (Component + its typed per-type
|
||||
* wrappers, Entity, Scene, require(), Mesh, Time, Console, Input,
|
||||
* UIElement + Label/Rectangle + UI, the platform globals). Called once
|
||||
* by scriptManagerInit().
|
||||
* wrappers, Entity, Scene, require(), Mesh, Time, Console, Input, Screen,
|
||||
* Locale, Save, UIElement + Label/Rectangle + UI, the platform globals).
|
||||
* Called once by scriptManagerInit().
|
||||
*/
|
||||
void moduleListInit(void);
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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
|
||||
modulesave.c
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "modulesave.h"
|
||||
#include "script/module/modulebase.h"
|
||||
#include "save/savesettings.h"
|
||||
|
||||
moduleBaseFunction(moduleSaveWrite) {
|
||||
errorret_t ret = saveSettingsWrite();
|
||||
if(errorIsNotOk(ret)) return moduleBaseThrowError(ret);
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
void moduleSaveInit(void) {
|
||||
jerry_value_t saveObj = jerry_object();
|
||||
moduleBaseDefineMethod(saveObj, "write", moduleSaveWrite);
|
||||
moduleBaseSetValue("Save", saveObj);
|
||||
jerry_value_free(saveObj);
|
||||
}
|
||||
|
||||
void moduleSaveDispose(void) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Registers the global `Save` object, exposing `write()`, which gathers
|
||||
* all game settings from their owning runtime state and persists them
|
||||
* (see save/savesettings.h's saveSettingsWrite()).
|
||||
*/
|
||||
void moduleSaveInit(void);
|
||||
|
||||
/**
|
||||
* Disposes the save module's script resources.
|
||||
*/
|
||||
void moduleSaveDispose(void);
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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
|
||||
modulescreen.c
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "modulescreen.h"
|
||||
#include "script/module/modulebase.h"
|
||||
#include "display/screen/screen.h"
|
||||
|
||||
moduleBaseFunction(moduleScreenGetWidth) {
|
||||
return jerry_number(SCREEN.width);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleScreenGetHeight) {
|
||||
return jerry_number(SCREEN.height);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleScreenGetScanX) {
|
||||
return jerry_number(SCREEN.scanX);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleScreenGetScanY) {
|
||||
return jerry_number(SCREEN.scanY);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleScreenGetScanWidth) {
|
||||
return jerry_number(SCREEN.scanWidth);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleScreenGetScanHeight) {
|
||||
return jerry_number(SCREEN.scanHeight);
|
||||
}
|
||||
|
||||
void moduleScreenInit(void) {
|
||||
jerry_value_t screenObj = jerry_object();
|
||||
moduleBaseDefineProperty(screenObj, "width", moduleScreenGetWidth, NULL);
|
||||
moduleBaseDefineProperty(screenObj, "height", moduleScreenGetHeight, NULL);
|
||||
moduleBaseDefineProperty(screenObj, "scanX", moduleScreenGetScanX, NULL);
|
||||
moduleBaseDefineProperty(screenObj, "scanY", moduleScreenGetScanY, NULL);
|
||||
moduleBaseDefineProperty(
|
||||
screenObj, "scanWidth", moduleScreenGetScanWidth, NULL
|
||||
);
|
||||
moduleBaseDefineProperty(
|
||||
screenObj, "scanHeight", moduleScreenGetScanHeight, NULL
|
||||
);
|
||||
moduleBaseSetValue("Screen", screenObj);
|
||||
jerry_value_free(screenObj);
|
||||
}
|
||||
|
||||
void moduleScreenDispose(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 global `Screen` object, exposing read-only `width`/
|
||||
* `height` (calculated viewport dimensions) and `scanX`/`scanY`/
|
||||
* `scanWidth`/`scanHeight` (the overscan-safe area within the viewport --
|
||||
* defaults to the full viewport, letterboxed/pillarboxed on platforms
|
||||
* that need it) as live getters over the engine's SCREEN state (see
|
||||
* display/screen/screen.h).
|
||||
*/
|
||||
void moduleScreenInit(void);
|
||||
|
||||
/**
|
||||
* Disposes the screen module's script resources.
|
||||
*/
|
||||
void moduleScreenDispose(void);
|
||||
@@ -17,10 +17,21 @@ moduleBaseFunction(moduleTimeGetTime) {
|
||||
return jerry_number(TIME.time);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleTimeGetRenderDelta) {
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
return jerry_number(TIME.dynamicDelta);
|
||||
#else
|
||||
return jerry_number(TIME.delta);
|
||||
#endif
|
||||
}
|
||||
|
||||
void moduleTimeInit(void) {
|
||||
jerry_value_t timeObj = jerry_object();
|
||||
moduleBaseDefineProperty(timeObj, "delta", moduleTimeGetDelta, NULL);
|
||||
moduleBaseDefineProperty(timeObj, "time", moduleTimeGetTime, NULL);
|
||||
moduleBaseDefineProperty(
|
||||
timeObj, "renderDelta", moduleTimeGetRenderDelta, NULL
|
||||
);
|
||||
moduleBaseSetValue("Time", timeObj);
|
||||
jerry_value_free(timeObj);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,13 @@
|
||||
|
||||
/**
|
||||
* Registers the global `Time` object, exposing `Time.delta` and
|
||||
* `Time.time` as live getters over the engine's TIME state (see
|
||||
* time/time.h). Values always reflect the most recent timeUpdate().
|
||||
* `Time.time` (fixed-timestep, only advance on the one timeUpdate() call
|
||||
* per tick that crosses a DUSK_TIME_STEP boundary) and `Time.renderDelta`
|
||||
* (real elapsed time since the previous timeUpdate() call, i.e.
|
||||
* TIME.dynamicDelta under DUSK_TIME_DYNAMIC or plain TIME.delta
|
||||
* otherwise -- for per-real-frame effects like FPS counters or tweens
|
||||
* that shouldn't be gated to the fixed tick) as live getters over the
|
||||
* engine's TIME state (see time/time.h).
|
||||
*/
|
||||
void moduleTimeInit(void);
|
||||
|
||||
|
||||
@@ -35,6 +35,19 @@ moduleBaseFunction(moduleLabelSetText) {
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleLabelGetColor) {
|
||||
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
|
||||
return moduleBaseColorToObject(UI_ELEMENTS[h->id].label.color);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleLabelSetColor) {
|
||||
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
|
||||
|
||||
uiLabelSetColor(h->id, moduleBaseColorFromObject(args[0]));
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
void moduleLabelInit(void) {
|
||||
scriptProtoInit(
|
||||
&MODULE_LABEL_PROTO,
|
||||
@@ -49,6 +62,9 @@ void moduleLabelInit(void) {
|
||||
scriptProtoDefineProp(
|
||||
&MODULE_LABEL_PROTO, "text", moduleLabelGetText, moduleLabelSetText
|
||||
);
|
||||
scriptProtoDefineProp(
|
||||
&MODULE_LABEL_PROTO, "color", moduleLabelGetColor, moduleLabelSetColor
|
||||
);
|
||||
}
|
||||
|
||||
void moduleLabelDispose(void) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 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).
|
||||
* adds `text` and `color` properties (both read/write).
|
||||
*/
|
||||
void moduleLabelInit(void);
|
||||
|
||||
|
||||
@@ -13,35 +13,6 @@
|
||||
|
||||
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
|
||||
@@ -80,14 +51,14 @@ moduleBaseFunction(moduleRectangleSetHeight) {
|
||||
|
||||
moduleBaseFunction(moduleRectangleGetColor) {
|
||||
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
|
||||
return moduleRectangleColorToObject(UI_ELEMENTS[h->id].rectangle.color);
|
||||
return moduleBaseColorToObject(UI_ELEMENTS[h->id].rectangle.color);
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleRectangleSetColor) {
|
||||
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
|
||||
|
||||
uiRectangleSetColor(h->id, moduleRectangleColorFromObject(args[0]));
|
||||
uiRectangleSetColor(h->id, moduleBaseColorFromObject(args[0]));
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,39 @@ moduleBaseFunction(moduleUiRemoveFn) {
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleUiAddOverlayFn) {
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
|
||||
|
||||
moduleuielementhandle_t *h = moduleUiElementGetFromValue(args[0]);
|
||||
if(!h) return moduleBaseThrow("Expected a UIElement");
|
||||
|
||||
// A root/overlay root and a child are mutually exclusive, same as
|
||||
// UI.add() -- promoting an existing child detaches it from its parent
|
||||
// first.
|
||||
uiElementSetParent(h->id, UI_ELEMENT_ID_INVALID);
|
||||
|
||||
if(!uiOverlayRootAdd(h->id)) {
|
||||
return moduleBaseThrow("UI overlay root capacity exceeded");
|
||||
}
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleUiRemoveOverlayFn) {
|
||||
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
|
||||
|
||||
moduleuielementhandle_t *h = moduleUiElementGetFromValue(args[0]);
|
||||
if(!h) return moduleBaseThrow("Expected a UIElement");
|
||||
|
||||
uiOverlayRootRemove(h->id);
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
void moduleUiInit(void) {
|
||||
jerry_value_t uiObj = jerry_object();
|
||||
moduleBaseDefineMethod(uiObj, "add", moduleUiAddFn);
|
||||
moduleBaseDefineMethod(uiObj, "remove", moduleUiRemoveFn);
|
||||
moduleBaseDefineMethod(uiObj, "addOverlay", moduleUiAddOverlayFn);
|
||||
moduleBaseDefineMethod(uiObj, "removeOverlay", moduleUiRemoveOverlayFn);
|
||||
moduleBaseSetValue("UI", uiObj);
|
||||
jerry_value_free(uiObj);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,19 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Registers the global `UI` object, exposing `UI.add(element)`/
|
||||
* `UI.remove(element)` and `UI.addOverlay(element)`/
|
||||
* `UI.removeOverlay(element)`. All four take a UIElement (or Label/
|
||||
* Rectangle/etc.) instance; add/remove manage the engine's normal render
|
||||
* roots (UI.root[] in ui/ui.h), addOverlay/removeOverlay manage a
|
||||
* second root array (UI.overlayRoot[]) that's always drawn/updated
|
||||
* strictly after every normal root, for things that must always sit on
|
||||
* top (debug overlays, screen fades/transitions). Root/overlay-root
|
||||
* elements get render()/update() called once per frame/tick by
|
||||
* uiRender()/uiUpdate(); everything else only renders as part of a
|
||||
* root's children (see UIElement.add()). A root (of either kind) and a
|
||||
* child are mutually exclusive: UI.add()/UI.addOverlay() detach the
|
||||
* element from any parent first.
|
||||
*/
|
||||
void moduleUiInit(void);
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@ scriptproto_t MODULE_UI_ELEMENT_PROTO;
|
||||
// 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.
|
||||
// Own (instance, not prototype) properties that a construction-time
|
||||
// override wrap stashes the (sub)class's real render()/update() under --
|
||||
// see moduleUiElementConstructShared and moduleUiElementRenderScripted/
|
||||
// moduleUiElementUpdateScripted.
|
||||
#define MODULE_UI_ELEMENT_USER_RENDER_PROP "__uiElementUserRender"
|
||||
#define MODULE_UI_ELEMENT_USER_UPDATE_PROP "__uiElementUserUpdate"
|
||||
|
||||
static bool_t moduleUiElementStrictEqual(
|
||||
const jerry_value_t a,
|
||||
@@ -38,6 +40,12 @@ static jerry_value_t moduleUiElementRenderMethod(
|
||||
const jerry_length_t argc
|
||||
);
|
||||
|
||||
static jerry_value_t moduleUiElementUpdateMethod(
|
||||
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.
|
||||
@@ -64,45 +72,67 @@ static void moduleUiElementRenderScripted(uielement_t *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
|
||||
// UI_ELEMENT_TYPE_SCRIPTED's update callback -- exact update-side twin of
|
||||
// moduleUiElementRenderScripted, see there for the reasoning.
|
||||
static void moduleUiElementUpdateScripted(uielement_t *element) {
|
||||
jerry_value_t instance = UI_ELEMENT_JS_INSTANCES[element->id];
|
||||
jerry_value_t updateFn = jerry_value_is_object(instance)
|
||||
? moduleBaseGetProp(instance, MODULE_UI_ELEMENT_USER_UPDATE_PROP)
|
||||
: jerry_undefined();
|
||||
|
||||
if(jerry_value_is_function(updateFn)) {
|
||||
errorret_t ret = scriptManagerCallValue(
|
||||
instance, updateFn, "UIElement", "update"
|
||||
);
|
||||
errorCatch(errorPrint(ret));
|
||||
} else {
|
||||
uiElementUpdateChildren(element);
|
||||
}
|
||||
|
||||
jerry_value_free(updateFn);
|
||||
}
|
||||
|
||||
// Calling instance.render()/update() (JS method-call syntax) only reaches
|
||||
// native code if the property lookup actually resolves to
|
||||
// UIElement.prototype's own (native) method -- if a (sub)class overrides
|
||||
// it, that override shadows it and callers bypass uiElementRender()/
|
||||
// uiElementUpdate() entirely, which is what actually tracks parent/
|
||||
// worldX/worldY (for render) or reaches this element at all from the
|
||||
// engine's per-tick/per-frame root walk (for update). So if this
|
||||
// instance's resolved method is anything other than the base one, stash
|
||||
// the override under stashProp and shadow methodName itself (as an own
|
||||
// property, just on this instance) with trampoline --
|
||||
// moduleUiElementRenderScripted/moduleUiElementUpdateScripted then call
|
||||
// the stashed override from inside uiElementRender()/uiElementUpdate(),
|
||||
// fully wired up.
|
||||
static void moduleUiElementWrapUserMethodIfOverridden(
|
||||
const jerry_value_t thisValue,
|
||||
const char_t *methodName,
|
||||
const char_t *stashProp,
|
||||
jerry_external_handler_t trampolineFn
|
||||
) {
|
||||
jerry_value_t resolvedRender = moduleBaseGetProp(thisValue, "render");
|
||||
jerry_value_t baseRender = moduleBaseGetProp(
|
||||
MODULE_UI_ELEMENT_PROTO.prototype, "render"
|
||||
jerry_value_t resolvedMethod = moduleBaseGetProp(thisValue, methodName);
|
||||
jerry_value_t baseMethod = moduleBaseGetProp(
|
||||
MODULE_UI_ELEMENT_PROTO.prototype, methodName
|
||||
);
|
||||
|
||||
if(
|
||||
jerry_value_is_function(resolvedRender) &&
|
||||
!moduleUiElementStrictEqual(resolvedRender, baseRender)
|
||||
jerry_value_is_function(resolvedMethod) &&
|
||||
!moduleUiElementStrictEqual(resolvedMethod, baseMethod)
|
||||
) {
|
||||
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 stashKey = jerry_string_sz(stashProp);
|
||||
jerry_object_set(thisValue, stashKey, resolvedMethod);
|
||||
jerry_value_free(stashKey);
|
||||
|
||||
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_t trampoline = jerry_function_external(trampolineFn);
|
||||
jerry_value_t methodKey = jerry_string_sz(methodName);
|
||||
jerry_object_set(thisValue, methodKey, trampoline);
|
||||
jerry_value_free(methodKey);
|
||||
jerry_value_free(trampoline);
|
||||
}
|
||||
|
||||
jerry_value_free(baseRender);
|
||||
jerry_value_free(resolvedRender);
|
||||
jerry_value_free(baseMethod);
|
||||
jerry_value_free(resolvedMethod);
|
||||
}
|
||||
|
||||
errorret_t moduleUiElementConstructShared(
|
||||
@@ -119,7 +149,14 @@ errorret_t moduleUiElementConstructShared(
|
||||
thisValue, &MODULE_UI_ELEMENT_PROTO.info, inst
|
||||
);
|
||||
UI_ELEMENT_JS_INSTANCES[inst->id] = jerry_value_copy(thisValue);
|
||||
moduleUiElementWrapUserRenderIfOverridden(thisValue);
|
||||
moduleUiElementWrapUserMethodIfOverridden(
|
||||
thisValue, "render", MODULE_UI_ELEMENT_USER_RENDER_PROP,
|
||||
moduleUiElementRenderMethod
|
||||
);
|
||||
moduleUiElementWrapUserMethodIfOverridden(
|
||||
thisValue, "update", MODULE_UI_ELEMENT_USER_UPDATE_PROP,
|
||||
moduleUiElementUpdateMethod
|
||||
);
|
||||
|
||||
jerry_value_t initFn = moduleBaseGetProp(thisValue, "init");
|
||||
if(jerry_value_is_function(initFn)) {
|
||||
@@ -259,6 +296,18 @@ moduleBaseFunction(moduleUiElementRenderMethod) {
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleUiElementUpdateChildrenMethod) {
|
||||
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
|
||||
uiElementUpdateChildren(&UI_ELEMENTS[h->id]);
|
||||
return jerry_undefined();
|
||||
}
|
||||
|
||||
moduleBaseFunction(moduleUiElementUpdateMethod) {
|
||||
moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet);
|
||||
uiElementUpdate(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
|
||||
@@ -342,12 +391,19 @@ void moduleUiElementInit(void) {
|
||||
scriptProtoDefineFunc(
|
||||
&MODULE_UI_ELEMENT_PROTO, "render", moduleUiElementRenderMethod
|
||||
);
|
||||
scriptProtoDefineFunc(
|
||||
&MODULE_UI_ELEMENT_PROTO, "updateChildren", moduleUiElementUpdateChildrenMethod
|
||||
);
|
||||
scriptProtoDefineFunc(
|
||||
&MODULE_UI_ELEMENT_PROTO, "update", moduleUiElementUpdateMethod
|
||||
);
|
||||
scriptProtoDefineFunc(
|
||||
&MODULE_UI_ELEMENT_PROTO, "dispose", moduleUiElementDisposeMethod
|
||||
);
|
||||
|
||||
UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_SCRIPTED] = (uilelementcallbacks_t){
|
||||
.render = moduleUiElementRenderScripted
|
||||
.render = moduleUiElementRenderScripted,
|
||||
.update = moduleUiElementUpdateScripted
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,12 @@ extern scriptproto_t MODULE_UI_ELEMENT_PROTO;
|
||||
* 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
|
||||
* UI_ELEMENT_CHILDREN_MAX per element), render()/update(),
|
||||
* renderChildren()/updateChildren() (render/update whatever was added,
|
||||
* in add-order -- called automatically by render()/update() unless a
|
||||
* subclass overrides it, in which case the override has full manual
|
||||
* control and must call the *Children() twin itself if it still wants
|
||||
* added children rendered/updated), and dispose() (cascades to every
|
||||
* descendant).
|
||||
*/
|
||||
void moduleUiElementInit(void);
|
||||
|
||||
+36
-1
@@ -35,10 +35,30 @@ void uiRootRemove(const uielementid_t elementId) {
|
||||
}
|
||||
}
|
||||
|
||||
bool_t uiOverlayRootAdd(const uielementid_t elementId) {
|
||||
for(uint8_t i = 0; i < UI_OVERLAY_ROOT_MAX; i++) {
|
||||
if(UI.overlayRoot[i] != UI_ELEMENT_ID_INVALID) continue;
|
||||
UI.overlayRoot[i] = elementId;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void uiOverlayRootRemove(const uielementid_t elementId) {
|
||||
for(uint8_t i = 0; i < UI_OVERLAY_ROOT_MAX; i++) {
|
||||
if(UI.overlayRoot[i] != elementId) continue;
|
||||
UI.overlayRoot[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;
|
||||
for(uint8_t i = 0; i < UI_OVERLAY_ROOT_MAX; i++) {
|
||||
UI.overlayRoot[i] = UI_ELEMENT_ID_INVALID;
|
||||
}
|
||||
|
||||
consolePrint(
|
||||
"UI elements size: %zu bytes (%.2f KB), %zu bytes/element, "
|
||||
@@ -56,7 +76,15 @@ errorret_t uiUpdate(void) {
|
||||
if(TIME.dynamicUpdate) errorOk();
|
||||
#endif
|
||||
|
||||
|
||||
for(uint8_t i = 0; i < UI_ROOT_MAX; i++) {
|
||||
if(UI.root[i] == UI_ELEMENT_ID_INVALID) continue;
|
||||
uiElementUpdate(UI.root[i]);
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < UI_OVERLAY_ROOT_MAX; i++) {
|
||||
if(UI.overlayRoot[i] == UI_ELEMENT_ID_INVALID) continue;
|
||||
uiElementUpdate(UI.overlayRoot[i]);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -99,6 +127,13 @@ errorret_t uiRender(void) {
|
||||
uiElementRender(UI.root[i]);
|
||||
}
|
||||
|
||||
// Always drawn after every UI.root[] element, regardless of add order
|
||||
// between the two arrays -- debug overlays/screen fades stay on top.
|
||||
for(uint8_t i = 0; i < UI_OVERLAY_ROOT_MAX; i++) {
|
||||
if(UI.overlayRoot[i] == UI_ELEMENT_ID_INVALID) continue;
|
||||
uiElementRender(UI.overlayRoot[i]);
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -10,9 +10,16 @@
|
||||
#include "uielement.h"
|
||||
|
||||
#define UI_ROOT_MAX 32
|
||||
#define UI_OVERLAY_ROOT_MAX 8
|
||||
|
||||
typedef struct {
|
||||
uielementid_t root[UI_ROOT_MAX];
|
||||
|
||||
// Drawn/updated strictly after every UI.root[] element, regardless of
|
||||
// add order between the two arrays -- for things that must always sit
|
||||
// on top (debug overlays, screen fades/transitions), so they don't
|
||||
// have to race normal UI content for root slots/order.
|
||||
uielementid_t overlayRoot[UI_OVERLAY_ROOT_MAX];
|
||||
} ui_t;
|
||||
|
||||
extern ui_t UI;
|
||||
@@ -33,6 +40,23 @@ bool_t uiRootAdd(const uielementid_t elementId);
|
||||
*/
|
||||
void uiRootRemove(const uielementid_t elementId);
|
||||
|
||||
/**
|
||||
* Adds an element to UI's overlay roots (drawn/updated after every
|
||||
* UI.root[] element), in the first free slot.
|
||||
*
|
||||
* @param elementId The element id to add as an overlay root.
|
||||
* @return True on success, false if UI.overlayRoot[] is already full.
|
||||
*/
|
||||
bool_t uiOverlayRootAdd(const uielementid_t elementId);
|
||||
|
||||
/**
|
||||
* Removes an element from UI's overlay roots, if present. No-op if
|
||||
* elementId isn't currently an overlay root.
|
||||
*
|
||||
* @param elementId The element id to remove.
|
||||
*/
|
||||
void uiOverlayRootRemove(const uielementid_t elementId);
|
||||
|
||||
/**
|
||||
* Initializes the UI system.
|
||||
*
|
||||
|
||||
+24
-1
@@ -25,6 +25,8 @@ uielement_t UI_ELEMENTS[UI_ELEMENT_COUNT_MAX];
|
||||
#define UI_ELEMENT_RENDER_DEPTH_MAX 16
|
||||
static uint8_t UI_ELEMENT_RENDER_DEPTH = 0;
|
||||
|
||||
static void uiElementRenderPrepared(uielement_t *element);
|
||||
|
||||
uilelementcallbacks_t UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_COUNT] = {
|
||||
[UI_ELEMENT_TYPE_NULL] = {0},
|
||||
|
||||
@@ -80,6 +82,14 @@ void uiElementUpdate(const uielementid_t elementId) {
|
||||
}
|
||||
}
|
||||
|
||||
void uiElementUpdateChildren(uielement_t *element) {
|
||||
assertNotNull(element, "Element is null");
|
||||
|
||||
for(uint8_t i = 0; i < element->childCount; i++) {
|
||||
uiElementUpdate(element->children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void uiElementRender(const uielementid_t elementId) {
|
||||
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
|
||||
|
||||
@@ -87,7 +97,16 @@ void uiElementRender(const uielementid_t elementId) {
|
||||
assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type");
|
||||
|
||||
uiElementUpdateWorld(element);
|
||||
uiElementRenderPrepared(element);
|
||||
}
|
||||
|
||||
// Shared depth-guarded dispatch to the type's render callback, once
|
||||
// worldX/worldY are already known-fresh. uiElementRender() gets there via
|
||||
// a full uiElementUpdateWorld() walk (parent freshness unknown); child
|
||||
// elements get there via uiElementRenderChildren(), which derives world
|
||||
// position in O(1) from the parent it was just called on -- avoiding a
|
||||
// second walk back up the same chain that's already mid-render.
|
||||
static void uiElementRenderPrepared(uielement_t *element) {
|
||||
assertTrue(
|
||||
UI_ELEMENT_RENDER_DEPTH < UI_ELEMENT_RENDER_DEPTH_MAX,
|
||||
"UI render depth exceeded -- elements nested too deep"
|
||||
@@ -120,7 +139,10 @@ void uiElementRenderChildren(uielement_t *element) {
|
||||
assertNotNull(element, "Element is null");
|
||||
|
||||
for(uint8_t i = 0; i < element->childCount; i++) {
|
||||
uiElementRender(element->children[i]);
|
||||
uielement_t *child = &UI_ELEMENTS[element->children[i]];
|
||||
child->worldX = child->x + element->worldX;
|
||||
child->worldY = child->y + element->worldY;
|
||||
uiElementRenderPrepared(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +223,7 @@ void uiElementDispose(const uielementid_t elementId) {
|
||||
|
||||
uiElementSetParent(elementId, UI_ELEMENT_ID_INVALID);
|
||||
uiRootRemove(elementId);
|
||||
uiOverlayRootRemove(elementId);
|
||||
|
||||
for(uint8_t i = 0; i < element->childCount; i++) {
|
||||
UI_ELEMENTS[element->children[i]].parent = UI_ELEMENT_ID_INVALID;
|
||||
|
||||
@@ -106,6 +106,17 @@ void uiElementUpdateWorld(uielement_t *element);
|
||||
*/
|
||||
void uiElementRenderChildren(uielement_t *element);
|
||||
|
||||
/**
|
||||
* Updates every child of element. Called unconditionally by native leaf
|
||||
* update callbacks (uiLabelUpdate, uiRectangleUpdate) since they have no
|
||||
* override concept; called by UI_ELEMENT_TYPE_SCRIPTED's update callback
|
||||
* only when the instance did NOT override update() -- mirrors
|
||||
* uiElementRenderChildren/render() exactly.
|
||||
*
|
||||
* @param element The UI element whose children to update.
|
||||
*/
|
||||
void uiElementUpdateChildren(uielement_t *element);
|
||||
|
||||
/**
|
||||
* Sets the persistent parent of a UI element, detaching it from any
|
||||
* current parent first. Pass UI_ELEMENT_ID_INVALID to detach only.
|
||||
|
||||
@@ -36,8 +36,18 @@ void uiLabelSetText(const uielementid_t elementId, const char_t *text) {
|
||||
);
|
||||
}
|
||||
|
||||
void uiLabelSetColor(const uielementid_t elementId, const color_t color) {
|
||||
assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID");
|
||||
|
||||
uielement_t *element = &UI_ELEMENTS[elementId];
|
||||
assertTrue(element->type == UI_ELEMENT_TYPE_LABEL, "Element is not a label");
|
||||
|
||||
element->label.color = color;
|
||||
}
|
||||
|
||||
void uiLabelUpdate(uielement_t *element) {
|
||||
assertNotNull(element, "Element cannot be NULL");
|
||||
uiElementUpdateChildren(element);
|
||||
}
|
||||
|
||||
void uiLabelRender(uielement_t *element) {
|
||||
|
||||
@@ -40,6 +40,14 @@ void uiLabelInit(uielement_t *element);
|
||||
*/
|
||||
void uiLabelSetText(const uielementid_t elementId, const char_t *text);
|
||||
|
||||
/**
|
||||
* Sets the label's text color.
|
||||
*
|
||||
* @param elementId The ID of the UI element to update as a label.
|
||||
* @param color The new text color.
|
||||
*/
|
||||
void uiLabelSetColor(const uielementid_t elementId, const color_t color);
|
||||
|
||||
/**
|
||||
* Updates a UI label element.
|
||||
*
|
||||
|
||||
@@ -50,6 +50,7 @@ void uiRectangleSetColor(const uielementid_t elementId, const color_t color) {
|
||||
|
||||
void uiRectangleUpdate(uielement_t *element) {
|
||||
assertNotNull(element, "Element cannot be NULL");
|
||||
uiElementUpdateChildren(element);
|
||||
}
|
||||
|
||||
void uiRectangleRender(uielement_t *element) {
|
||||
|
||||
@@ -13,6 +13,14 @@ dusktest(test_moduleconsole.c)
|
||||
|
||||
dusktest(test_moduleplatform.c)
|
||||
|
||||
dusktest(test_moduletime.c)
|
||||
|
||||
dusktest(test_modulescreen.c)
|
||||
|
||||
dusktest(test_modulelocale.c)
|
||||
|
||||
dusktest(test_modulesave.c)
|
||||
|
||||
dusktest(test_moduleinput.c)
|
||||
|
||||
dusktest(test_scriptinput.c)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "dusktest.h"
|
||||
#include "console/console.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "script/module/modulebase.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
static int console_setup(void **state) {
|
||||
@@ -53,6 +54,59 @@ static void test_console_print_no_args_prints_empty_line(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_console_line_count_is_history_max(void **state) {
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Console.lineCount;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), CONSOLE_HISTORY_MAX);
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_console_get_line_reads_history(void **state) {
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec(
|
||||
"Console.print('a line'); Console.getLine(Console.lineCount - 1);",
|
||||
&result
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
char_t buf[32];
|
||||
moduleBaseToString(result, buf, sizeof(buf));
|
||||
assert_string_equal(buf, "a line");
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_console_get_line_out_of_range_throws(void **state) {
|
||||
errorret_t ret = scriptManagerExec("Console.getLine(999);", NULL);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
static void test_console_visible_defaults_false(void **state) {
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Console.visible;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_false(jerry_value_is_true(result));
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_console_consume_dirty_clears_flag(void **state) {
|
||||
CONSOLE.dirty = true;
|
||||
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Console.consumeDirty();", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_true(jerry_value_is_true(result));
|
||||
jerry_value_free(result);
|
||||
|
||||
assert_false(CONSOLE.dirty);
|
||||
|
||||
ret = scriptManagerExec("Console.consumeDirty();", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_false(jerry_value_is_true(result));
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
@@ -64,6 +118,22 @@ int main(void) {
|
||||
test_console_print_no_args_prints_empty_line,
|
||||
console_setup, console_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_console_line_count_is_history_max, console_setup, console_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_console_get_line_reads_history, console_setup, console_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_console_get_line_out_of_range_throws,
|
||||
console_setup, console_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_console_visible_defaults_false, console_setup, console_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_console_consume_dirty_clears_flag, console_setup, console_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
||||
@@ -122,12 +122,29 @@ static void test_input_query_functions_default_to_zero(void **state) {
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_input_deadzone_readback(void **state) {
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Input.deadzone = 0.25;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
jerry_value_free(result);
|
||||
|
||||
assert_float_equal(INPUT.deadzone, 0.25f, 0.0001f);
|
||||
|
||||
ret = scriptManagerExec("Input.deadzone;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_float_equal((float_t)jerry_value_as_number(result), 0.25f, 0.0001f);
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_inputbind_exposes_action_ordinals, input_setup, input_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_input_deadzone_readback, input_setup, input_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_inputbutton_exposes_platform_button_names,
|
||||
input_setup, input_teardown
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "locale/localemanager.h"
|
||||
|
||||
static int locale_setup(void **state) {
|
||||
errorret_t ret = scriptManagerInit();
|
||||
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int locale_teardown(void **state) {
|
||||
errorret_t ret = scriptManagerDispose();
|
||||
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only the binding shape is covered here -- localeManagerInit() was never
|
||||
// called (LOCALE.locale stays NULL, as a fresh process leaves it), and an
|
||||
// actual successful Locale.set() requires the real asset pipeline (a
|
||||
// mounted locale/*.po file) which the main Dusk binary's headless run
|
||||
// exercises instead; a plain module test isn't the place to stand up the
|
||||
// full asset system just for this.
|
||||
|
||||
static void test_locale_current_defaults_to_empty_string(void **state) {
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Locale.current;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
char_t buf[8];
|
||||
jerry_string_to_buffer(
|
||||
result, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
|
||||
);
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_locale_set_unknown_name_throws(void **state) {
|
||||
errorret_t ret = scriptManagerExec("Locale.set('xx-XX');", NULL);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
static void test_locale_set_requires_a_string(void **state) {
|
||||
errorret_t ret = scriptManagerExec("Locale.set(42);", NULL);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_locale_current_defaults_to_empty_string,
|
||||
locale_setup, locale_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_locale_set_unknown_name_throws, locale_setup, locale_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_locale_set_requires_a_string, locale_setup, locale_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "save/save.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "input/input.h"
|
||||
|
||||
static int save_setup(void **state) {
|
||||
// Writes under ./saves relative to this test binary's own working
|
||||
// directory (SAVE_LINUX_PATH's default) -- a build artifact, not real
|
||||
// user save data.
|
||||
errorret_t ret = saveInit();
|
||||
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
|
||||
|
||||
ret = scriptManagerInit();
|
||||
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int save_teardown(void **state) {
|
||||
errorret_t ret = scriptManagerDispose();
|
||||
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||
|
||||
ret = saveDispose();
|
||||
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_save_write_persists_settings(void **state) {
|
||||
INPUT.deadzone = 0.42f;
|
||||
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Save.write();", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
jerry_value_free(result);
|
||||
|
||||
INPUT.deadzone = 0.0f;
|
||||
ret = saveSettingsLoad();
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_float_equal(INPUT.deadzone, 0.42f, 0.0001f);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_save_write_persists_settings, save_setup, save_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "display/screen/screen.h"
|
||||
|
||||
static int screen_setup(void **state) {
|
||||
errorret_t ret = scriptManagerInit();
|
||||
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int screen_teardown(void **state) {
|
||||
errorret_t ret = scriptManagerDispose();
|
||||
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_screen_dimensions_readback(void **state) {
|
||||
SCREEN.width = 640;
|
||||
SCREEN.height = 480;
|
||||
SCREEN.scanX = 10;
|
||||
SCREEN.scanY = 20;
|
||||
SCREEN.scanWidth = 600;
|
||||
SCREEN.scanHeight = 440;
|
||||
|
||||
jerry_value_t result;
|
||||
|
||||
errorret_t ret = scriptManagerExec("Screen.width;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), 640);
|
||||
jerry_value_free(result);
|
||||
|
||||
ret = scriptManagerExec("Screen.height;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), 480);
|
||||
jerry_value_free(result);
|
||||
|
||||
ret = scriptManagerExec("Screen.scanX;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), 10);
|
||||
jerry_value_free(result);
|
||||
|
||||
ret = scriptManagerExec("Screen.scanY;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), 20);
|
||||
jerry_value_free(result);
|
||||
|
||||
ret = scriptManagerExec("Screen.scanWidth;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), 600);
|
||||
jerry_value_free(result);
|
||||
|
||||
ret = scriptManagerExec("Screen.scanHeight;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int32_t)jerry_value_as_number(result), 440);
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_screen_dimensions_readback, screen_setup, screen_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "time/time.h"
|
||||
|
||||
static int time_setup(void **state) {
|
||||
errorret_t ret = scriptManagerInit();
|
||||
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int time_teardown(void **state) {
|
||||
errorret_t ret = scriptManagerDispose();
|
||||
if(errorIsNotOk(ret)) errorCatch(ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_time_delta_and_time_readback(void **state) {
|
||||
TIME.delta = 0.5f;
|
||||
TIME.time = 12.0f;
|
||||
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Time.delta;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_float_equal((float_t)jerry_value_as_number(result), 0.5f, 0.0001f);
|
||||
jerry_value_free(result);
|
||||
|
||||
ret = scriptManagerExec("Time.time;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_float_equal((float_t)jerry_value_as_number(result), 12.0f, 0.0001f);
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_time_render_delta_readback(void **state) {
|
||||
// Under DUSK_TIME_DYNAMIC, renderDelta reads dynamicDelta (real
|
||||
// per-render-frame time); otherwise it falls back to the fixed delta.
|
||||
// Set both so this test passes either way this binary was built.
|
||||
TIME.delta = 0.25f;
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
TIME.dynamicDelta = 0.75f;
|
||||
#endif
|
||||
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec("Time.renderDelta;", &result);
|
||||
assert_true(errorIsOk(ret));
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
assert_float_equal((float_t)jerry_value_as_number(result), 0.75f, 0.0001f);
|
||||
#else
|
||||
assert_float_equal((float_t)jerry_value_as_number(result), 0.25f, 0.0001f);
|
||||
#endif
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_time_delta_and_time_readback, time_setup, time_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_time_render_delta_readback, time_setup, time_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -17,6 +17,9 @@ static int ui_setup(void **state) {
|
||||
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;
|
||||
for(uint8_t i = 0; i < UI_OVERLAY_ROOT_MAX; i++) {
|
||||
UI.overlayRoot[i] = UI_ELEMENT_ID_INVALID;
|
||||
}
|
||||
|
||||
errorret_t ret = scriptManagerInit();
|
||||
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
|
||||
@@ -32,6 +35,17 @@ static int ui_teardown(void **state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void exec(const char_t *script) {
|
||||
jerry_value_t result;
|
||||
errorret_t ret = scriptManagerExec(script, &result);
|
||||
if(errorIsNotOk(ret)) {
|
||||
errorCatch(errorPrint(ret));
|
||||
fail_msg("Script execution failed (see printed error above)");
|
||||
}
|
||||
assert_true(errorIsOk(ret));
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_ui_add_requires_an_argument(void **state) {
|
||||
errorret_t ret = scriptManagerExec("UI.add();", NULL);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
@@ -65,6 +79,64 @@ static void test_ui_remove_of_untracked_element_is_a_noop(void **state) {
|
||||
jerry_value_free(result);
|
||||
}
|
||||
|
||||
static void test_ui_add_overlay_and_remove_overlay(void **state) {
|
||||
assert_true(UI.overlayRoot[0] == UI_ELEMENT_ID_INVALID);
|
||||
|
||||
exec(
|
||||
"globalThis.el = new UIElement();"
|
||||
"el.x = 3;"
|
||||
"UI.addOverlay(el);"
|
||||
);
|
||||
|
||||
assert_true(UI.overlayRoot[0] != UI_ELEMENT_ID_INVALID);
|
||||
assert_true(UI_ELEMENTS[UI.overlayRoot[0]].type == UI_ELEMENT_TYPE_SCRIPTED);
|
||||
assert_float_equal(UI_ELEMENTS[UI.overlayRoot[0]].x, 3.0f, 0.001f);
|
||||
|
||||
exec("UI.removeOverlay(el);");
|
||||
|
||||
assert_true(UI.overlayRoot[0] == UI_ELEMENT_ID_INVALID);
|
||||
}
|
||||
|
||||
static void test_ui_add_overlay_detaches_from_parent(void **state) {
|
||||
exec(
|
||||
"globalThis.parent = new UIElement();"
|
||||
"globalThis.child = new UIElement();"
|
||||
"parent.add(child);"
|
||||
"if(parent.childCount !== 1) throw new Error('expected 1 child');"
|
||||
"UI.addOverlay(child);"
|
||||
"if(parent.childCount !== 0) {"
|
||||
" throw new Error('expected 0 children after UI.addOverlay');"
|
||||
"}"
|
||||
"if(child.parent !== undefined) throw new Error('child.parent should be undefined');"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_ui_dispose_removes_overlay_root(void **state) {
|
||||
exec(
|
||||
"globalThis.el = new UIElement();"
|
||||
"UI.addOverlay(el);"
|
||||
);
|
||||
assert_true(UI.overlayRoot[0] != UI_ELEMENT_ID_INVALID);
|
||||
|
||||
exec("el.dispose();");
|
||||
assert_true(UI.overlayRoot[0] == UI_ELEMENT_ID_INVALID);
|
||||
}
|
||||
|
||||
static void test_ui_overlay_root_is_independent_of_root(void **state) {
|
||||
// A normal root and an overlay root are two separate arrays -- adding
|
||||
// to one must never touch the other.
|
||||
exec(
|
||||
"globalThis.a = new UIElement();"
|
||||
"globalThis.b = new UIElement();"
|
||||
"UI.add(a);"
|
||||
"UI.addOverlay(b);"
|
||||
);
|
||||
|
||||
assert_true(UI.root[0] != UI_ELEMENT_ID_INVALID);
|
||||
assert_true(UI.overlayRoot[0] != UI_ELEMENT_ID_INVALID);
|
||||
assert_true(UI.root[0] != UI.overlayRoot[0]);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
@@ -80,6 +152,18 @@ int main(void) {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_ui_remove_of_untracked_element_is_a_noop, ui_setup, ui_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_ui_add_overlay_and_remove_overlay, ui_setup, ui_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_ui_add_overlay_detaches_from_parent, ui_setup, ui_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_ui_dispose_removes_overlay_root, ui_setup, ui_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_ui_overlay_root_is_independent_of_root, ui_setup, ui_teardown
|
||||
),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,96 @@ static void test_uielement_render_children_explicit_opt_in(void **state) {
|
||||
);
|
||||
}
|
||||
|
||||
static void test_uielement_update_no_override_is_noop_when_childless(
|
||||
void **state
|
||||
) {
|
||||
// No override, no children -- update() should just do nothing, safely.
|
||||
exec("new UIElement().update();");
|
||||
}
|
||||
|
||||
static void test_uielement_update_dispatches_to_js_override(void **state) {
|
||||
exec(
|
||||
"class Foo extends UIElement {"
|
||||
" update() { this.updated = true; }"
|
||||
"}"
|
||||
"var foo = new Foo();"
|
||||
"foo.update();"
|
||||
"if(foo.updated !== true) throw new Error('update() override not called');"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_uielement_no_override_auto_updates_children(void **state) {
|
||||
exec(
|
||||
"class Child extends UIElement {"
|
||||
" update() { this.updated = true; }"
|
||||
"}"
|
||||
"var parent = new UIElement();"
|
||||
"var child = new Child();"
|
||||
"parent.add(child);"
|
||||
"parent.update();"
|
||||
"if(child.updated !== true) throw new Error('child was not auto-updated');"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_uielement_override_does_not_auto_update_children(
|
||||
void **state
|
||||
) {
|
||||
exec(
|
||||
"class Child extends UIElement {"
|
||||
" update() { this.updated = true; }"
|
||||
"}"
|
||||
"class Parent extends UIElement {"
|
||||
" update() { this.overrideCalled = true; }"
|
||||
"}"
|
||||
"var parent = new Parent();"
|
||||
"var child = new Child();"
|
||||
"parent.add(child);"
|
||||
"parent.update();"
|
||||
"if(parent.overrideCalled !== true) throw new Error('override not called');"
|
||||
"if(child.updated === true) {"
|
||||
" throw new Error('child should not auto-update under an override');"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_uielement_update_children_explicit_opt_in(void **state) {
|
||||
exec(
|
||||
"class Child extends UIElement {"
|
||||
" update() { this.updated = true; }"
|
||||
"}"
|
||||
"class Parent extends UIElement {"
|
||||
" update() { this.updateChildren(); }"
|
||||
"}"
|
||||
"var parent = new Parent();"
|
||||
"var child = new Child();"
|
||||
"parent.add(child);"
|
||||
"parent.update();"
|
||||
"if(child.updated !== true) throw new Error('updateChildren() did not update child');"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_ui_update_reaches_root_and_children(void **state) {
|
||||
exec(
|
||||
"globalThis.parent = new UIElement();"
|
||||
"globalThis.child = new UIElement();"
|
||||
"class Grandchild extends UIElement {"
|
||||
" update() { this.updated = true; }"
|
||||
"}"
|
||||
"globalThis.grandchild = new Grandchild();"
|
||||
"parent.add(child);"
|
||||
"child.add(grandchild);"
|
||||
"UI.add(parent);"
|
||||
);
|
||||
|
||||
errorCatch(errorPrint(uiUpdate()));
|
||||
|
||||
exec(
|
||||
"if(grandchild.updated !== true) {"
|
||||
" throw new Error('uiUpdate() did not reach a rooted grandchild');"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_uielement_add_self_throws(void **state) {
|
||||
execExpectError("var el = new UIElement(); el.add(el);");
|
||||
}
|
||||
@@ -252,6 +342,17 @@ static void test_label_text_default_and_set(void **state) {
|
||||
);
|
||||
}
|
||||
|
||||
static void test_label_color_readback(void **state) {
|
||||
exec(
|
||||
"var label = new Label();"
|
||||
"label.color = { r: 10, g: 20, b: 30, a: 40 };"
|
||||
"if(label.color.r !== 10 || label.color.g !== 20 ||"
|
||||
" label.color.b !== 30 || label.color.a !== 40) {"
|
||||
" throw new Error('color readback wrong');"
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_rectangle_size_and_color(void **state) {
|
||||
exec(
|
||||
"var rect = new Rectangle();"
|
||||
@@ -332,6 +433,30 @@ int main(void) {
|
||||
test_uielement_render_children_explicit_opt_in,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_uielement_update_no_override_is_noop_when_childless,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_uielement_update_dispatches_to_js_override,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_uielement_no_override_auto_updates_children,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_uielement_override_does_not_auto_update_children,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_uielement_update_children_explicit_opt_in,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_ui_update_reaches_root_and_children,
|
||||
uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_uielement_add_self_throws, uielement_setup, uielement_teardown
|
||||
),
|
||||
@@ -355,6 +480,9 @@ int main(void) {
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_label_text_default_and_set, uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_label_color_readback, uielement_setup, uielement_teardown
|
||||
),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
test_rectangle_size_and_color, uielement_setup, uielement_teardown
|
||||
),
|
||||
|
||||
Vendored
+2
@@ -14,4 +14,6 @@ declare class Label extends UIElement {
|
||||
|
||||
/** Display text. Rebuilds the label's glyph cache on assignment. */
|
||||
text: string;
|
||||
/** Text color, 0-255 per channel. */
|
||||
color: { r: number; g: number; b: number; a: number };
|
||||
}
|
||||
|
||||
Vendored
+15
@@ -99,6 +99,21 @@ declare class UIElement {
|
||||
* overridden render(). */
|
||||
renderChildren(): void;
|
||||
|
||||
/**
|
||||
* Called once per tick. The base implementation calls updateChildren()
|
||||
* and nothing else. Override it for per-tick logic (e.g. polling
|
||||
* input) -- an override does NOT get its children auto-updated; call
|
||||
* updateChildren() yourself if you still want them updated. Not called
|
||||
* automatically by the engine unless this element is a render root
|
||||
* (added via UI.add()) or a descendant of one.
|
||||
*/
|
||||
update(): void;
|
||||
|
||||
/** Updates every added child, in add-order. Called automatically by
|
||||
* the base update() -- only needed explicitly from inside an
|
||||
* overridden update(). */
|
||||
updateChildren(): void;
|
||||
|
||||
/** Releases this element's pool slot and cascades to every descendant
|
||||
* (each disposed the same way, recursively). Also removes it from
|
||||
* UI's render roots if it was added there. Safe to call more than
|
||||
|
||||
Reference in New Issue
Block a user