From 7ee04c78cd465447859b7a276f24c1eb8aeb4015 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 13 Aug 2026 11:13:23 -0500 Subject: [PATCH] Rebuild UI as a scriptable element tree, archive the old system The old UI system (X-macro static element list, hand-authored C screens/widgets/focus stack) is archived under archive/ rather than deleted, since it's a useful reference during the rewrite. New native UI element pool (src/dusk/ui): a flat UI_ELEMENTS[128] pool of tagged-union elements (Label, Rectangle, Scripted), a real persistent parent/child tree (children[8] per element, cycle- and capacity-checked uiElementSetParent), always-fresh worldX/worldY (cheap enough to recompute on every read, no dirty-flag cache needed), and cascading dispose. Rendering stays manual/immediate: a scripted element with no render() override auto-renders its children by default, but overriding render() takes full control (an override must call renderChildren() itself to opt back in) -- this is deliberately preserved end to end via a render()-shadow trampoline so overriding render() always keeps working the same way regardless of how a node is reached. New scripting layer (src/dusk/script/module/ui): UIElement/Label/ Rectangle JS classes (Label/Rectangle share UIElement's prototype via manual chaining, not JS `extends`), exposing x/y/worldX/worldY/parent/ add()/remove()/render()/renderChildren()/dispose(). UI.add()/ UI.remove() manage top-level render roots, mutually exclusive with being someone's child. Also: Scene gains a lateUpdate() hook (called once per frame after every other update, for things like camera-follow that need to react to where everything else ended up); several duskrpg call sites (cutscene items, entityinteractable, entityplayer) that depended on the now-archived RPG textbox are stubbed to console output instead of a dialogue box, pending the new UI reaching that far. Co-Authored-By: Claude Sonnet 5 --- archive/dusk/ui/CMakeLists.txt | 18 + {src => archive}/dusk/ui/debug/CMakeLists.txt | 0 {src => archive}/dusk/ui/debug/uiconsole.c | 0 {src => archive}/dusk/ui/debug/uiconsole.h | 0 {src => archive}/dusk/ui/debug/uifps.c | 0 {src => archive}/dusk/ui/debug/uifps.h | 0 {src => archive}/dusk/ui/focus/CMakeLists.txt | 0 {src => archive}/dusk/ui/focus/uifocus.c | 0 {src => archive}/dusk/ui/focus/uifocus.h | 0 {src => archive}/dusk/ui/focus/uifocusitem.h | 0 {src => archive}/dusk/ui/frame/CMakeLists.txt | 0 .../dusk/ui/frame/settings/CMakeLists.txt | 0 .../dusk/ui/frame/settings/uisettings.c | 0 .../dusk/ui/frame/settings/uisettings.h | 0 .../dusk/ui/frame/settings/uisettingsaudio.c | 0 .../dusk/ui/frame/settings/uisettingsaudio.h | 0 .../dusk/ui/frame/settings/uisettingsdata.h | 0 .../ui/frame/settings/uisettingsdisplay.c | 0 .../ui/frame/settings/uisettingsdisplay.h | 0 .../ui/frame/settings/uisettingsgeneral.c | 0 .../ui/frame/settings/uisettingsgeneral.h | 0 .../dusk/ui/frame/settings/uisettingsinput.c | 0 .../dusk/ui/frame/settings/uisettingsinput.h | 0 {src => archive}/dusk/ui/frame/uiconfirm.c | 0 {src => archive}/dusk/ui/frame/uiconfirm.h | 0 {src => archive}/dusk/ui/frame/uiframe.c | 0 {src => archive}/dusk/ui/frame/uiframe.h | 0 .../dusk/ui/overlay/CMakeLists.txt | 0 {src => archive}/dusk/ui/overlay/uicrop.c | 0 {src => archive}/dusk/ui/overlay/uicrop.h | 0 {src => archive}/dusk/ui/overlay/uifullbox.c | 0 {src => archive}/dusk/ui/overlay/uifullbox.h | 0 {src => archive}/dusk/ui/overlay/uiloading.c | 0 {src => archive}/dusk/ui/overlay/uiloading.h | 0 .../dusk/ui/transition/CMakeLists.txt | 0 .../dusk/ui/transition/uitransition.c | 0 .../dusk/ui/transition/uitransition.h | 0 .../dusk/ui/transition/uitransitiondata.h | 0 .../dusk/ui/transition/uitransitionfade.c | 0 .../dusk/ui/transition/uitransitionfade.h | 0 archive/dusk/ui/ui.c | 62 +++ archive/dusk/ui/ui.h | 41 ++ archive/dusk/ui/uielement.c | 86 ++++ archive/dusk/ui/uielement.h | 84 ++++ {src => archive}/dusk/ui/uielementlist.h | 0 archive/dusk/ui/widget/CMakeLists.txt | 17 + {src => archive}/dusk/ui/widget/uibutton.c | 0 {src => archive}/dusk/ui/widget/uibutton.h | 0 {src => archive}/dusk/ui/widget/uicheckbox.c | 0 {src => archive}/dusk/ui/widget/uicheckbox.h | 0 {src => archive}/dusk/ui/widget/uidropdown.c | 0 {src => archive}/dusk/ui/widget/uidropdown.h | 0 archive/dusk/ui/widget/uilabel.c | 70 ++++ archive/dusk/ui/widget/uilabel.h | 85 ++++ {src => archive}/dusk/ui/widget/uimenu.c | 0 {src => archive}/dusk/ui/widget/uimenu.h | 0 {src => archive}/dusk/ui/widget/uiscrolling.c | 0 {src => archive}/dusk/ui/widget/uiscrolling.h | 0 {src => archive}/dusk/ui/widget/uislider.c | 0 {src => archive}/dusk/ui/widget/uislider.h | 0 {src => archive}/dusk/ui/widget/uitab.c | 0 {src => archive}/dusk/ui/widget/uitab.h | 0 .../dusk/ui/widget/uiwidgetlabel.c | 0 .../dusk/ui/widget/uiwidgetlabel.h | 0 .../duskrpg/ui/textbox/CMakeLists.txt | 0 .../duskrpg/ui/textbox/uitextbox.c | 0 .../duskrpg/ui/textbox/uitextbox.h | 0 .../duskrpg/ui/textbox/uitextboxmain.c | 0 .../duskrpg/ui/textbox/uitextboxmain.h | 0 {src => archive}/duskrpg/ui/uitestlabel.c | 0 {src => archive}/duskrpg/ui/uitestlabel.h | 0 {test => archive/test}/ui/CMakeLists.txt | 0 {test => archive/test}/ui/test_uiframe.c | 0 {test => archive/test}/ui/test_uislider.c | 0 {test => archive/test}/ui/test_uitab.c | 0 assets/scripts/OverworldScene.js | 25 +- src/dusk/display/display.c | 1 + src/dusk/engine/engine.c | 1 + src/dusk/scene/scene.c | 36 -- src/dusk/script/module/modulelist.c | 9 + src/dusk/script/module/modulelist.h | 5 +- src/dusk/script/module/scene/modulescene.c | 19 + src/dusk/script/module/scene/modulescene.h | 13 + src/dusk/script/module/ui/CMakeLists.txt | 3 + src/dusk/script/module/ui/modulelabel.c | 55 +++ src/dusk/script/module/ui/modulelabel.h | 22 ++ src/dusk/script/module/ui/modulerectangle.c | 120 ++++++ src/dusk/script/module/ui/modulerectangle.h | 23 ++ src/dusk/script/module/ui/moduleui.c | 52 +-- src/dusk/script/module/ui/moduleui.h | 11 +- src/dusk/script/module/ui/moduleuielement.c | 361 +++++++++++++++++ src/dusk/script/module/ui/moduleuielement.h | 92 +++++ src/dusk/ui/CMakeLists.txt | 9 +- src/dusk/ui/ui.c | 104 +++-- src/dusk/ui/ui.h | 32 +- src/dusk/ui/uibase.h | 13 + src/dusk/ui/uielement.c | 279 ++++++++++--- src/dusk/ui/uielement.h | 180 ++++++--- src/dusk/ui/widget/CMakeLists.txt | 9 +- src/dusk/ui/widget/uilabel.c | 72 ++-- src/dusk/ui/widget/uilabel.h | 67 ++-- src/dusk/ui/widget/uirectangle.c | 84 ++++ src/dusk/ui/widget/uirectangle.h | 69 ++++ .../item/entity/cutsceneentityitempickup.c | 10 +- src/duskrpg/cutscene/item/ui/cutscenetext.c | 7 +- .../component/overworld/entityinteractable.c | 14 +- .../entity/component/overworld/entityplayer.c | 5 +- src/duskrpg/ui/CMakeLists.txt | 7 - test/CMakeLists.txt | 1 - test/script/CMakeLists.txt | 2 + test/script/test_moduleui.c | 72 ++-- test/script/test_moduleuielement.c | 369 ++++++++++++++++++ types/index.d.ts | 3 + types/scene/scene.d.ts | 14 +- types/ui/label.d.ts | 17 + types/ui/rectangle.d.ts | 21 + types/ui/ui.d.ts | 15 +- types/ui/uielement.d.ts | 107 +++++ 118 files changed, 2502 insertions(+), 391 deletions(-) create mode 100644 archive/dusk/ui/CMakeLists.txt rename {src => archive}/dusk/ui/debug/CMakeLists.txt (100%) rename {src => archive}/dusk/ui/debug/uiconsole.c (100%) rename {src => archive}/dusk/ui/debug/uiconsole.h (100%) rename {src => archive}/dusk/ui/debug/uifps.c (100%) rename {src => archive}/dusk/ui/debug/uifps.h (100%) rename {src => archive}/dusk/ui/focus/CMakeLists.txt (100%) rename {src => archive}/dusk/ui/focus/uifocus.c (100%) rename {src => archive}/dusk/ui/focus/uifocus.h (100%) rename {src => archive}/dusk/ui/focus/uifocusitem.h (100%) rename {src => archive}/dusk/ui/frame/CMakeLists.txt (100%) rename {src => archive}/dusk/ui/frame/settings/CMakeLists.txt (100%) rename {src => archive}/dusk/ui/frame/settings/uisettings.c (100%) rename {src => archive}/dusk/ui/frame/settings/uisettings.h (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsaudio.c (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsaudio.h (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsdata.h (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsdisplay.c (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsdisplay.h (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsgeneral.c (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsgeneral.h (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsinput.c (100%) rename {src => archive}/dusk/ui/frame/settings/uisettingsinput.h (100%) rename {src => archive}/dusk/ui/frame/uiconfirm.c (100%) rename {src => archive}/dusk/ui/frame/uiconfirm.h (100%) rename {src => archive}/dusk/ui/frame/uiframe.c (100%) rename {src => archive}/dusk/ui/frame/uiframe.h (100%) rename {src => archive}/dusk/ui/overlay/CMakeLists.txt (100%) rename {src => archive}/dusk/ui/overlay/uicrop.c (100%) rename {src => archive}/dusk/ui/overlay/uicrop.h (100%) rename {src => archive}/dusk/ui/overlay/uifullbox.c (100%) rename {src => archive}/dusk/ui/overlay/uifullbox.h (100%) rename {src => archive}/dusk/ui/overlay/uiloading.c (100%) rename {src => archive}/dusk/ui/overlay/uiloading.h (100%) rename {src => archive}/dusk/ui/transition/CMakeLists.txt (100%) rename {src => archive}/dusk/ui/transition/uitransition.c (100%) rename {src => archive}/dusk/ui/transition/uitransition.h (100%) rename {src => archive}/dusk/ui/transition/uitransitiondata.h (100%) rename {src => archive}/dusk/ui/transition/uitransitionfade.c (100%) rename {src => archive}/dusk/ui/transition/uitransitionfade.h (100%) create mode 100644 archive/dusk/ui/ui.c create mode 100644 archive/dusk/ui/ui.h create mode 100644 archive/dusk/ui/uielement.c create mode 100644 archive/dusk/ui/uielement.h rename {src => archive}/dusk/ui/uielementlist.h (100%) create mode 100644 archive/dusk/ui/widget/CMakeLists.txt rename {src => archive}/dusk/ui/widget/uibutton.c (100%) rename {src => archive}/dusk/ui/widget/uibutton.h (100%) rename {src => archive}/dusk/ui/widget/uicheckbox.c (100%) rename {src => archive}/dusk/ui/widget/uicheckbox.h (100%) rename {src => archive}/dusk/ui/widget/uidropdown.c (100%) rename {src => archive}/dusk/ui/widget/uidropdown.h (100%) create mode 100644 archive/dusk/ui/widget/uilabel.c create mode 100644 archive/dusk/ui/widget/uilabel.h rename {src => archive}/dusk/ui/widget/uimenu.c (100%) rename {src => archive}/dusk/ui/widget/uimenu.h (100%) rename {src => archive}/dusk/ui/widget/uiscrolling.c (100%) rename {src => archive}/dusk/ui/widget/uiscrolling.h (100%) rename {src => archive}/dusk/ui/widget/uislider.c (100%) rename {src => archive}/dusk/ui/widget/uislider.h (100%) rename {src => archive}/dusk/ui/widget/uitab.c (100%) rename {src => archive}/dusk/ui/widget/uitab.h (100%) rename {src => archive}/dusk/ui/widget/uiwidgetlabel.c (100%) rename {src => archive}/dusk/ui/widget/uiwidgetlabel.h (100%) rename {src => archive}/duskrpg/ui/textbox/CMakeLists.txt (100%) rename {src => archive}/duskrpg/ui/textbox/uitextbox.c (100%) rename {src => archive}/duskrpg/ui/textbox/uitextbox.h (100%) rename {src => archive}/duskrpg/ui/textbox/uitextboxmain.c (100%) rename {src => archive}/duskrpg/ui/textbox/uitextboxmain.h (100%) rename {src => archive}/duskrpg/ui/uitestlabel.c (100%) rename {src => archive}/duskrpg/ui/uitestlabel.h (100%) rename {test => archive/test}/ui/CMakeLists.txt (100%) rename {test => archive/test}/ui/test_uiframe.c (100%) rename {test => archive/test}/ui/test_uislider.c (100%) rename {test => archive/test}/ui/test_uitab.c (100%) create mode 100644 src/dusk/script/module/ui/modulelabel.c create mode 100644 src/dusk/script/module/ui/modulelabel.h create mode 100644 src/dusk/script/module/ui/modulerectangle.c create mode 100644 src/dusk/script/module/ui/modulerectangle.h create mode 100644 src/dusk/script/module/ui/moduleuielement.c create mode 100644 src/dusk/script/module/ui/moduleuielement.h create mode 100644 src/dusk/ui/uibase.h create mode 100644 src/dusk/ui/widget/uirectangle.c create mode 100644 src/dusk/ui/widget/uirectangle.h create mode 100644 test/script/test_moduleuielement.c create mode 100644 types/ui/label.d.ts create mode 100644 types/ui/rectangle.d.ts create mode 100644 types/ui/uielement.d.ts diff --git a/archive/dusk/ui/CMakeLists.txt b/archive/dusk/ui/CMakeLists.txt new file mode 100644 index 00000000..c7b76cd4 --- /dev/null +++ b/archive/dusk/ui/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(debug) +add_subdirectory(frame) +add_subdirectory(focus) +add_subdirectory(overlay) +add_subdirectory(transition) +add_subdirectory(widget) + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + ui.c + uielement.c +) \ No newline at end of file diff --git a/src/dusk/ui/debug/CMakeLists.txt b/archive/dusk/ui/debug/CMakeLists.txt similarity index 100% rename from src/dusk/ui/debug/CMakeLists.txt rename to archive/dusk/ui/debug/CMakeLists.txt diff --git a/src/dusk/ui/debug/uiconsole.c b/archive/dusk/ui/debug/uiconsole.c similarity index 100% rename from src/dusk/ui/debug/uiconsole.c rename to archive/dusk/ui/debug/uiconsole.c diff --git a/src/dusk/ui/debug/uiconsole.h b/archive/dusk/ui/debug/uiconsole.h similarity index 100% rename from src/dusk/ui/debug/uiconsole.h rename to archive/dusk/ui/debug/uiconsole.h diff --git a/src/dusk/ui/debug/uifps.c b/archive/dusk/ui/debug/uifps.c similarity index 100% rename from src/dusk/ui/debug/uifps.c rename to archive/dusk/ui/debug/uifps.c diff --git a/src/dusk/ui/debug/uifps.h b/archive/dusk/ui/debug/uifps.h similarity index 100% rename from src/dusk/ui/debug/uifps.h rename to archive/dusk/ui/debug/uifps.h diff --git a/src/dusk/ui/focus/CMakeLists.txt b/archive/dusk/ui/focus/CMakeLists.txt similarity index 100% rename from src/dusk/ui/focus/CMakeLists.txt rename to archive/dusk/ui/focus/CMakeLists.txt diff --git a/src/dusk/ui/focus/uifocus.c b/archive/dusk/ui/focus/uifocus.c similarity index 100% rename from src/dusk/ui/focus/uifocus.c rename to archive/dusk/ui/focus/uifocus.c diff --git a/src/dusk/ui/focus/uifocus.h b/archive/dusk/ui/focus/uifocus.h similarity index 100% rename from src/dusk/ui/focus/uifocus.h rename to archive/dusk/ui/focus/uifocus.h diff --git a/src/dusk/ui/focus/uifocusitem.h b/archive/dusk/ui/focus/uifocusitem.h similarity index 100% rename from src/dusk/ui/focus/uifocusitem.h rename to archive/dusk/ui/focus/uifocusitem.h diff --git a/src/dusk/ui/frame/CMakeLists.txt b/archive/dusk/ui/frame/CMakeLists.txt similarity index 100% rename from src/dusk/ui/frame/CMakeLists.txt rename to archive/dusk/ui/frame/CMakeLists.txt diff --git a/src/dusk/ui/frame/settings/CMakeLists.txt b/archive/dusk/ui/frame/settings/CMakeLists.txt similarity index 100% rename from src/dusk/ui/frame/settings/CMakeLists.txt rename to archive/dusk/ui/frame/settings/CMakeLists.txt diff --git a/src/dusk/ui/frame/settings/uisettings.c b/archive/dusk/ui/frame/settings/uisettings.c similarity index 100% rename from src/dusk/ui/frame/settings/uisettings.c rename to archive/dusk/ui/frame/settings/uisettings.c diff --git a/src/dusk/ui/frame/settings/uisettings.h b/archive/dusk/ui/frame/settings/uisettings.h similarity index 100% rename from src/dusk/ui/frame/settings/uisettings.h rename to archive/dusk/ui/frame/settings/uisettings.h diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.c b/archive/dusk/ui/frame/settings/uisettingsaudio.c similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsaudio.c rename to archive/dusk/ui/frame/settings/uisettingsaudio.c diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.h b/archive/dusk/ui/frame/settings/uisettingsaudio.h similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsaudio.h rename to archive/dusk/ui/frame/settings/uisettingsaudio.h diff --git a/src/dusk/ui/frame/settings/uisettingsdata.h b/archive/dusk/ui/frame/settings/uisettingsdata.h similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsdata.h rename to archive/dusk/ui/frame/settings/uisettingsdata.h diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.c b/archive/dusk/ui/frame/settings/uisettingsdisplay.c similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsdisplay.c rename to archive/dusk/ui/frame/settings/uisettingsdisplay.c diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.h b/archive/dusk/ui/frame/settings/uisettingsdisplay.h similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsdisplay.h rename to archive/dusk/ui/frame/settings/uisettingsdisplay.h diff --git a/src/dusk/ui/frame/settings/uisettingsgeneral.c b/archive/dusk/ui/frame/settings/uisettingsgeneral.c similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsgeneral.c rename to archive/dusk/ui/frame/settings/uisettingsgeneral.c diff --git a/src/dusk/ui/frame/settings/uisettingsgeneral.h b/archive/dusk/ui/frame/settings/uisettingsgeneral.h similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsgeneral.h rename to archive/dusk/ui/frame/settings/uisettingsgeneral.h diff --git a/src/dusk/ui/frame/settings/uisettingsinput.c b/archive/dusk/ui/frame/settings/uisettingsinput.c similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsinput.c rename to archive/dusk/ui/frame/settings/uisettingsinput.c diff --git a/src/dusk/ui/frame/settings/uisettingsinput.h b/archive/dusk/ui/frame/settings/uisettingsinput.h similarity index 100% rename from src/dusk/ui/frame/settings/uisettingsinput.h rename to archive/dusk/ui/frame/settings/uisettingsinput.h diff --git a/src/dusk/ui/frame/uiconfirm.c b/archive/dusk/ui/frame/uiconfirm.c similarity index 100% rename from src/dusk/ui/frame/uiconfirm.c rename to archive/dusk/ui/frame/uiconfirm.c diff --git a/src/dusk/ui/frame/uiconfirm.h b/archive/dusk/ui/frame/uiconfirm.h similarity index 100% rename from src/dusk/ui/frame/uiconfirm.h rename to archive/dusk/ui/frame/uiconfirm.h diff --git a/src/dusk/ui/frame/uiframe.c b/archive/dusk/ui/frame/uiframe.c similarity index 100% rename from src/dusk/ui/frame/uiframe.c rename to archive/dusk/ui/frame/uiframe.c diff --git a/src/dusk/ui/frame/uiframe.h b/archive/dusk/ui/frame/uiframe.h similarity index 100% rename from src/dusk/ui/frame/uiframe.h rename to archive/dusk/ui/frame/uiframe.h diff --git a/src/dusk/ui/overlay/CMakeLists.txt b/archive/dusk/ui/overlay/CMakeLists.txt similarity index 100% rename from src/dusk/ui/overlay/CMakeLists.txt rename to archive/dusk/ui/overlay/CMakeLists.txt diff --git a/src/dusk/ui/overlay/uicrop.c b/archive/dusk/ui/overlay/uicrop.c similarity index 100% rename from src/dusk/ui/overlay/uicrop.c rename to archive/dusk/ui/overlay/uicrop.c diff --git a/src/dusk/ui/overlay/uicrop.h b/archive/dusk/ui/overlay/uicrop.h similarity index 100% rename from src/dusk/ui/overlay/uicrop.h rename to archive/dusk/ui/overlay/uicrop.h diff --git a/src/dusk/ui/overlay/uifullbox.c b/archive/dusk/ui/overlay/uifullbox.c similarity index 100% rename from src/dusk/ui/overlay/uifullbox.c rename to archive/dusk/ui/overlay/uifullbox.c diff --git a/src/dusk/ui/overlay/uifullbox.h b/archive/dusk/ui/overlay/uifullbox.h similarity index 100% rename from src/dusk/ui/overlay/uifullbox.h rename to archive/dusk/ui/overlay/uifullbox.h diff --git a/src/dusk/ui/overlay/uiloading.c b/archive/dusk/ui/overlay/uiloading.c similarity index 100% rename from src/dusk/ui/overlay/uiloading.c rename to archive/dusk/ui/overlay/uiloading.c diff --git a/src/dusk/ui/overlay/uiloading.h b/archive/dusk/ui/overlay/uiloading.h similarity index 100% rename from src/dusk/ui/overlay/uiloading.h rename to archive/dusk/ui/overlay/uiloading.h diff --git a/src/dusk/ui/transition/CMakeLists.txt b/archive/dusk/ui/transition/CMakeLists.txt similarity index 100% rename from src/dusk/ui/transition/CMakeLists.txt rename to archive/dusk/ui/transition/CMakeLists.txt diff --git a/src/dusk/ui/transition/uitransition.c b/archive/dusk/ui/transition/uitransition.c similarity index 100% rename from src/dusk/ui/transition/uitransition.c rename to archive/dusk/ui/transition/uitransition.c diff --git a/src/dusk/ui/transition/uitransition.h b/archive/dusk/ui/transition/uitransition.h similarity index 100% rename from src/dusk/ui/transition/uitransition.h rename to archive/dusk/ui/transition/uitransition.h diff --git a/src/dusk/ui/transition/uitransitiondata.h b/archive/dusk/ui/transition/uitransitiondata.h similarity index 100% rename from src/dusk/ui/transition/uitransitiondata.h rename to archive/dusk/ui/transition/uitransitiondata.h diff --git a/src/dusk/ui/transition/uitransitionfade.c b/archive/dusk/ui/transition/uitransitionfade.c similarity index 100% rename from src/dusk/ui/transition/uitransitionfade.c rename to archive/dusk/ui/transition/uitransitionfade.c diff --git a/src/dusk/ui/transition/uitransitionfade.h b/archive/dusk/ui/transition/uitransitionfade.h similarity index 100% rename from src/dusk/ui/transition/uitransitionfade.h rename to archive/dusk/ui/transition/uitransitionfade.h diff --git a/archive/dusk/ui/ui.c b/archive/dusk/ui/ui.c new file mode 100644 index 00000000..e875be04 --- /dev/null +++ b/archive/dusk/ui/ui.c @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "ui.h" +#include "util/memory.h" +#include "assert/assert.h" +#include "display/spritebatch/spritebatch.h" +#include "display/screen/screen.h" +#include "ui/uielement.h" +#include "ui/focus/uifocus.h" + +ui_t UI; + +errorret_t uiInit(void) { + memoryZero(&UI, sizeof(ui_t)); + uiFocusInit(); + uiElementsSort(); + + uielement_t *element = &UI_ELEMENTS[0]; + while(!uiElementIsNull(element)) { + errorChain(uiElementInit(element)); + element++; + } + + errorOk(); +} + +errorret_t uiUpdate(void) { + uiFocusUpdate(); + + uielement_t *element = &UI_ELEMENTS[0]; + while(!uiElementIsNull(element)) { + errorChain(uiElementUpdate(element)); + element++; + } + + errorOk(); +} + +errorret_t uiRender(void) { + const uielement_t *element = &UI_ELEMENTS[0]; + while(!uiElementIsNull(element)) { + errorChain(uiElementDraw(element)); + element++; + } + + errorChain(spriteBatchFlush()); + errorOk(); +} + +errorret_t uiDispose(void) { + uielement_t *element = &UI_ELEMENTS[0]; + while(!uiElementIsNull(element)) { + errorChain(uiElementDispose(element)); + element++; + } + errorOk(); +} \ No newline at end of file diff --git a/archive/dusk/ui/ui.h b/archive/dusk/ui/ui.h new file mode 100644 index 00000000..2887a262 --- /dev/null +++ b/archive/dusk/ui/ui.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct { + void *nothing; +} ui_t; + +extern ui_t UI; + +/** + * Initializes the UI system. + */ +errorret_t uiInit(void); + +/** + * Updates the UI system. + * + * @return Any error that occurs. + */ +errorret_t uiUpdate(void); + +/** + * Renders the UI system. + * + * @return Any error that occurs. + */ +errorret_t uiRender(void); + +/** + * Disposes of the UI system. + * + * @return Any error that occurs. + */ +errorret_t uiDispose(void); \ No newline at end of file diff --git a/archive/dusk/ui/uielement.c b/archive/dusk/ui/uielement.c new file mode 100644 index 00000000..f119f534 --- /dev/null +++ b/archive/dusk/ui/uielement.c @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uielement.h" +#include "assert/assert.h" +#include "util/sort.h" +#include "ui/frame/uiframe.h" +#include "ui/debug/uifps.h" +#include "engine/engine.h" +#include "ui/overlay/uifullbox.h" +#include "ui/overlay/uiloading.h" +#include "ui/overlay/uicrop.h" +#include "ui/transition/uitransition.h" +#include "ui/debug/uiconsole.h" +#include "ui/frame/uiconfirm.h" + +// Priming pass: X does nothing here, so this only exists to process any +// #include directives nested in uielementlist.h/ui/uielementgame.h at +// file scope (each such header's own #pragma once makes it a no-op on +// the real pass below, which happens inside UI_ELEMENTS[]'s braces, +// where a raw #include of a declaration would be invalid). +#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing +#include "uielementlist.h" +#undef X + +uielement_t UI_ELEMENTS[] = { + #define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \ + { \ + .init = initFn, .update = updateFn, .draw = drawFn, \ + .dispose = disposeFn, .order = elementOrder \ + }, + #include "uielementlist.h" + #undef X + + { 0 } // Null terminator +}; + +bool_t uiElementIsNull(const uielement_t *element) { + return element->init == NULL && + element->update == NULL && + element->draw == NULL && + element->dispose == NULL; +} + +int_t uiElementCompareOrder(const void *a, const void *b) { + const uielement_t *elementA = (const uielement_t *)a; + const uielement_t *elementB = (const uielement_t *)b; + if(elementA->order < elementB->order) return -1; + if(elementA->order > elementB->order) return 1; + return 0; +} + +void uiElementsSort(void) { + // The trailing null terminator is always the last static entry -- sort + // everything before it, and leave it in place. + const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1; + sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder); +} + +errorret_t uiElementInit(uielement_t *element) { + assertNotNull(element, "element must not be NULL"); + if(element->init != NULL) errorChain(element->init()); + errorOk(); +} + +errorret_t uiElementUpdate(uielement_t *element) { + assertNotNull(element, "element must not be NULL"); + if(element->update != NULL) errorChain(element->update()); + errorOk(); +} + +errorret_t uiElementDraw(const uielement_t *element) { + assertNotNull(element, "element must not be NULL"); + if(element->draw != NULL) errorChain(element->draw()); + errorOk(); +} + +errorret_t uiElementDispose(uielement_t *element) { + assertNotNull(element, "element must not be NULL"); + if(element->dispose != NULL) errorChain(element->dispose()); + errorOk(); +} diff --git a/archive/dusk/ui/uielement.h b/archive/dusk/ui/uielement.h new file mode 100644 index 00000000..d9b8a525 --- /dev/null +++ b/archive/dusk/ui/uielement.h @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +// Built-in order tiers. Lower values update/render first; ties preserve +// their relative UI_ELEMENTS declaration order (uiElementsSort uses a +// stable sort). Game-specific elements can use any int32_t value -- these +// are just the ones the engine itself relies on. +#define UI_ELEMENT_ORDER_DEFAULT 0 +#define UI_ELEMENT_ORDER_DEBUG 1000 + +typedef struct { + errorret_t (*init)(); + errorret_t (*update)(); + errorret_t (*draw)(); + errorret_t (*dispose)(); + int32_t order; +} uielement_t; + +extern uielement_t UI_ELEMENTS[]; + +/** + * Returns true when all four callbacks on the element are NULL, + * which marks the end of the UI_ELEMENTS array. + * + * @param element The element to test. + * @returns True if the element is the null terminator. + */ +bool_t uiElementIsNull(const uielement_t *element); + +/** + * Compares two elements by their .order field, ascending. Matches + * sortcompare_t, for use with the project's sort utilities. + * + * @param a First uielement_t to compare. + * @param b Second uielement_t to compare. + * @return Negative if a < b, zero if a == b, positive if a > b. + */ +int_t uiElementCompareOrder(const void *a, const void *b); + +/** + * Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing + * null terminator is never moved. Called once by uiInit -- element order + * is static after that, so there's no need to re-sort every frame. + */ +void uiElementsSort(void); + +/** + * Initializes a UI element, invoking its init callback if set. + * + * @param element The element to initialize. + * @return Any error that occurs. + */ +errorret_t uiElementInit(uielement_t *element); + +/** + * Updates a UI element, calling its update callback if set. + * + * @param element The element to update. + * @return Any error that occurs. + */ +errorret_t uiElementUpdate(uielement_t *element); + +/** + * Draws a UI element, calling its draw callback if set. + * + * @param element The element to render. + * @return Any error that occurs. + */ +errorret_t uiElementDraw(const uielement_t *element); + +/** + * Disposes of a UI element, invoking its dispose callback if set. + * + * @param element The element to dispose. + * @return Any error that occurs. + */ +errorret_t uiElementDispose(uielement_t *element); diff --git a/src/dusk/ui/uielementlist.h b/archive/dusk/ui/uielementlist.h similarity index 100% rename from src/dusk/ui/uielementlist.h rename to archive/dusk/ui/uielementlist.h diff --git a/archive/dusk/ui/widget/CMakeLists.txt b/archive/dusk/ui/widget/CMakeLists.txt new file mode 100644 index 00000000..6beebee4 --- /dev/null +++ b/archive/dusk/ui/widget/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + uibutton.c + uicheckbox.c + uitab.c + uislider.c + uidropdown.c + uiscrolling.c + uimenu.c + uilabel.c + uiwidgetlabel.c +) diff --git a/src/dusk/ui/widget/uibutton.c b/archive/dusk/ui/widget/uibutton.c similarity index 100% rename from src/dusk/ui/widget/uibutton.c rename to archive/dusk/ui/widget/uibutton.c diff --git a/src/dusk/ui/widget/uibutton.h b/archive/dusk/ui/widget/uibutton.h similarity index 100% rename from src/dusk/ui/widget/uibutton.h rename to archive/dusk/ui/widget/uibutton.h diff --git a/src/dusk/ui/widget/uicheckbox.c b/archive/dusk/ui/widget/uicheckbox.c similarity index 100% rename from src/dusk/ui/widget/uicheckbox.c rename to archive/dusk/ui/widget/uicheckbox.c diff --git a/src/dusk/ui/widget/uicheckbox.h b/archive/dusk/ui/widget/uicheckbox.h similarity index 100% rename from src/dusk/ui/widget/uicheckbox.h rename to archive/dusk/ui/widget/uicheckbox.h diff --git a/src/dusk/ui/widget/uidropdown.c b/archive/dusk/ui/widget/uidropdown.c similarity index 100% rename from src/dusk/ui/widget/uidropdown.c rename to archive/dusk/ui/widget/uidropdown.c diff --git a/src/dusk/ui/widget/uidropdown.h b/archive/dusk/ui/widget/uidropdown.h similarity index 100% rename from src/dusk/ui/widget/uidropdown.h rename to archive/dusk/ui/widget/uidropdown.h diff --git a/archive/dusk/ui/widget/uilabel.c b/archive/dusk/ui/widget/uilabel.c new file mode 100644 index 00000000..53afb233 --- /dev/null +++ b/archive/dusk/ui/widget/uilabel.c @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uilabel.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "display/text/text.h" + +void uiLabelInit(uilabel_t *label, font_t *font) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(font, "Font cannot be NULL"); + + memoryZero(label, sizeof(uilabel_t)); + label->font = font; + label->color = COLOR_WHITE; +} + +void uiLabelSetText(uilabel_t *label, const char_t *text) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(text, "Text cannot be NULL"); + assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long"); + + stringCopy(label->text, text, UI_LABEL_TEXT_MAX); + label->spriteCount = textBuildSpriteCache( + label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX, + &label->width, &label->height + ); +} + +void uiLabelSetColor(uilabel_t *label, const color_t color) { + assertNotNull(label, "Label cannot be NULL"); + label->color = color; +} + +void uiLabelGetSize( + const uilabel_t *label, + int32_t *outWidth, + int32_t *outHeight +) { + assertNotNull(label, "Label cannot be NULL"); + assertNotNull(outWidth, "Output width cannot be NULL"); + assertNotNull(outHeight, "Output height cannot be NULL"); + *outWidth = label->width; + *outHeight = label->height; +} + +errorret_t uiLabelDraw( + const uilabel_t *label, + const float_t x, + const float_t y +) { + assertNotNull(label, "Label cannot be NULL"); + if(label->spriteCount == 0) errorOk(); + + // Cached sprites are relative to (0,0); textDrawSpriteCache translates + // into the requested screen position here instead of in + // uiLabelSetText, so a label can be repositioned every frame without + // rebuilding the (much more expensive) glyph/UV cache. + spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX]; + errorChain(textDrawSpriteCache( + label->sprites, label->spriteCount, scratch, x, y, label->color, + label->font->texture + )); + errorOk(); +} diff --git a/archive/dusk/ui/widget/uilabel.h b/archive/dusk/ui/widget/uilabel.h new file mode 100644 index 00000000..d0f6e10d --- /dev/null +++ b/archive/dusk/ui/widget/uilabel.h @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "display/text/font.h" +#include "display/spritebatch/spritebatchsprite.h" +#include "display/color.h" + +#define UI_LABEL_TEXT_MAX 256 +#define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX + +typedef struct { + char_t text[UI_LABEL_TEXT_MAX]; + color_t color; + font_t *font; + + spritebatchsprite_t sprites[UI_LABEL_SPRITE_COUNT_MAX]; + uint32_t spriteCount; + int32_t width; + int32_t height; +} uilabel_t; + +/** + * Initializes a label, defaulting to white text and no text set. + * + * @param label The label to initialize. + * @param font Font to use for rendering. Must outlive the label. + */ +void uiLabelInit(uilabel_t *label, font_t *font); + +/** + * Sets the label's text, rebuilding its cached sprites (glyph lookup, UV, + * and layout) immediately. This is the only way the sprite cache gets + * rebuilt -- uiLabelDraw never recomputes it, so call this whenever the + * text changes rather than once up front and expecting it to stay in sync. + * + * @param label The label to update. + * @param text Null-terminated string to display. Must be shorter than + * UI_LABEL_TEXT_MAX. + */ +void uiLabelSetText(uilabel_t *label, const char_t *text); + +/** + * Sets the label's tint color. Cheap -- doesn't touch the sprite cache, + * since color is applied via the draw material, not baked into sprites. + * + * @param label The label to update. + * @param color The new tint color. + */ +void uiLabelSetColor(uilabel_t *label, const color_t color); + +/** + * Gets the measured size (in pixels) of the label's current text, cached + * from the last uiLabelSetText call. + * + * @param label The label to query. + * @param outWidth Pointer to store the width. + * @param outHeight Pointer to store the height. + */ +void uiLabelGetSize( + const uilabel_t *label, + int32_t *outWidth, + int32_t *outHeight +); + +/** + * Draws the label's cached sprites at the given screen position in a + * single batched buffer call. Does not recompute glyph layout -- call + * uiLabelSetText first whenever the text changes. + * + * @param label The label to draw. + * @param x Screen x position. + * @param y Screen y position. + * @return Any error that occurs. + */ +errorret_t uiLabelDraw( + const uilabel_t *label, + const float_t x, + const float_t y +); diff --git a/src/dusk/ui/widget/uimenu.c b/archive/dusk/ui/widget/uimenu.c similarity index 100% rename from src/dusk/ui/widget/uimenu.c rename to archive/dusk/ui/widget/uimenu.c diff --git a/src/dusk/ui/widget/uimenu.h b/archive/dusk/ui/widget/uimenu.h similarity index 100% rename from src/dusk/ui/widget/uimenu.h rename to archive/dusk/ui/widget/uimenu.h diff --git a/src/dusk/ui/widget/uiscrolling.c b/archive/dusk/ui/widget/uiscrolling.c similarity index 100% rename from src/dusk/ui/widget/uiscrolling.c rename to archive/dusk/ui/widget/uiscrolling.c diff --git a/src/dusk/ui/widget/uiscrolling.h b/archive/dusk/ui/widget/uiscrolling.h similarity index 100% rename from src/dusk/ui/widget/uiscrolling.h rename to archive/dusk/ui/widget/uiscrolling.h diff --git a/src/dusk/ui/widget/uislider.c b/archive/dusk/ui/widget/uislider.c similarity index 100% rename from src/dusk/ui/widget/uislider.c rename to archive/dusk/ui/widget/uislider.c diff --git a/src/dusk/ui/widget/uislider.h b/archive/dusk/ui/widget/uislider.h similarity index 100% rename from src/dusk/ui/widget/uislider.h rename to archive/dusk/ui/widget/uislider.h diff --git a/src/dusk/ui/widget/uitab.c b/archive/dusk/ui/widget/uitab.c similarity index 100% rename from src/dusk/ui/widget/uitab.c rename to archive/dusk/ui/widget/uitab.c diff --git a/src/dusk/ui/widget/uitab.h b/archive/dusk/ui/widget/uitab.h similarity index 100% rename from src/dusk/ui/widget/uitab.h rename to archive/dusk/ui/widget/uitab.h diff --git a/src/dusk/ui/widget/uiwidgetlabel.c b/archive/dusk/ui/widget/uiwidgetlabel.c similarity index 100% rename from src/dusk/ui/widget/uiwidgetlabel.c rename to archive/dusk/ui/widget/uiwidgetlabel.c diff --git a/src/dusk/ui/widget/uiwidgetlabel.h b/archive/dusk/ui/widget/uiwidgetlabel.h similarity index 100% rename from src/dusk/ui/widget/uiwidgetlabel.h rename to archive/dusk/ui/widget/uiwidgetlabel.h diff --git a/src/duskrpg/ui/textbox/CMakeLists.txt b/archive/duskrpg/ui/textbox/CMakeLists.txt similarity index 100% rename from src/duskrpg/ui/textbox/CMakeLists.txt rename to archive/duskrpg/ui/textbox/CMakeLists.txt diff --git a/src/duskrpg/ui/textbox/uitextbox.c b/archive/duskrpg/ui/textbox/uitextbox.c similarity index 100% rename from src/duskrpg/ui/textbox/uitextbox.c rename to archive/duskrpg/ui/textbox/uitextbox.c diff --git a/src/duskrpg/ui/textbox/uitextbox.h b/archive/duskrpg/ui/textbox/uitextbox.h similarity index 100% rename from src/duskrpg/ui/textbox/uitextbox.h rename to archive/duskrpg/ui/textbox/uitextbox.h diff --git a/src/duskrpg/ui/textbox/uitextboxmain.c b/archive/duskrpg/ui/textbox/uitextboxmain.c similarity index 100% rename from src/duskrpg/ui/textbox/uitextboxmain.c rename to archive/duskrpg/ui/textbox/uitextboxmain.c diff --git a/src/duskrpg/ui/textbox/uitextboxmain.h b/archive/duskrpg/ui/textbox/uitextboxmain.h similarity index 100% rename from src/duskrpg/ui/textbox/uitextboxmain.h rename to archive/duskrpg/ui/textbox/uitextboxmain.h diff --git a/src/duskrpg/ui/uitestlabel.c b/archive/duskrpg/ui/uitestlabel.c similarity index 100% rename from src/duskrpg/ui/uitestlabel.c rename to archive/duskrpg/ui/uitestlabel.c diff --git a/src/duskrpg/ui/uitestlabel.h b/archive/duskrpg/ui/uitestlabel.h similarity index 100% rename from src/duskrpg/ui/uitestlabel.h rename to archive/duskrpg/ui/uitestlabel.h diff --git a/test/ui/CMakeLists.txt b/archive/test/ui/CMakeLists.txt similarity index 100% rename from test/ui/CMakeLists.txt rename to archive/test/ui/CMakeLists.txt diff --git a/test/ui/test_uiframe.c b/archive/test/ui/test_uiframe.c similarity index 100% rename from test/ui/test_uiframe.c rename to archive/test/ui/test_uiframe.c diff --git a/test/ui/test_uislider.c b/archive/test/ui/test_uislider.c similarity index 100% rename from test/ui/test_uislider.c rename to archive/test/ui/test_uislider.c diff --git a/test/ui/test_uitab.c b/archive/test/ui/test_uitab.c similarity index 100% rename from test/ui/test_uitab.c rename to archive/test/ui/test_uitab.c diff --git a/assets/scripts/OverworldScene.js b/assets/scripts/OverworldScene.js index 1d1cb505..5e4f8284 100644 --- a/assets/scripts/OverworldScene.js +++ b/assets/scripts/OverworldScene.js @@ -4,22 +4,26 @@ var TestPlane = require('./TestPlane.js'); class OverworldScene { constructor() { - this.time = 0; } init() { this.player = new Player(); this.plane = new TestPlane(); this.camera = new PlayerCamera(this.player); + + this.rect = new Rectangle(); + this.rect.color = { r: 255, g: 0, b: 0, a: 255 }; + this.rect.width = 100; + this.rect.height = 100; + UI.add(this.rect); + + this.label = new Label(); + this.label.text = 'Hello World!'; + this.rect.add(this.label); } - update() { + lateUpdate() { this.camera.update(); - - this.time += Time.delta; - if(this.time > 3.0) { - Scene.set(new OverworldScene()); - } } dispose() { @@ -27,9 +31,16 @@ class OverworldScene { this.player.dispose(); this.plane.dispose(); + // rect.dispose() cascades to label (added as its child) and removes + // itself from UI's render roots -- no separate UI.remove()/dispose() + // needed for either. + this.rect.dispose(); + this.camera = null; this.player = null; this.plane = null; + this.label = null; + this.rect = null; } } diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c index 4e514306..8baef16f 100644 --- a/src/dusk/display/display.c +++ b/src/dusk/display/display.c @@ -80,6 +80,7 @@ errorret_t displayUpdate(void) { ); errorChain(sceneRender()); + errorChain(uiRender()); // Finish up screenUnbind(); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 9e96dcaa..ca993fc2 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -90,6 +90,7 @@ errorret_t engineUpdate(void) { errorChain(sceneUpdate()); errorChain(assetUpdate()); errorChain(uiUpdate()); + errorChain(moduleSceneLateUpdateCurrent()); } // Render diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 19731e80..9d1931dd 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -14,7 +14,6 @@ #include "entity/component/display/entityposition.h" #include "entity/component/display/entityrenderable.h" #include "physics/triggersystem.h" -#include "ui/ui.h" #include "console/console.h" scenemanager_t SCENE_MANAGER; @@ -148,41 +147,6 @@ errorret_t sceneRender(void) { } } - // Screen-space matrices for UI rendering. - mat4 screenIdentity; - mat4 screenProj; - mat4 screenView; - - glm_mat4_identity(screenIdentity); - - glm_ortho( - 0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi), - (float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f, - 0.1f, 100.0f, - screenProj - ); - - glm_lookat( - (vec3){ 0.0f, 0.0f, 1.0f }, - (vec3){ 0.0f, 0.0f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f }, - screenView - ); - - errorChain(shaderBind(&SHADER_UNLIT)); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_MODEL, screenIdentity - )); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, screenProj - )); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, screenView)); - - errorChain(displaySetState((displaystate_t){ - .flags = DISPLAY_STATE_FLAG_BLEND - })); - errorChain(uiRender()); - errorOk(); } diff --git a/src/dusk/script/module/modulelist.c b/src/dusk/script/module/modulelist.c index ae6ce14e..e887d97c 100644 --- a/src/dusk/script/module/modulelist.c +++ b/src/dusk/script/module/modulelist.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/ui/moduleuielement.h" +#include "script/module/ui/modulelabel.h" +#include "script/module/ui/modulerectangle.h" #include "script/module/ui/moduleui.h" void moduleListInit(void) { @@ -25,6 +28,9 @@ void moduleListInit(void) { moduleTimeInit(); moduleConsoleInit(); moduleInputInit(); + moduleUiElementInit(); + moduleLabelInit(); + moduleRectangleInit(); moduleUiInit(); moduleComponentInit(); moduleComponentListInit(); @@ -38,6 +44,9 @@ void moduleListDispose(void) { moduleComponentListDispose(); moduleComponentDispose(); moduleUiDispose(); + moduleRectangleDispose(); + moduleLabelDispose(); + moduleUiElementDispose(); moduleInputDispose(); moduleConsoleDispose(); moduleTimeDispose(); diff --git a/src/dusk/script/module/modulelist.h b/src/dusk/script/module/modulelist.h index 1c24b597..928ebdbe 100644 --- a/src/dusk/script/module/modulelist.h +++ b/src/dusk/script/module/modulelist.h @@ -9,8 +9,9 @@ /** * Registers every script module (Component + its typed per-type - * wrappers, Entity, Scene, require(), Mesh, Time, Console, Input, UI, - * the platform globals). Called once by scriptManagerInit(). + * wrappers, Entity, Scene, require(), Mesh, Time, Console, Input, + * UIElement + Label/Rectangle + UI, the platform globals). Called once + * by scriptManagerInit(). */ void moduleListInit(void); diff --git a/src/dusk/script/module/scene/modulescene.c b/src/dusk/script/module/scene/modulescene.c index 08206752..440cd052 100644 --- a/src/dusk/script/module/scene/modulescene.c +++ b/src/dusk/script/module/scene/modulescene.c @@ -160,3 +160,22 @@ errorret_t moduleSceneUpdateCurrent(void) { errorChain(ret); errorOk(); } + +errorret_t moduleSceneLateUpdateCurrent(void) { + if(MODULE_SCENE_CURRENT_ID == SCENE_ID_INVALID) errorOk(); + + jerry_value_t lateUpdateFn = moduleBaseGetProp( + MODULE_SCENE_CURRENT, "lateUpdate" + ); + if(!jerry_value_is_function(lateUpdateFn)) { + jerry_value_free(lateUpdateFn); + errorOk(); + } + + errorret_t ret = scriptManagerCallValue( + MODULE_SCENE_CURRENT, lateUpdateFn, "Scene module", "lateUpdate" + ); + jerry_value_free(lateUpdateFn); + errorChain(ret); + errorOk(); +} diff --git a/src/dusk/script/module/scene/modulescene.h b/src/dusk/script/module/scene/modulescene.h index c44b9b3d..73a23535 100644 --- a/src/dusk/script/module/scene/modulescene.h +++ b/src/dusk/script/module/scene/modulescene.h @@ -59,3 +59,16 @@ void moduleSceneTeardownCurrent(void); * throws, or if it returns a rejected promise. */ errorret_t moduleSceneUpdateCurrent(void); + +/** + * Calls lateUpdate() on whichever module Scene.set() last installed, if + * any, and if it defines one. No-op if Scene.set() has never been + * called. Called once per frame by engineUpdate(), after every other + * update (entities, physics, UI, etc.) has already run -- useful for + * things like camera follow that need to react to where everything + * else ended up this frame, rather than where it was at the start. + * + * @return The error return value. An error is thrown if lateUpdate() + * itself throws, or if it returns a rejected promise. + */ +errorret_t moduleSceneLateUpdateCurrent(void); diff --git a/src/dusk/script/module/ui/CMakeLists.txt b/src/dusk/script/module/ui/CMakeLists.txt index a2c2bf0f..d8465ab9 100644 --- a/src/dusk/script/module/ui/CMakeLists.txt +++ b/src/dusk/script/module/ui/CMakeLists.txt @@ -5,5 +5,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + moduleuielement.c + modulelabel.c + modulerectangle.c moduleui.c ) diff --git a/src/dusk/script/module/ui/modulelabel.c b/src/dusk/script/module/ui/modulelabel.c new file mode 100644 index 00000000..bce9bcd1 --- /dev/null +++ b/src/dusk/script/module/ui/modulelabel.c @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulelabel.h" +#include "moduleuielement.h" +#include "script/module/modulebase.h" +#include "ui/widget/uilabel.h" + +static scriptproto_t MODULE_LABEL_PROTO; + +moduleBaseFunction(moduleLabelConstructor) { + errorret_t ret = moduleUiElementConstructShared( + callInfo->this_value, UI_ELEMENT_TYPE_LABEL + ); + if(errorIsNotOk(ret)) return moduleBaseThrowError(ret); + return jerry_undefined(); +} + +moduleBaseFunction(moduleLabelGetText) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_string_sz(UI_ELEMENTS[h->id].label.text); +} + +moduleBaseFunction(moduleLabelSetText) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + moduleBaseRequireArgs(1); moduleBaseRequireString(0); + + char_t buffer[UI_LABEL_TEXT_MAX]; + moduleBaseToString(args[0], buffer, sizeof(buffer)); + uiLabelSetText(h->id, buffer); + return jerry_undefined(); +} + +void moduleLabelInit(void) { + scriptProtoInit( + &MODULE_LABEL_PROTO, + "Label", + sizeof(moduleuielementhandle_t), + moduleLabelConstructor + ); + jerry_object_set_proto( + MODULE_LABEL_PROTO.prototype, MODULE_UI_ELEMENT_PROTO.prototype + ); + + scriptProtoDefineProp( + &MODULE_LABEL_PROTO, "text", moduleLabelGetText, moduleLabelSetText + ); +} + +void moduleLabelDispose(void) { +} diff --git a/src/dusk/script/module/ui/modulelabel.h b/src/dusk/script/module/ui/modulelabel.h new file mode 100644 index 00000000..1c7ce28a --- /dev/null +++ b/src/dusk/script/module/ui/modulelabel.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +/** + * Registers the Label class: `new Label()` allocates a + * UI_ELEMENT_TYPE_LABEL pool element. Its prototype chains to + * UIElement.prototype (see moduleuielement.h), so x/y/worldX/worldY/ + * parent/render/dispose all work on a Label unmodified; Label itself + * only adds a `text` property (read/write). + */ +void moduleLabelInit(void); + +/** + * Disposes the Label class's script resources. + */ +void moduleLabelDispose(void); diff --git a/src/dusk/script/module/ui/modulerectangle.c b/src/dusk/script/module/ui/modulerectangle.c new file mode 100644 index 00000000..d2eab690 --- /dev/null +++ b/src/dusk/script/module/ui/modulerectangle.c @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "modulerectangle.h" +#include "moduleuielement.h" +#include "script/module/modulebase.h" +#include "ui/widget/uirectangle.h" +#include "display/color.h" + +static scriptproto_t MODULE_RECTANGLE_PROTO; + +static jerry_value_t moduleRectangleColorToObject(const color_t c) { + jerry_value_t obj = jerry_object(); + moduleBaseObjectSetNumber(obj, "r", c.r); + moduleBaseObjectSetNumber(obj, "g", c.g); + moduleBaseObjectSetNumber(obj, "b", c.b); + moduleBaseObjectSetNumber(obj, "a", c.a); + return obj; +} + +static color_t moduleRectangleColorFromObject(const jerry_value_t obj) { + color_t c = COLOR_WHITE; + + jerry_value_t rVal = moduleBaseGetProp(obj, "r"); + jerry_value_t gVal = moduleBaseGetProp(obj, "g"); + jerry_value_t bVal = moduleBaseGetProp(obj, "b"); + jerry_value_t aVal = moduleBaseGetProp(obj, "a"); + + if(jerry_value_is_number(rVal)) c.r = (colorchannel8_t)moduleBaseValueInt(rVal); + if(jerry_value_is_number(gVal)) c.g = (colorchannel8_t)moduleBaseValueInt(gVal); + if(jerry_value_is_number(bVal)) c.b = (colorchannel8_t)moduleBaseValueInt(bVal); + if(jerry_value_is_number(aVal)) c.a = (colorchannel8_t)moduleBaseValueInt(aVal); + + jerry_value_free(rVal); + jerry_value_free(gVal); + jerry_value_free(bVal); + jerry_value_free(aVal); + return c; +} + +moduleBaseFunction(moduleRectangleConstructor) { + errorret_t ret = moduleUiElementConstructShared( + callInfo->this_value, UI_ELEMENT_TYPE_RECTANGLE + ); + if(errorIsNotOk(ret)) return moduleBaseThrowError(ret); + return jerry_undefined(); +} + +moduleBaseFunction(moduleRectangleGetWidth) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_number(UI_ELEMENTS[h->id].rectangle.width); +} + +moduleBaseFunction(moduleRectangleSetWidth) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + + uirectangle_t *rect = &UI_ELEMENTS[h->id].rectangle; + uiRectangleSetSize(h->id, moduleBaseArgFloat(0), rect->height); + return jerry_undefined(); +} + +moduleBaseFunction(moduleRectangleGetHeight) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_number(UI_ELEMENTS[h->id].rectangle.height); +} + +moduleBaseFunction(moduleRectangleSetHeight) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + + uirectangle_t *rect = &UI_ELEMENTS[h->id].rectangle; + uiRectangleSetSize(h->id, rect->width, moduleBaseArgFloat(0)); + return jerry_undefined(); +} + +moduleBaseFunction(moduleRectangleGetColor) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return moduleRectangleColorToObject(UI_ELEMENTS[h->id].rectangle.color); +} + +moduleBaseFunction(moduleRectangleSetColor) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); + + uiRectangleSetColor(h->id, moduleRectangleColorFromObject(args[0])); + return jerry_undefined(); +} + +void moduleRectangleInit(void) { + scriptProtoInit( + &MODULE_RECTANGLE_PROTO, + "Rectangle", + sizeof(moduleuielementhandle_t), + moduleRectangleConstructor + ); + jerry_object_set_proto( + MODULE_RECTANGLE_PROTO.prototype, MODULE_UI_ELEMENT_PROTO.prototype + ); + + scriptProtoDefineProp( + &MODULE_RECTANGLE_PROTO, "width", + moduleRectangleGetWidth, moduleRectangleSetWidth + ); + scriptProtoDefineProp( + &MODULE_RECTANGLE_PROTO, "height", + moduleRectangleGetHeight, moduleRectangleSetHeight + ); + scriptProtoDefineProp( + &MODULE_RECTANGLE_PROTO, "color", + moduleRectangleGetColor, moduleRectangleSetColor + ); +} + +void moduleRectangleDispose(void) { +} diff --git a/src/dusk/script/module/ui/modulerectangle.h b/src/dusk/script/module/ui/modulerectangle.h new file mode 100644 index 00000000..4e266e29 --- /dev/null +++ b/src/dusk/script/module/ui/modulerectangle.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +/** + * Registers the Rectangle class: `new Rectangle()` allocates a + * UI_ELEMENT_TYPE_RECTANGLE pool element. Its prototype chains to + * UIElement.prototype (see moduleuielement.h), so x/y/worldX/worldY/ + * parent/render/dispose all work on a Rectangle unmodified; Rectangle + * itself adds width/height (read/write, in screen space) and color + * (read/write, a plain {r,g,b,a} object, 0-255 per channel). + */ +void moduleRectangleInit(void); + +/** + * Disposes the Rectangle class's script resources. + */ +void moduleRectangleDispose(void); diff --git a/src/dusk/script/module/ui/moduleui.c b/src/dusk/script/module/ui/moduleui.c index 16034273..167b8f70 100644 --- a/src/dusk/script/module/ui/moduleui.c +++ b/src/dusk/script/module/ui/moduleui.c @@ -6,50 +6,32 @@ */ #include "moduleui.h" +#include "moduleuielement.h" #include "script/module/modulebase.h" -#include "console/console.h" +#include "ui/ui.h" -// Stub: stringify " " (like Console.print) and forward to -// consolePrint(), so UI.add/UI.remove are visibly wired up before there's -// any real UI system behind them. -static jerry_value_t moduleUiLogCall( - const char_t *name, - const jerry_value_t args[], - const jerry_length_t argc -) { - char_t buffer[CONSOLE_LINE_MAX]; - size_t pos = strlen(name); - if(pos >= CONSOLE_LINE_MAX) pos = CONSOLE_LINE_MAX - 1; - memoryCopy(buffer, name, pos); +moduleBaseFunction(moduleUiAddFn) { + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); - for(jerry_length_t i = 0; i < argc; i++) { - jerry_value_t strVal = jerry_value_to_string(args[i]); - if(jerry_value_is_exception(strVal)) return strVal; + moduleuielementhandle_t *h = moduleUiElementGetFromValue(args[0]); + if(!h) return moduleBaseThrow("Expected a UIElement"); - if(pos + 1 < CONSOLE_LINE_MAX) buffer[pos++] = ' '; - - jerry_size_t written = jerry_string_to_buffer( - strVal, JERRY_ENCODING_UTF8, - (jerry_char_t *)(buffer + pos), (jerry_size_t)(CONSOLE_LINE_MAX - pos - 1) - ); - jerry_value_free(strVal); - pos += written; - } - buffer[pos] = '\0'; - - // Fixed "%s" format -- buffer is arbitrary JS-provided text and must - // never be used as the format string itself. - consolePrint("%s", buffer); + // A root and a child are mutually exclusive -- promoting an existing + // child to a root detaches it from its parent first. + uiElementSetParent(h->id, UI_ELEMENT_ID_INVALID); + if(!uiRootAdd(h->id)) return moduleBaseThrow("UI root capacity exceeded"); return jerry_undefined(); } -moduleBaseFunction(moduleUiAddFn) { - return moduleUiLogCall("UI.add", args, argc); -} - moduleBaseFunction(moduleUiRemoveFn) { - return moduleUiLogCall("UI.remove", args, argc); + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); + + moduleuielementhandle_t *h = moduleUiElementGetFromValue(args[0]); + if(!h) return moduleBaseThrow("Expected a UIElement"); + + uiRootRemove(h->id); + return jerry_undefined(); } void moduleUiInit(void) { diff --git a/src/dusk/script/module/ui/moduleui.h b/src/dusk/script/module/ui/moduleui.h index 0b636fd5..5d5adfd1 100644 --- a/src/dusk/script/module/ui/moduleui.h +++ b/src/dusk/script/module/ui/moduleui.h @@ -8,10 +8,13 @@ #pragma once /** - * Registers the global `UI` object, exposing `UI.add(...)` and - * `UI.remove(...)`. Both are stubs for now -- they stringify and - * space-join their arguments (like Console.print) and forward the - * result to consolePrint(), prefixed with the called method's name. + * Registers the global `UI` object, exposing `UI.add(element)` and + * `UI.remove(element)`. Both take a UIElement (or Label/Rectangle/etc.) + * instance and add/remove it from the engine's render roots (UI.root[] + * in ui/ui.h) -- root elements get their render() called once per frame + * by uiRender(); everything else only renders as part of a root's + * children (see UIElement.add()). A root and a child are mutually + * exclusive: UI.add() detaches the element from any parent first. */ void moduleUiInit(void); diff --git a/src/dusk/script/module/ui/moduleuielement.c b/src/dusk/script/module/ui/moduleuielement.c new file mode 100644 index 00000000..eaaff4b7 --- /dev/null +++ b/src/dusk/script/module/ui/moduleuielement.c @@ -0,0 +1,361 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "moduleuielement.h" +#include "script/module/modulebase.h" +#include "script/scriptmanager.h" +#include "util/memory.h" + +scriptproto_t MODULE_UI_ELEMENT_PROTO; + +// Registered JS instance per pool slot, so the SCRIPTED render callback +// and the parent property can call back into whatever JS object owns a +// given element id. Indices with no registered instance hold undefined. +static jerry_value_t UI_ELEMENT_JS_INSTANCES[UI_ELEMENT_COUNT_MAX]; + +// Own (instance, not prototype) property that a construction-time +// override wrap stashes the (sub)class's real render() under -- see +// moduleUiElementConstructShared and moduleUiElementRenderScripted. +#define MODULE_UI_ELEMENT_USER_RENDER_PROP "__uiElementUserRender" + +static bool_t moduleUiElementStrictEqual( + const jerry_value_t a, + const jerry_value_t b +) { + jerry_value_t result = jerry_binary_op(JERRY_BIN_OP_STRICT_EQUAL, a, b); + bool_t equal = jerry_value_is_true(result); + jerry_value_free(result); + return equal; +} + +static jerry_value_t moduleUiElementRenderMethod( + const jerry_call_info_t *callInfo, + const jerry_value_t args[], + const jerry_length_t argc +); + +// UI_ELEMENT_TYPE_SCRIPTED's render callback: calls the (sub)class's own +// render(), stashed at construction time under +// MODULE_UI_ELEMENT_USER_RENDER_PROP -- see moduleUiElementConstructShared. +// If the instance never had one stashed (it never overrode +// UIElement.prototype's own (native) render in the first place), the +// default behavior is to auto-render whatever was added() -- an override +// takes full manual control and must call renderChildren() itself if it +// still wants added children drawn. +static void moduleUiElementRenderScripted(uielement_t *element) { + jerry_value_t instance = UI_ELEMENT_JS_INSTANCES[element->id]; + jerry_value_t renderFn = jerry_value_is_object(instance) + ? moduleBaseGetProp(instance, MODULE_UI_ELEMENT_USER_RENDER_PROP) + : jerry_undefined(); + + if(jerry_value_is_function(renderFn)) { + errorret_t ret = scriptManagerCallValue( + instance, renderFn, "UIElement", "render" + ); + errorCatch(errorPrint(ret)); + } else { + uiElementRenderChildren(element); + } + + jerry_value_free(renderFn); +} + +// Calling instance.render() (JS method-call syntax) only reaches native +// code if the property lookup actually resolves to +// UIElement.prototype.render -- if a (sub)class overrides render(), that +// override shadows it and callers bypass uiElementRender() entirely, +// which is what actually tracks parent/worldX/worldY. So if this +// instance's resolved render is anything other than the base one, stash +// the override under MODULE_UI_ELEMENT_USER_RENDER_PROP and shadow +// render itself (as an own property, just on this instance) with the +// native trampoline -- moduleUiElementRenderScripted then calls the +// stashed override from inside uiElementRender(), fully wired up. +static void moduleUiElementWrapUserRenderIfOverridden( + const jerry_value_t thisValue +) { + jerry_value_t resolvedRender = moduleBaseGetProp(thisValue, "render"); + jerry_value_t baseRender = moduleBaseGetProp( + MODULE_UI_ELEMENT_PROTO.prototype, "render" + ); + + if( + jerry_value_is_function(resolvedRender) && + !moduleUiElementStrictEqual(resolvedRender, baseRender) + ) { + jerry_value_t userRenderKey = jerry_string_sz( + MODULE_UI_ELEMENT_USER_RENDER_PROP + ); + jerry_object_set(thisValue, userRenderKey, resolvedRender); + jerry_value_free(userRenderKey); + + jerry_value_t trampoline = jerry_function_external( + moduleUiElementRenderMethod + ); + jerry_value_t renderKey = jerry_string_sz("render"); + jerry_object_set(thisValue, renderKey, trampoline); + jerry_value_free(renderKey); + jerry_value_free(trampoline); + } + + jerry_value_free(baseRender); + jerry_value_free(resolvedRender); +} + +errorret_t moduleUiElementConstructShared( + const jerry_value_t thisValue, + const uielementtype_t type +) { + moduleuielementhandle_t *inst = (moduleuielementhandle_t *)memoryAllocate( + sizeof(moduleuielementhandle_t) + ); + inst->id = uiElementGetAvailable(); + uiElementInit(inst->id, type); + + jerry_object_set_native_ptr( + thisValue, &MODULE_UI_ELEMENT_PROTO.info, inst + ); + UI_ELEMENT_JS_INSTANCES[inst->id] = jerry_value_copy(thisValue); + moduleUiElementWrapUserRenderIfOverridden(thisValue); + + jerry_value_t initFn = moduleBaseGetProp(thisValue, "init"); + if(jerry_value_is_function(initFn)) { + errorret_t ret = scriptManagerCallValue( + thisValue, initFn, "UIElement", "init" + ); + jerry_value_free(initFn); + errorChain(ret); + errorOk(); + } + jerry_value_free(initFn); + errorOk(); +} + +moduleBaseFunction(moduleUiElementConstructor) { + errorret_t ret = moduleUiElementConstructShared( + callInfo->this_value, UI_ELEMENT_TYPE_SCRIPTED + ); + if(errorIsNotOk(ret)) return moduleBaseThrowError(ret); + return jerry_undefined(); +} + +moduleuielementhandle_t *moduleUiElementGetFromValue(const jerry_value_t val) { + return (moduleuielementhandle_t *)scriptProtoGetValue( + &MODULE_UI_ELEMENT_PROTO, val + ); +} + +moduleuielementhandle_t *moduleUiElementGet( + const jerry_call_info_t *callInfo +) { + return moduleUiElementGetFromValue(callInfo->this_value); +} + +jerry_value_t moduleUiElementGetInstance(const uielementid_t elementId) { + return UI_ELEMENT_JS_INSTANCES[elementId]; +} + +moduleBaseFunction(moduleUiElementGetId) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_number(h->id); +} + +moduleBaseFunction(moduleUiElementGetX) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_number(UI_ELEMENTS[h->id].x); +} + +moduleBaseFunction(moduleUiElementSetX) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + UI_ELEMENTS[h->id].x = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleUiElementGetY) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_number(UI_ELEMENTS[h->id].y); +} + +moduleBaseFunction(moduleUiElementSetY) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + UI_ELEMENTS[h->id].y = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleUiElementGetWorldX) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + uiElementUpdateWorld(&UI_ELEMENTS[h->id]); + return jerry_number(UI_ELEMENTS[h->id].worldX); +} + +moduleBaseFunction(moduleUiElementGetWorldY) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + uiElementUpdateWorld(&UI_ELEMENTS[h->id]); + return jerry_number(UI_ELEMENTS[h->id].worldY); +} + +moduleBaseFunction(moduleUiElementGetParent) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + + uielementid_t parentId = UI_ELEMENTS[h->id].parent; + if(parentId == UI_ELEMENT_ID_INVALID) return jerry_undefined(); + + jerry_value_t parentInstance = moduleUiElementGetInstance(parentId); + if(!jerry_value_is_object(parentInstance)) return jerry_undefined(); + return jerry_value_copy(parentInstance); +} + +moduleBaseFunction(moduleUiElementGetChildCount) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + return jerry_number(UI_ELEMENTS[h->id].childCount); +} + +moduleBaseFunction(moduleUiElementAddMethod) { + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + + moduleuielementhandle_t *childH = moduleUiElementGetFromValue(args[0]); + if(!childH) return moduleBaseThrow("UIElement.add: expected a UIElement"); + if(childH->id == h->id) { + return moduleBaseThrow("UIElement.add: cannot add an element to itself"); + } + if(uiElementIsAncestorOf(childH->id, h->id)) { + return moduleBaseThrow("UIElement.add: would create a cycle"); + } + if(!uiElementSetParent(childH->id, h->id)) { + return moduleBaseThrow("UIElement.add: child capacity exceeded"); + } + + return jerry_value_copy(args[0]); +} + +moduleBaseFunction(moduleUiElementRemoveMethod) { + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + + moduleuielementhandle_t *childH = moduleUiElementGetFromValue(args[0]); + if(!childH) return moduleBaseThrow("UIElement.remove: expected a UIElement"); + + if(UI_ELEMENTS[childH->id].parent == h->id) { + uiElementSetParent(childH->id, UI_ELEMENT_ID_INVALID); + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleUiElementRenderChildrenMethod) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + uiElementRenderChildren(&UI_ELEMENTS[h->id]); + return jerry_undefined(); +} + +moduleBaseFunction(moduleUiElementRenderMethod) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + uiElementRender(h->id); + return jerry_undefined(); +} + +// Frees the registered JS instance for elementId and every descendant +// (walked via the still-intact children[]/childCount), so the registry +// never points at an about-to-be-disposed id. Must run BEFORE +// uiElementDisposeDeep, which zeroes each struct as it goes. +static void moduleUiElementReleaseInstanceTree(const uielementid_t elementId) { + uielement_t *element = &UI_ELEMENTS[elementId]; + + jerry_value_t instance = UI_ELEMENT_JS_INSTANCES[elementId]; + if(jerry_value_is_object(instance)) jerry_value_free(instance); + UI_ELEMENT_JS_INSTANCES[elementId] = jerry_undefined(); + + for(uint8_t i = 0; i < element->childCount; i++) { + moduleUiElementReleaseInstanceTree(element->children[i]); + } +} + +moduleBaseFunction(moduleUiElementDisposeMethod) { + moduleBaseGetOrReturn(moduleuielementhandle_t, h, moduleUiElementGet); + + // Idempotency: this id's registry slot must still be this exact + // instance. If it's not (already disposed, or the pool slot was + // reused by something else entirely), there's nothing to do -- this + // is what makes cascading dispose safe to call redundantly, e.g. a + // script that still manually disposes a child after its parent + // already cascaded to it. + if( + !moduleUiElementStrictEqual( + UI_ELEMENT_JS_INSTANCES[h->id], callInfo->this_value + ) + ) { + return jerry_undefined(); + } + + moduleUiElementReleaseInstanceTree(h->id); + uiElementDisposeDeep(h->id); + return jerry_undefined(); +} + +void moduleUiElementInit(void) { + for(uielementid_t i = 0; i < UI_ELEMENT_COUNT_MAX; i++) { + UI_ELEMENT_JS_INSTANCES[i] = jerry_undefined(); + } + + scriptProtoInit( + &MODULE_UI_ELEMENT_PROTO, + "UIElement", + sizeof(moduleuielementhandle_t), + moduleUiElementConstructor + ); + + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "id", moduleUiElementGetId, NULL + ); + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "x", moduleUiElementGetX, moduleUiElementSetX + ); + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "y", moduleUiElementGetY, moduleUiElementSetY + ); + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "worldX", moduleUiElementGetWorldX, NULL + ); + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "worldY", moduleUiElementGetWorldY, NULL + ); + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "parent", moduleUiElementGetParent, NULL + ); + scriptProtoDefineProp( + &MODULE_UI_ELEMENT_PROTO, "childCount", moduleUiElementGetChildCount, NULL + ); + scriptProtoDefineFunc( + &MODULE_UI_ELEMENT_PROTO, "add", moduleUiElementAddMethod + ); + scriptProtoDefineFunc( + &MODULE_UI_ELEMENT_PROTO, "remove", moduleUiElementRemoveMethod + ); + scriptProtoDefineFunc( + &MODULE_UI_ELEMENT_PROTO, "renderChildren", moduleUiElementRenderChildrenMethod + ); + scriptProtoDefineFunc( + &MODULE_UI_ELEMENT_PROTO, "render", moduleUiElementRenderMethod + ); + scriptProtoDefineFunc( + &MODULE_UI_ELEMENT_PROTO, "dispose", moduleUiElementDisposeMethod + ); + + UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_SCRIPTED] = (uilelementcallbacks_t){ + .render = moduleUiElementRenderScripted + }; +} + +void moduleUiElementDispose(void) { + for(uielementid_t i = 0; i < UI_ELEMENT_COUNT_MAX; i++) { + if(jerry_value_is_object(UI_ELEMENT_JS_INSTANCES[i])) { + jerry_value_free(UI_ELEMENT_JS_INSTANCES[i]); + UI_ELEMENT_JS_INSTANCES[i] = jerry_undefined(); + } + } +} diff --git a/src/dusk/script/module/ui/moduleuielement.h b/src/dusk/script/module/ui/moduleuielement.h new file mode 100644 index 00000000..f200f9db --- /dev/null +++ b/src/dusk/script/module/ui/moduleuielement.h @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "script/scriptproto.h" +#include "ui/uielement.h" +#include + +/** Native data wrapped by a JS UIElement instance (and every class built + * on top of it -- Label, Rectangle, etc. share this exact handle shape + * and store it under MODULE_UI_ELEMENT_PROTO's own native-pointer tag, + * not one of their own, so UIElement's x/y/worldX/worldY/parent/render + * accessors work on them unmodified via the prototype chain. */ +typedef struct { + uielementid_t id; +} moduleuielementhandle_t; + +extern scriptproto_t MODULE_UI_ELEMENT_PROTO; + +/** + * Registers the UIElement class: `new UIElement()` allocates a + * UI_ELEMENT_TYPE_SCRIPTED pool element and, if the (sub)class defines + * an init() method, calls it immediately. Exposes id (read-only), x/y + * (read/write), worldX/worldY (read-only, always current -- no dirty-flag + * cache, just cheap to recompute), parent (read-only, the UIElement this + * one was add()ed to, or undefined), childCount (read-only), add(child)/ + * remove(child) (persistent parent/child links, up to + * UI_ELEMENT_CHILDREN_MAX per element), render(), renderChildren() + * (renders whatever was added, in add-order -- called automatically by + * render() unless a subclass overrides render(), in which case the + * override has full manual control and must call this itself if it + * still wants added children drawn), and dispose() (cascades to every + * descendant). + */ +void moduleUiElementInit(void); + +/** + * Disposes the UIElement class's script resources. + */ +void moduleUiElementDispose(void); + +/** + * Internal. Gets the native handle wrapped by a UIElement (or + * Label/Rectangle/etc.) instance's `this` value. + * + * @param callInfo The JS call info, whose this_value is the instance. + * @return The wrapped handle, or NULL if this_value isn't a UIElement. + */ +moduleuielementhandle_t *moduleUiElementGet(const jerry_call_info_t *callInfo); + +/** + * Internal. Same as moduleUiElementGet, but for a plain JS value rather + * than a call's this_value -- e.g. an argument passed to UI.add(). + * + * @param val The JS value to inspect. + * @return The wrapped handle, or NULL if val isn't a UIElement. + */ +moduleuielementhandle_t *moduleUiElementGetFromValue(const jerry_value_t val); + +/** + * Internal. Shared construction logic for UIElement and every class + * built on top of it (Label, Rectangle, ...): allocates a pool element + * of the given type, attaches the native handle to thisValue under + * MODULE_UI_ELEMENT_PROTO's tag, registers thisValue as the element's + * JS instance (see moduleUiElementGetInstance), and calls init() on + * thisValue if one is defined. + * + * @param thisValue The new instance (a call's this_value). + * @param type The UI element type to allocate. + * @return The error return value; an error is thrown if init() itself + * throws, or if it returns a rejected promise. + */ +errorret_t moduleUiElementConstructShared( + const jerry_value_t thisValue, + const uielementtype_t type +); + +/** + * Internal. Gets the JS instance registered for a UI element id by + * moduleUiElementConstructShared, e.g. so a parent-lookup or the + * UI_ELEMENT_TYPE_SCRIPTED render callback can call back into it. + * + * @param elementId The element id to look up. + * @return The JS instance, or undefined if none is registered (e.g. a + * native-only element that was never constructed from script). + */ +jerry_value_t moduleUiElementGetInstance(const uielementid_t elementId); diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index c7b76cd4..25340efd 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -1,13 +1,8 @@ # Copyright (c) 2026 Dominic Masters -# +# # This software is released under the MIT License. # https://opensource.org/licenses/MIT -add_subdirectory(debug) -add_subdirectory(frame) -add_subdirectory(focus) -add_subdirectory(overlay) -add_subdirectory(transition) add_subdirectory(widget) # Sources @@ -15,4 +10,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ui.c uielement.c -) \ No newline at end of file +) diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c index e875be04..8cbefc93 100644 --- a/src/dusk/ui/ui.c +++ b/src/dusk/ui/ui.c @@ -1,51 +1,102 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "ui.h" +#include "time/time.h" #include "util/memory.h" #include "assert/assert.h" -#include "display/spritebatch/spritebatch.h" +#include "display/display.h" +#include "display/displaystate.h" #include "display/screen/screen.h" -#include "ui/uielement.h" -#include "ui/focus/uifocus.h" +#include "display/shader/shaderunlit.h" +#include "display/spritebatch/spritebatch.h" +#include "console/console.h" ui_t UI; -errorret_t uiInit(void) { - memoryZero(&UI, sizeof(ui_t)); - uiFocusInit(); - uiElementsSort(); - - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementInit(element)); - element++; +bool_t uiRootAdd(const uielementid_t elementId) { + for(uint8_t i = 0; i < UI_ROOT_MAX; i++) { + if(UI.root[i] != UI_ELEMENT_ID_INVALID) continue; + UI.root[i] = elementId; + return true; } + return false; +} + +void uiRootRemove(const uielementid_t elementId) { + for(uint8_t i = 0; i < UI_ROOT_MAX; i++) { + if(UI.root[i] != elementId) continue; + UI.root[i] = UI_ELEMENT_ID_INVALID; + return; + } +} + +errorret_t uiInit(void) { + memoryZero(UI_ELEMENTS, sizeof(UI_ELEMENTS)); + memoryZero(&UI, sizeof(UI)); + for(uint8_t i = 0; i < UI_ROOT_MAX; i++) UI.root[i] = UI_ELEMENT_ID_INVALID; + + consolePrint( + "UI elements size: %zu bytes (%.2f KB), %zu bytes/element, " + "%zu bytes/element for children[%d]", + sizeof(UI_ELEMENTS), sizeof(UI_ELEMENTS) / 1024.0f, + sizeof(uielement_t), + sizeof(uielementid_t) * UI_ELEMENT_CHILDREN_MAX, UI_ELEMENT_CHILDREN_MAX + ); errorOk(); } errorret_t uiUpdate(void) { - uiFocusUpdate(); + #ifdef DUSK_TIME_DYNAMIC + if(TIME.dynamicUpdate) errorOk(); + #endif - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementUpdate(element)); - element++; - } + errorOk(); } errorret_t uiRender(void) { - const uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementDraw(element)); - element++; + // UI draws in screen space -- bind an orthographic projection looking + // straight at the screen, independent of whatever 3D projection/view + // the scene left bound. + mat4 screenIdentity; + mat4 screenProj; + mat4 screenView; + + glm_mat4_identity(screenIdentity); + + glm_ortho( + 0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi), + (float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f, + 0.1f, 100.0f, + screenProj + ); + + glm_lookat( + (vec3){ 0.0f, 0.0f, 1.0f }, + (vec3){ 0.0f, 0.0f, 0.0f }, + (vec3){ 0.0f, 1.0f, 0.0f }, + screenView + ); + + errorChain(shaderBind(&SHADER_UNLIT)); + errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, screenIdentity)); + errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, screenProj)); + errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, screenView)); + + errorChain(displaySetState((displaystate_t){ + .flags = DISPLAY_STATE_FLAG_BLEND + })); + + for(uint8_t i = 0; i < UI_ROOT_MAX; i++) { + if(UI.root[i] == UI_ELEMENT_ID_INVALID) continue; + uiElementRender(UI.root[i]); } errorChain(spriteBatchFlush()); @@ -53,10 +104,5 @@ errorret_t uiRender(void) { } errorret_t uiDispose(void) { - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementDispose(element)); - element++; - } errorOk(); -} \ No newline at end of file +} diff --git a/src/dusk/ui/ui.h b/src/dusk/ui/ui.h index 2887a262..a73df030 100644 --- a/src/dusk/ui/ui.h +++ b/src/dusk/ui/ui.h @@ -1,21 +1,42 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #pragma once #include "error/error.h" +#include "uielement.h" + +#define UI_ROOT_MAX 32 typedef struct { - void *nothing; + uielementid_t root[UI_ROOT_MAX]; } ui_t; extern ui_t UI; +/** + * Adds an element to UI's render roots, in the first free slot. + * + * @param elementId The element id to add as a root. + * @return True on success, false if UI.root[] is already full. + */ +bool_t uiRootAdd(const uielementid_t elementId); + +/** + * Removes an element from UI's render roots, if present. No-op if + * elementId isn't currently a root. + * + * @param elementId The element id to remove. + */ +void uiRootRemove(const uielementid_t elementId); + /** * Initializes the UI system. + * + * @return Any error that occurs. */ errorret_t uiInit(void); @@ -27,8 +48,9 @@ errorret_t uiInit(void); errorret_t uiUpdate(void); /** - * Renders the UI system. - * + * Renders the UI system. Unlike uiUpdate(), this runs every real frame + * unconditionally, regardless of the fixed-timestep cadence. + * * @return Any error that occurs. */ errorret_t uiRender(void); @@ -38,4 +60,4 @@ errorret_t uiRender(void); * * @return Any error that occurs. */ -errorret_t uiDispose(void); \ No newline at end of file +errorret_t uiDispose(void); diff --git a/src/dusk/ui/uibase.h b/src/dusk/ui/uibase.h new file mode 100644 index 00000000..47305433 --- /dev/null +++ b/src/dusk/ui/uibase.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +#define UI_ELEMENT_ID_INVALID 0xFFFF +typedef uint_fast16_t uielementid_t; +typedef struct uielement_s uielement_t; \ No newline at end of file diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index f119f534..f8b609e9 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + /** * Copyright (c) 2026 Dominic Masters * @@ -6,81 +13,235 @@ */ #include "uielement.h" +#include "ui.h" #include "assert/assert.h" -#include "util/sort.h" -#include "ui/frame/uiframe.h" -#include "ui/debug/uifps.h" -#include "engine/engine.h" -#include "ui/overlay/uifullbox.h" -#include "ui/overlay/uiloading.h" -#include "ui/overlay/uicrop.h" -#include "ui/transition/uitransition.h" -#include "ui/debug/uiconsole.h" -#include "ui/frame/uiconfirm.h" +#include "util/memory.h" -// Priming pass: X does nothing here, so this only exists to process any -// #include directives nested in uielementlist.h/ui/uielementgame.h at -// file scope (each such header's own #pragma once makes it a no-op on -// the real pass below, which happens inside UI_ELEMENTS[]'s braces, -// where a raw #include of a declaration would be invalid). -#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing -#include "uielementlist.h" -#undef X +uielement_t UI_ELEMENTS[UI_ELEMENT_COUNT_MAX]; -uielement_t UI_ELEMENTS[] = { - #define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \ - { \ - .init = initFn, .update = updateFn, .draw = drawFn, \ - .dispose = disposeFn, .order = elementOrder \ - }, - #include "uielementlist.h" - #undef X +// Belt-and-braces guard against runaway recursion in uiElementRender()/ +// uiElementUpdateWorld() -- cycles are already rejected at +// uiElementSetParent() time, so this should never actually trip. +#define UI_ELEMENT_RENDER_DEPTH_MAX 16 +static uint8_t UI_ELEMENT_RENDER_DEPTH = 0; - { 0 } // Null terminator +uilelementcallbacks_t UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_COUNT] = { + [UI_ELEMENT_TYPE_NULL] = {0}, + + [UI_ELEMENT_TYPE_LABEL] = { + .init = uiLabelInit, + .update = uiLabelUpdate, + .render = uiLabelRender, + .dispose = uiLabelDispose + }, + + [UI_ELEMENT_TYPE_RECTANGLE] = { + .init = uiRectangleInit, + .update = uiRectangleUpdate, + .render = uiRectangleRender, + .dispose = uiRectangleDispose + }, + + [UI_ELEMENT_TYPE_SCRIPTED] = {0} }; -bool_t uiElementIsNull(const uielement_t *element) { - return element->init == NULL && - element->update == NULL && - element->draw == NULL && - element->dispose == NULL; +void uiElementInit( + const uielementid_t elementId, + const uielementtype_t type +) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + assertTrue(type < UI_ELEMENT_TYPE_COUNT, "Invalid type"); + assertTrue(type != UI_ELEMENT_TYPE_NULL, "Cannot initialize null type"); + assertTrue( + UI_ELEMENTS[elementId].type == UI_ELEMENT_TYPE_NULL, + "Element already initialized?" + ); + + uielement_t *element = &UI_ELEMENTS[elementId]; + memoryZero(element, sizeof(uielement_t)); + element->id = elementId; + element->type = type; + element->parent = UI_ELEMENT_ID_INVALID; + element->childCount = 0; + + if(UI_ELEMENT_CALLBACKS[type].init) { + UI_ELEMENT_CALLBACKS[type].init(element); + } } -int_t uiElementCompareOrder(const void *a, const void *b) { - const uielement_t *elementA = (const uielement_t *)a; - const uielement_t *elementB = (const uielement_t *)b; - if(elementA->order < elementB->order) return -1; - if(elementA->order > elementB->order) return 1; - return 0; +void uiElementUpdate(const uielementid_t elementId) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + + uielement_t *element = &UI_ELEMENTS[elementId]; + assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type"); + + if(UI_ELEMENT_CALLBACKS[element->type].update) { + UI_ELEMENT_CALLBACKS[element->type].update(element); + } } -void uiElementsSort(void) { - // The trailing null terminator is always the last static entry -- sort - // everything before it, and leave it in place. - const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1; - sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder); +void uiElementRender(const uielementid_t elementId) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + + uielement_t *element = &UI_ELEMENTS[elementId]; + assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type"); + + uiElementUpdateWorld(element); + + assertTrue( + UI_ELEMENT_RENDER_DEPTH < UI_ELEMENT_RENDER_DEPTH_MAX, + "UI render depth exceeded -- elements nested too deep" + ); + UI_ELEMENT_RENDER_DEPTH++; + + if(UI_ELEMENT_CALLBACKS[element->type].render) { + UI_ELEMENT_CALLBACKS[element->type].render(element); + } + + UI_ELEMENT_RENDER_DEPTH--; } -errorret_t uiElementInit(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->init != NULL) errorChain(element->init()); - errorOk(); +void uiElementUpdateWorld(uielement_t *element) { + assertNotNull(element, "Element is null"); + assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type"); + + element->worldX = element->x; + element->worldY = element->y; + + if(element->parent != UI_ELEMENT_ID_INVALID) { + uielement_t *parent = &UI_ELEMENTS[element->parent]; + uiElementUpdateWorld(parent); + element->worldX += parent->worldX; + element->worldY += parent->worldY; + } } -errorret_t uiElementUpdate(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->update != NULL) errorChain(element->update()); - errorOk(); +void uiElementRenderChildren(uielement_t *element) { + assertNotNull(element, "Element is null"); + + for(uint8_t i = 0; i < element->childCount; i++) { + uiElementRender(element->children[i]); + } } -errorret_t uiElementDraw(const uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->draw != NULL) errorChain(element->draw()); - errorOk(); +bool_t uiElementIsAncestorOf( + const uielementid_t elementId, + const uielementid_t otherId +) { + uielementid_t current = otherId; + for(uielementid_t i = 0; i < UI_ELEMENT_COUNT_MAX; i++) { + if(current == elementId) return true; + if(current == UI_ELEMENT_ID_INVALID) return false; + current = UI_ELEMENTS[current].parent; + } + return false; } -errorret_t uiElementDispose(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->dispose != NULL) errorChain(element->dispose()); - errorOk(); +bool_t uiElementSetParent( + const uielementid_t elementId, + const uielementid_t parentElementId +) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + assertTrue( + UI_ELEMENTS[elementId].type != UI_ELEMENT_TYPE_NULL, + "Element is null type" + ); + + if(parentElementId != UI_ELEMENT_ID_INVALID) { + assertTrue(parentElementId < UI_ELEMENT_COUNT_MAX, "Invalid parent ID"); + assertTrue( + UI_ELEMENTS[parentElementId].type != UI_ELEMENT_TYPE_NULL, + "Parent element is null type" + ); + assertTrue( + elementId != parentElementId, "Cannot parent an element to itself" + ); + + if(uiElementIsAncestorOf(elementId, parentElementId)) return false; + if(UI_ELEMENTS[parentElementId].childCount >= UI_ELEMENT_CHILDREN_MAX) { + return false; + } + } + + uielement_t *element = &UI_ELEMENTS[elementId]; + + if(element->parent != UI_ELEMENT_ID_INVALID) { + uielement_t *oldParent = &UI_ELEMENTS[element->parent]; + for(uint8_t i = 0; i < oldParent->childCount; i++) { + if(oldParent->children[i] != elementId) continue; + for(uint8_t j = i; j < oldParent->childCount - 1; j++) { + oldParent->children[j] = oldParent->children[j + 1]; + } + oldParent->childCount--; + break; + } + } + + element->parent = parentElementId; + + if(parentElementId != UI_ELEMENT_ID_INVALID) { + uielement_t *parent = &UI_ELEMENTS[parentElementId]; + parent->children[parent->childCount++] = elementId; + uiRootRemove(elementId); + } + + return true; } + +uielementid_t uiElementGetParent(const uielementid_t elementId) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + return UI_ELEMENTS[elementId].parent; +} + +void uiElementDispose(const uielementid_t elementId) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + + uielement_t *element = &UI_ELEMENTS[elementId]; + assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type"); + + uiElementSetParent(elementId, UI_ELEMENT_ID_INVALID); + uiRootRemove(elementId); + + for(uint8_t i = 0; i < element->childCount; i++) { + UI_ELEMENTS[element->children[i]].parent = UI_ELEMENT_ID_INVALID; + } + + if(UI_ELEMENT_CALLBACKS[element->type].dispose) { + UI_ELEMENT_CALLBACKS[element->type].dispose(element); + } + + memoryZero(element, sizeof(uielement_t)); +} + +void uiElementDisposeDeep(const uielementid_t elementId) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + + uielement_t *element = &UI_ELEMENTS[elementId]; + assertTrue(element->type != UI_ELEMENT_TYPE_NULL, "Element is null type"); + + uint8_t childCount = element->childCount; + uielementid_t children[UI_ELEMENT_CHILDREN_MAX]; + if(childCount > 0) { + memoryCopy( + children, element->children, sizeof(uielementid_t) * childCount + ); + } + + uiElementDispose(elementId); + + for(uint8_t i = 0; i < childCount; i++) { + uiElementDisposeDeep(children[i]); + } +} + +uielementid_t uiElementGetAvailable() { + uielementid_t id; + for(id = 0; id < UI_ELEMENT_COUNT_MAX; id++) { + if(UI_ELEMENTS[id].type == UI_ELEMENT_TYPE_NULL) { + return id; + } + } + + assertUnreachable("No available UI element IDs"); + return UI_ELEMENT_COUNT_MAX; +} \ No newline at end of file diff --git a/src/dusk/ui/uielement.h b/src/dusk/ui/uielement.h index d9b8a525..51c71e6b 100644 --- a/src/dusk/ui/uielement.h +++ b/src/dusk/ui/uielement.h @@ -7,78 +7,164 @@ #pragma once #include "error/error.h" +#include "ui/widget/uilabel.h" +#include "ui/widget/uirectangle.h" -// Built-in order tiers. Lower values update/render first; ties preserve -// their relative UI_ELEMENTS declaration order (uiElementsSort uses a -// stable sort). Game-specific elements can use any int32_t value -- these -// are just the ones the engine itself relies on. -#define UI_ELEMENT_ORDER_DEFAULT 0 -#define UI_ELEMENT_ORDER_DEBUG 1000 +#define UI_ELEMENT_COUNT_MAX 128 +#define UI_ELEMENT_CHILDREN_MAX 8 -typedef struct { - errorret_t (*init)(); - errorret_t (*update)(); - errorret_t (*draw)(); - errorret_t (*dispose)(); - int32_t order; +#define UI_ELEMENT_STATE_ACTIVE (1 << 0) + +typedef enum { + UI_ELEMENT_TYPE_NULL, + + UI_ELEMENT_TYPE_LABEL, + UI_ELEMENT_TYPE_RECTANGLE, + UI_ELEMENT_TYPE_SCRIPTED, + + UI_ELEMENT_TYPE_COUNT +} uielementtype_t; + +typedef struct uielement_s { + uielementid_t id; + uint8_t state; + uielementtype_t type; + float_t x, y; + float_t worldX, worldY; + + /** + * Persistent parent link, or UI_ELEMENT_ID_INVALID if this element is + * unparented. Set only via uiElementSetParent() -- add()/remove()/ + * UI.add() all route through it -- and kept symmetric with the + * parent's own children[]/childCount. + */ + uielementid_t parent; + uint8_t childCount; + uielementid_t children[UI_ELEMENT_CHILDREN_MAX]; + + union { + uilabel_t label; + uirectangle_t rectangle; + // uiscripted_t scripted; + }; } uielement_t; -extern uielement_t UI_ELEMENTS[]; +typedef struct { + void (*init)(uielement_t *element); + void (*update)(uielement_t *element); + void (*render)(uielement_t *element); + void (*dispose)(uielement_t *element); +} uilelementcallbacks_t; + +extern uielement_t UI_ELEMENTS[UI_ELEMENT_COUNT_MAX]; +extern uilelementcallbacks_t UI_ELEMENT_CALLBACKS[UI_ELEMENT_TYPE_COUNT]; /** - * Returns true when all four callbacks on the element are NULL, - * which marks the end of the UI_ELEMENTS array. + * Initializes a UI element with the given ID. + * + * @param elementId The ID of the UI element to initialize. + */ +void uiElementInit( + const uielementid_t elementId, + const uielementtype_t type +); + +/** + * Updates a UI element with the given ID. + * + * @param elementId The ID of the UI element to update. + */ +void uiElementUpdate(const uielementid_t elementId); + +/** + * Renders a UI element with the given ID. + * + * @param elementId The ID of the UI element to render. + */ +void uiElementRender(const uielementid_t elementId); + +/** + * Recomputes worldX/worldY by walking the persistent parent chain. + * Always recomputes unconditionally (no dirty-flag cache) -- the + * underlying math is two float adds, cheap enough that caching it isn't + * worth the complexity. Safe to call any time, not just during render. * - * @param element The element to test. - * @returns True if the element is the null terminator. + * @param element The UI element to update. */ -bool_t uiElementIsNull(const uielement_t *element); +void uiElementUpdateWorld(uielement_t *element); /** - * Compares two elements by their .order field, ascending. Matches - * sortcompare_t, for use with the project's sort utilities. + * Renders every child of element, in child-array (add-order) order. + * Called unconditionally by native leaf render callbacks (uiLabelRender, + * uiRectangleRender) since they have no override concept; called by + * UI_ELEMENT_TYPE_SCRIPTED's render callback only when the instance did + * NOT override render() -- an override takes full manual control and + * must call this itself (see UIElement.prototype.renderChildren) if it + * still wants added children drawn. * - * @param a First uielement_t to compare. - * @param b Second uielement_t to compare. - * @return Negative if a < b, zero if a == b, positive if a > b. + * @param element The UI element whose children to render. */ -int_t uiElementCompareOrder(const void *a, const void *b); +void uiElementRenderChildren(uielement_t *element); /** - * Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing - * null terminator is never moved. Called once by uiInit -- element order - * is static after that, so there's no need to re-sort every frame. - */ -void uiElementsSort(void); - -/** - * Initializes a UI element, invoking its init callback if set. + * Sets the persistent parent of a UI element, detaching it from any + * current parent first. Pass UI_ELEMENT_ID_INVALID to detach only. + * Rejects parenting an element to itself or to one of its own + * descendants (a cycle would hang uiElementRender()/uiElementUpdateWorld + * forever). Also removes elementId from UI.root[] when attaching to a + * real parent -- an element is either a render root or somebody's + * child, never both. * - * @param element The element to initialize. - * @return Any error that occurs. + * @param elementId The child element id. + * @param parentElementId The new parent id, or UI_ELEMENT_ID_INVALID. + * @return True on success, false if parentElementId's children[] is + * already full (UI_ELEMENT_CHILDREN_MAX) or the reparent would create + * a cycle -- in both failure cases nothing is changed. */ -errorret_t uiElementInit(uielement_t *element); +bool_t uiElementSetParent( + const uielementid_t elementId, + const uielementid_t parentElementId +); /** - * Updates a UI element, calling its update callback if set. + * Gets the persistent parent of a UI element. * - * @param element The element to update. - * @return Any error that occurs. + * @param elementId The element id to query. + * @return The parent id, or UI_ELEMENT_ID_INVALID if unparented. */ -errorret_t uiElementUpdate(uielement_t *element); +uielementid_t uiElementGetParent(const uielementid_t elementId); /** - * Draws a UI element, calling its draw callback if set. + * Internal. True if elementId is otherId itself or an ancestor of + * otherId, walking otherId's parent chain. Used by uiElementSetParent + * to reject cycles before they're created. * - * @param element The element to render. - * @return Any error that occurs. + * @param elementId The potential ancestor. + * @param otherId The element to walk up from. */ -errorret_t uiElementDraw(const uielement_t *element); +bool_t uiElementIsAncestorOf( + const uielementid_t elementId, + const uielementid_t otherId +); /** - * Disposes of a UI element, invoking its dispose callback if set. + * Disposes of a UI element with the given ID. * - * @param element The element to dispose. - * @return Any error that occurs. + * @param elementId The ID of the UI element to dispose of. */ -errorret_t uiElementDispose(uielement_t *element); +void uiElementDispose(const uielementid_t elementId); + +/** + * Disposes of a UI element and every descendant, depth-first. Detaches + * from any parent and removes from UI.root[] first. + * + * @param elementId The ID of the UI element to dispose of. + */ +void uiElementDisposeDeep(const uielementid_t elementId); + +/** + * Gets an available UI element ID from the pool. + * + * @return An available UI element ID. + */ +uielementid_t uiElementGetAvailable(); \ No newline at end of file diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index 6beebee4..7fd3a92d 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -5,13 +5,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - uibutton.c - uicheckbox.c - uitab.c - uislider.c - uidropdown.c - uiscrolling.c - uimenu.c uilabel.c - uiwidgetlabel.c + uirectangle.c ) diff --git a/src/dusk/ui/widget/uilabel.c b/src/dusk/ui/widget/uilabel.c index 53afb233..ad3f627b 100644 --- a/src/dusk/ui/widget/uilabel.c +++ b/src/dusk/ui/widget/uilabel.c @@ -5,66 +5,56 @@ * https://opensource.org/licenses/MIT */ -#include "uilabel.h" +#include "ui/uielement.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" #include "display/text/text.h" +#include "display/color.h" -void uiLabelInit(uilabel_t *label, font_t *font) { - assertNotNull(label, "Label cannot be NULL"); - assertNotNull(font, "Font cannot be NULL"); +void uiLabelInit(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); - memoryZero(label, sizeof(uilabel_t)); - label->font = font; + uilabel_t *label = &element->label; + label->font = &FONT_DEFAULT; label->color = COLOR_WHITE; } -void uiLabelSetText(uilabel_t *label, const char_t *text) { - assertNotNull(label, "Label cannot be NULL"); +void uiLabelSetText(const uielementid_t elementId, const char_t *text) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); assertNotNull(text, "Text cannot be NULL"); assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long"); + uielement_t *element = &UI_ELEMENTS[elementId]; + assertTrue(element->type == UI_ELEMENT_TYPE_LABEL, "Element is not a label"); + + uilabel_t *label = &element->label; stringCopy(label->text, text, UI_LABEL_TEXT_MAX); label->spriteCount = textBuildSpriteCache( - label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX, - &label->width, &label->height + label->text, label->font, label->sprites, + UI_LABEL_SPRITE_COUNT_MAX, &label->width, &label->height ); } -void uiLabelSetColor(uilabel_t *label, const color_t color) { - assertNotNull(label, "Label cannot be NULL"); - label->color = color; +void uiLabelUpdate(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); } -void uiLabelGetSize( - const uilabel_t *label, - int32_t *outWidth, - int32_t *outHeight -) { - assertNotNull(label, "Label cannot be NULL"); - assertNotNull(outWidth, "Output width cannot be NULL"); - assertNotNull(outHeight, "Output height cannot be NULL"); - *outWidth = label->width; - *outHeight = label->height; +void uiLabelRender(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); + + uilabel_t *label = &element->label; + if(label->spriteCount > 0) { + spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX]; + errorCatch(errorPrint(textDrawSpriteCache( + label->sprites, label->spriteCount, scratch, + element->worldX, element->worldY, label->color, label->font->texture + ))); + } + + uiElementRenderChildren(element); } -errorret_t uiLabelDraw( - const uilabel_t *label, - const float_t x, - const float_t y -) { - assertNotNull(label, "Label cannot be NULL"); - if(label->spriteCount == 0) errorOk(); - - // Cached sprites are relative to (0,0); textDrawSpriteCache translates - // into the requested screen position here instead of in - // uiLabelSetText, so a label can be repositioned every frame without - // rebuilding the (much more expensive) glyph/UV cache. - spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX]; - errorChain(textDrawSpriteCache( - label->sprites, label->spriteCount, scratch, x, y, label->color, - label->font->texture - )); - errorOk(); +void uiLabelDispose(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); } diff --git a/src/dusk/ui/widget/uilabel.h b/src/dusk/ui/widget/uilabel.h index d0f6e10d..41bf766e 100644 --- a/src/dusk/ui/widget/uilabel.h +++ b/src/dusk/ui/widget/uilabel.h @@ -10,8 +10,9 @@ #include "display/text/font.h" #include "display/spritebatch/spritebatchsprite.h" #include "display/color.h" +#include "ui/uibase.h" -#define UI_LABEL_TEXT_MAX 256 +#define UI_LABEL_TEXT_MAX 64 #define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX typedef struct { @@ -26,60 +27,36 @@ typedef struct { } uilabel_t; /** - * Initializes a label, defaulting to white text and no text set. - * - * @param label The label to initialize. - * @param font Font to use for rendering. Must outlive the label. + * Initializes a UI label element. + * + * @param element The UI element to initialize as a label. */ -void uiLabelInit(uilabel_t *label, font_t *font); +void uiLabelInit(uielement_t *element); /** - * Sets the label's text, rebuilding its cached sprites (glyph lookup, UV, - * and layout) immediately. This is the only way the sprite cache gets - * rebuilt -- uiLabelDraw never recomputes it, so call this whenever the - * text changes rather than once up front and expecting it to stay in sync. - * - * @param label The label to update. - * @param text Null-terminated string to display. Must be shorter than - * UI_LABEL_TEXT_MAX. + * Updates a UI label element. + * + * @param element The UI element to update as a label. */ -void uiLabelSetText(uilabel_t *label, const char_t *text); +void uiLabelSetText(const uielementid_t elementId, const char_t *text); /** - * Sets the label's tint color. Cheap -- doesn't touch the sprite cache, - * since color is applied via the draw material, not baked into sprites. - * - * @param label The label to update. - * @param color The new tint color. + * Updates a UI label element. + * + * @param elementId The ID of the UI element to update as a label. */ -void uiLabelSetColor(uilabel_t *label, const color_t color); +void uiLabelUpdate(uielement_t *element); /** - * Gets the measured size (in pixels) of the label's current text, cached - * from the last uiLabelSetText call. - * - * @param label The label to query. - * @param outWidth Pointer to store the width. - * @param outHeight Pointer to store the height. + * Renders a UI label element. + * + * @param elementId The ID of the UI element to render as a label. */ -void uiLabelGetSize( - const uilabel_t *label, - int32_t *outWidth, - int32_t *outHeight -); +void uiLabelRender(uielement_t *element); /** - * Draws the label's cached sprites at the given screen position in a - * single batched buffer call. Does not recompute glyph layout -- call - * uiLabelSetText first whenever the text changes. - * - * @param label The label to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. + * Disposes of a UI label element. + * + * @param element The UI element to dispose of as a label. */ -errorret_t uiLabelDraw( - const uilabel_t *label, - const float_t x, - const float_t y -); +void uiLabelDispose(uielement_t *element); \ No newline at end of file diff --git a/src/dusk/ui/widget/uirectangle.c b/src/dusk/ui/widget/uirectangle.c new file mode 100644 index 00000000..6ab20c4a --- /dev/null +++ b/src/dusk/ui/widget/uirectangle.c @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uirectangle.h" +#include "ui/uielement.h" +#include "assert/assert.h" +#include "display/texture/texture.h" +#include "display/shader/shaderunlit.h" +#include "display/spritebatch/spritebatch.h" + +void uiRectangleInit(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); + + uirectangle_t *rect = &element->rectangle; + rect->width = 0.0f; + rect->height = 0.0f; + rect->color = COLOR_WHITE; +} + +void uiRectangleSetSize( + const uielementid_t elementId, + const float_t width, + const float_t height +) { + assertTrue(elementId < UI_ELEMENT_COUNT_MAX, "Invalid ID"); + + uielement_t *element = &UI_ELEMENTS[elementId]; + assertTrue( + element->type == UI_ELEMENT_TYPE_RECTANGLE, "Element is not a rectangle" + ); + + element->rectangle.width = width; + element->rectangle.height = height; +} + +void uiRectangleSetColor(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_RECTANGLE, "Element is not a rectangle" + ); + + element->rectangle.color = color; +} + +void uiRectangleUpdate(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); +} + +void uiRectangleRender(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); + + uirectangle_t *rect = &element->rectangle; + if(rect->width > 0.0f && rect->height > 0.0f) { + spritebatchsprite_t sprite = { + .min = { element->worldX, element->worldY, 0.0f }, + .max = { + element->worldX + rect->width, element->worldY + rect->height, 0.0f + }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t material = { + .unlit = { + .color = rect->color, + .texture = &TEXTURE_WHITE + } + }; + errorCatch( + errorPrint(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)) + ); + } + + uiElementRenderChildren(element); +} + +void uiRectangleDispose(uielement_t *element) { + assertNotNull(element, "Element cannot be NULL"); +} diff --git a/src/dusk/ui/widget/uirectangle.h b/src/dusk/ui/widget/uirectangle.h new file mode 100644 index 00000000..aee7a73b --- /dev/null +++ b/src/dusk/ui/widget/uirectangle.h @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "display/color.h" +#include "ui/uibase.h" + +typedef struct { + float_t width; + float_t height; + color_t color; +} uirectangle_t; + +/** + * Initializes a UI rectangle element: zero size, white color. + * + * @param element The UI element to initialize as a rectangle. + */ +void uiRectangleInit(uielement_t *element); + +/** + * Sets the rectangle's size. + * + * @param elementId The ID of the UI element to update as a rectangle. + * @param width The new width, in screen space. + * @param height The new height, in screen space. + */ +void uiRectangleSetSize( + const uielementid_t elementId, + const float_t width, + const float_t height +); + +/** + * Sets the rectangle's fill color. + * + * @param elementId The ID of the UI element to update as a rectangle. + * @param color The new fill color. + */ +void uiRectangleSetColor(const uielementid_t elementId, const color_t color); + +/** + * Updates a UI rectangle element. Currently a no-op -- rectangles have no + * per-frame state of their own. + * + * @param element The UI element to update as a rectangle. + */ +void uiRectangleUpdate(uielement_t *element); + +/** + * Renders a UI rectangle element: a single flat-colored sprite spanning + * its world position and size. No-op if width or height is <= 0. + * + * @param element The UI element to render as a rectangle. + */ +void uiRectangleRender(uielement_t *element); + +/** + * Disposes of a UI rectangle element. Currently a no-op -- rectangles own + * no resources. + * + * @param element The UI element to dispose of as a rectangle. + */ +void uiRectangleDispose(uielement_t *element); diff --git a/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c b/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c index aa75c5a0..acaef879 100644 --- a/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c +++ b/src/duskrpg/cutscene/item/entity/cutsceneentityitempickup.c @@ -9,7 +9,11 @@ #include "cutscene/cutscenesystem.h" #include "entity/entitymanager.h" #include "entity/component/overworld/entityinteractable.h" -#include "ui/textbox/uitextboxmain.h" +#include "console/console.h" + +// Temporarily stubbed -- ui/textbox is archived pending the UI rewrite. +// The pickup message goes to the console instead, and the item completes +// immediately instead of waiting on a textbox. void cutsceneEntityItemPickupStart( const cutsceneitem_t *item, @@ -21,7 +25,7 @@ void cutsceneEntityItemPickupStart( const char_t *message = item->entityItemPickup.message[0] != '\0' ? item->entityItemPickup.message : ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT; - uiTextboxMainSetText(message); + consolePrint("Entity %d picked up an item: %s", entityId, message); entityDisposeDeep(CUTSCENE_SYSTEM.mgr, entityId); } @@ -29,5 +33,5 @@ bool_t cutsceneEntityItemPickupUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - return !uiTextboxMainIsActive(); + return true; } diff --git a/src/duskrpg/cutscene/item/ui/cutscenetext.c b/src/duskrpg/cutscene/item/ui/cutscenetext.c index 219ecbe6..2afa8182 100644 --- a/src/duskrpg/cutscene/item/ui/cutscenetext.c +++ b/src/duskrpg/cutscene/item/ui/cutscenetext.c @@ -6,18 +6,19 @@ */ #include "cutscene/item/cutsceneitem.h" -#include "ui/textbox/uitextboxmain.h" + +// Temporarily stubbed -- ui/textbox is archived pending the UI rewrite. +// Text items complete immediately instead of showing/waiting on a textbox. void cutsceneTextStart( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - uiTextboxMainSetText(item->text.text); } bool_t cutsceneTextUpdate( const cutsceneitem_t *item, cutsceneitemdata_t *data ) { - return !uiTextboxMainIsActive(); + return true; } diff --git a/src/duskrpg/entity/component/overworld/entityinteractable.c b/src/duskrpg/entity/component/overworld/entityinteractable.c index 14297c0e..2a4de3f0 100644 --- a/src/duskrpg/entity/component/overworld/entityinteractable.c +++ b/src/duskrpg/entity/component/overworld/entityinteractable.c @@ -9,7 +9,6 @@ #include "entity/entitymanager.h" #include "console/console.h" #include "cutscene/cutscenesystem.h" -#include "ui/textbox/uitextboxmain.h" #include "util/memory.h" #include "util/string.h" #include "assert/assert.h" @@ -121,15 +120,16 @@ errorret_t entityInteractableTryInteract( switch(interactable->type) { case ENTITY_INTERACTABLE_TYPE_ITEM_PICKUP: { - consolePrint( - "Entity %d picked up pickup %u from entity %d ('%s')", - interactorEntityId, interactable->data.itemPickup.pickupId, - entityId, entityGetName(mgr, entityId) - ); + // Temporarily stubbed -- ui/textbox is archived pending the UI + // rewrite, so the pickup message goes to the console instead. const char_t *message = interactable->data.itemPickup.message[0] != '\0' ? interactable->data.itemPickup.message : ENTITY_INTERACTABLE_ITEM_PICKUP_MESSAGE_DEFAULT; - uiTextboxMainSetText(message); + consolePrint( + "Entity %d picked up pickup %u from entity %d ('%s'): %s", + interactorEntityId, interactable->data.itemPickup.pickupId, + entityId, entityGetName(mgr, entityId), message + ); entityDisposeDeep(mgr, entityId); break; } diff --git a/src/duskrpg/entity/component/overworld/entityplayer.c b/src/duskrpg/entity/component/overworld/entityplayer.c index cf20e95d..cd4cfdd0 100644 --- a/src/duskrpg/entity/component/overworld/entityplayer.c +++ b/src/duskrpg/entity/component/overworld/entityplayer.c @@ -12,7 +12,6 @@ #include "entity/component/display/entityposition.h" #include "entity/component/trigger/entitytrigger.h" #include "entity/component/overworld/entityinteractable.h" -#include "ui/textbox/uitextboxmain.h" #include "input/input.h" #include "time/time.h" #include "util/memory.h" @@ -69,7 +68,9 @@ void entityPlayerUpdate( void *user ) { entityplayer_t *player = entityPlayerGet(mgr, entityId, componentId); - bool_t textboxOpen = uiTextboxMainIsActive(); + // Temporarily stubbed -- ui/textbox is archived pending the UI rewrite, + // so there's currently nothing that can hold textboxOpen true. + bool_t textboxOpen = false; if(player->canInteract && !textboxOpen && inputPressed(INPUT_BIND_ACCEPT)) { entityPlayerTryInteract(mgr, entityId); diff --git a/src/duskrpg/ui/CMakeLists.txt b/src/duskrpg/ui/CMakeLists.txt index f0282dc1..4e1b4307 100644 --- a/src/duskrpg/ui/CMakeLists.txt +++ b/src/duskrpg/ui/CMakeLists.txt @@ -2,10 +2,3 @@ # # This software is released under the MIT License. # https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uitestlabel.c -) - -add_subdirectory(textbox) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 856d129a..897625d8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -19,5 +19,4 @@ add_subdirectory(scene) # add_subdirectory(item) add_subdirectory(script) add_subdirectory(time) -add_subdirectory(ui) add_subdirectory(util) \ No newline at end of file diff --git a/test/script/CMakeLists.txt b/test/script/CMakeLists.txt index 21287129..7d867a77 100644 --- a/test/script/CMakeLists.txt +++ b/test/script/CMakeLists.txt @@ -21,3 +21,5 @@ target_compile_definitions(test_scriptinput PRIVATE ) dusktest(test_moduleui.c) + +dusktest(test_moduleuielement.c) diff --git a/test/script/test_moduleui.c b/test/script/test_moduleui.c index 8d30fea3..4233382b 100644 --- a/test/script/test_moduleui.c +++ b/test/script/test_moduleui.c @@ -8,11 +8,16 @@ #include "dusktest.h" #include "console/console.h" #include "script/scriptmanager.h" +#include "ui/ui.h" #include "util/memory.h" static int ui_setup(void **state) { consoleInit(); + 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; + errorret_t ret = scriptManagerInit(); if(errorIsNotOk(ret)) { errorCatch(ret); return -1; } @@ -27,56 +32,53 @@ static int ui_teardown(void **state) { return 0; } -static void test_ui_add_logs_prefixed_args(void **state) { +static void test_ui_add_requires_an_argument(void **state) { + errorret_t ret = scriptManagerExec("UI.add();", NULL); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_ui_add_requires_an_object(void **state) { + errorret_t ret = scriptManagerExec("UI.add('button');", NULL); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_ui_add_requires_a_uielement(void **state) { + errorret_t ret = scriptManagerExec("UI.add({});", NULL); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_ui_remove_of_untracked_element_is_a_noop(void **state) { jerry_value_t result; errorret_t ret = scriptManagerExec( - "UI.add('button', 1, true);", &result + "UI.remove(new UIElement());", &result ); assert_true(errorIsOk(ret)); jerry_value_free(result); - - assert_string_equal( - CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "UI.add button 1 true" - ); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_ui_remove_logs_prefixed_args(void **state) { - jerry_value_t result; - errorret_t ret = scriptManagerExec("UI.remove('button');", &result); - assert_true(errorIsOk(ret)); - jerry_value_free(result); - - assert_string_equal( - CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "UI.remove button" - ); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_ui_add_no_args_logs_just_the_name(void **state) { - jerry_value_t result; - errorret_t ret = scriptManagerExec("UI.add();", &result); - assert_true(errorIsOk(ret)); - jerry_value_free(result); - - assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "UI.add"); - - assert_int_equal(memoryGetAllocatedCount(), 0); } int main(void) { assertInit(); const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( - test_ui_add_logs_prefixed_args, ui_setup, ui_teardown + test_ui_add_requires_an_argument, ui_setup, ui_teardown ), cmocka_unit_test_setup_teardown( - test_ui_remove_logs_prefixed_args, ui_setup, ui_teardown + test_ui_add_requires_an_object, ui_setup, ui_teardown ), cmocka_unit_test_setup_teardown( - test_ui_add_no_args_logs_just_the_name, ui_setup, ui_teardown + test_ui_add_requires_a_uielement, ui_setup, ui_teardown + ), + cmocka_unit_test_setup_teardown( + test_ui_remove_of_untracked_element_is_a_noop, ui_setup, ui_teardown ), }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/test/script/test_moduleuielement.c b/test/script/test_moduleuielement.c new file mode 100644 index 00000000..88ea0b08 --- /dev/null +++ b/test/script/test_moduleuielement.c @@ -0,0 +1,369 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "dusktest.h" +#include "console/console.h" +#include "script/scriptmanager.h" +#include "ui/ui.h" +#include "display/text/font.h" +#include "util/memory.h" + +// Label's text rebuild (uiLabelSetText -> textBuildSpriteCache) only +// ever reads font->tileset (pure geometry math), never font->texture -- +// so a fake tileset with no real GPU texture behind it is enough to +// exercise it here, without needing the real FONT_DEFAULT (which this +// headless test binary can't initialize -- fontInitDefault() uploads a +// real GL texture, and there's no GL context in a plain cmocka binary). +static tileset_t TEST_FAKE_TILESET = { + .tileWidth = 6, .tileHeight = 10, .tileCount = 100, + .columns = 10, .rows = 10, .uv = { 0.1f, 0.1f } +}; +static font_t TEST_FAKE_FONT = { .texture = NULL, .tileset = &TEST_FAKE_TILESET }; + +static int uielement_setup(void **state) { + consoleInit(); + + 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; + + errorret_t ret = scriptManagerInit(); + if(errorIsNotOk(ret)) { errorCatch(ret); return -1; } + + return 0; +} + +static int uielement_teardown(void **state) { + errorret_t ret = scriptManagerDispose(); + if(errorIsNotOk(ret)) errorCatch(ret); + + consoleDispose(); + 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 execExpectError(const char_t *script) { + errorret_t ret = scriptManagerExec(script, NULL); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); +} + +static void test_uielement_x_y_readback(void **state) { + exec( + "var el = new UIElement();" + "el.x = 10;" + "el.y = 20;" + "if(el.x !== 10 || el.y !== 20) throw new Error('x/y mismatch');" + ); +} + +static void test_uielement_construct_calls_init(void **state) { + exec( + "class Foo extends UIElement {" + " init() { this.x = 42; }" + "}" + "var foo = new Foo();" + "if(foo.x !== 42) throw new Error('init() did not run');" + ); +} + +static void test_uielement_render_no_override_is_noop_when_childless( + void **state +) { + // No override, no children -- render() should just do nothing, safely. + exec("new UIElement().render();"); +} + +static void test_uielement_render_dispatches_to_js_override(void **state) { + exec( + "class Foo extends UIElement {" + " render() { this.rendered = true; }" + "}" + "var foo = new Foo();" + "foo.render();" + "if(foo.rendered !== true) throw new Error('render() override not called');" + ); +} + +static void test_uielement_add_sets_parent_and_world_position(void **state) { + // No render() anywhere -- worldX/worldY/parent are set by add() itself + // and always current, not just "as of last render()". + exec( + "class Child extends UIElement {" + " init() { this.x = 32; this.y = 32; }" + "}" + "class Parent extends UIElement {" + " init() {" + " this.child = new Child();" + " this.add(this.child);" + " this.x = 64;" + " this.y = 64;" + " }" + "}" + "var parent = new Parent();" + "if(parent.child.worldX !== 96) throw new Error('worldX wrong: ' + parent.child.worldX);" + "if(parent.child.worldY !== 96) throw new Error('worldY wrong: ' + parent.child.worldY);" + "if(parent.child.parent !== parent) throw new Error('parent identity wrong');" + "if(parent.child.parent.x !== 64) throw new Error('parent.x wrong');" + "if(parent.childCount !== 1) throw new Error('childCount wrong: ' + parent.childCount);" + ); +} + +static void test_uielement_no_override_auto_renders_children(void **state) { + exec( + "class Child extends UIElement {" + " render() { this.rendered = true; }" + "}" + "var parent = new UIElement();" + "var child = new Child();" + "parent.add(child);" + "parent.render();" + "if(child.rendered !== true) throw new Error('child was not auto-rendered');" + ); +} + +static void test_uielement_override_does_not_auto_render_children( + void **state +) { + exec( + "class Child extends UIElement {" + " render() { this.rendered = true; }" + "}" + "class Parent extends UIElement {" + " render() { this.overrideCalled = true; }" + "}" + "var parent = new Parent();" + "var child = new Child();" + "parent.add(child);" + "parent.render();" + "if(parent.overrideCalled !== true) throw new Error('override not called');" + "if(child.rendered === true) {" + " throw new Error('child should not auto-render under an override');" + "}" + ); +} + +static void test_uielement_render_children_explicit_opt_in(void **state) { + exec( + "class Child extends UIElement {" + " render() { this.rendered = true; }" + "}" + "class Parent extends UIElement {" + " render() { this.renderChildren(); }" + "}" + "var parent = new Parent();" + "var child = new Child();" + "parent.add(child);" + "parent.render();" + "if(child.rendered !== true) throw new Error('renderChildren() did not render child');" + ); +} + +static void test_uielement_add_self_throws(void **state) { + execExpectError("var el = new UIElement(); el.add(el);"); +} + +static void test_uielement_add_cycle_throws(void **state) { + execExpectError( + "var a = new UIElement();" + "var b = new UIElement();" + "a.add(b);" + "b.add(a);" + ); +} + +static void test_uielement_add_reparents(void **state) { + exec( + "var parentA = new UIElement();" + "var parentB = new UIElement();" + "var child = new UIElement();" + "parentA.add(child);" + "if(parentA.childCount !== 1) throw new Error('parentA should have 1 child');" + "parentB.add(child);" + "if(parentA.childCount !== 0) {" + " throw new Error('parentA should have 0 children after reparent');" + "}" + "if(parentB.childCount !== 1) throw new Error('parentB should have 1 child');" + "if(child.parent !== parentB) throw new Error('child.parent should be parentB');" + ); +} + +static void test_uielement_add_capacity_exceeded_throws(void **state) { + exec( + "globalThis.parent = new UIElement();" + "for(var i = 0; i < 8; i++) parent.add(new UIElement());" + "if(parent.childCount !== 8) throw new Error('expected 8 children');" + ); + execExpectError("parent.add(new UIElement());"); +} + +static void test_uielement_dispose_cascades(void **state) { + // Freshly-reset pool (see uielement_setup) -- parent is id 0, child is + // id 1, in construction order. + exec( + "globalThis.parent = new UIElement();" + "globalThis.child = new UIElement();" + "parent.add(child);" + "UI.add(parent);" + ); + + assert_true(UI.root[0] == 0); + assert_true(UI_ELEMENTS[1].type != UI_ELEMENT_TYPE_NULL); + + exec("parent.dispose();"); + + assert_true(UI.root[0] == UI_ELEMENT_ID_INVALID); + assert_true(UI_ELEMENTS[0].type == UI_ELEMENT_TYPE_NULL); + assert_true(UI_ELEMENTS[1].type == UI_ELEMENT_TYPE_NULL); +} + +static void test_uielement_dispose_is_idempotent(void **state) { + exec( + "globalThis.el = new UIElement();" + "el.dispose();" + "el.dispose();" + ); +} + +static void test_label_text_default_and_set(void **state) { + // Freshly-reset pool (see uielement_setup) -- the first element + // constructed in this test is always id 0. + exec("globalThis.label = new Label();"); + UI_ELEMENTS[0].label.font = &TEST_FAKE_FONT; + + exec( + "if(label.text !== '') throw new Error('default text not empty');" + "label.text = 'Hello World!';" + "if(label.text !== 'Hello World!') throw new Error('text readback wrong');" + ); +} + +static void test_rectangle_size_and_color(void **state) { + exec( + "var rect = new Rectangle();" + "rect.width = 100;" + "rect.height = 50;" + "rect.color = { r: 255, g: 0, b: 0, a: 255 };" + "if(rect.width !== 100 || rect.height !== 50) {" + " throw new Error('size readback wrong');" + "}" + "if(rect.color.r !== 255 || rect.color.g !== 0 || rect.color.b !== 0) {" + " throw new Error('color readback wrong');" + "}" + ); +} + +static void test_ui_add_and_remove(void **state) { + assert_true(UI.root[0] == UI_ELEMENT_ID_INVALID); + + exec( + "var el = new UIElement();" + "el.x = 7;" + "UI.add(el);" + ); + + assert_true(UI.root[0] != UI_ELEMENT_ID_INVALID); + assert_true(UI_ELEMENTS[UI.root[0]].type == UI_ELEMENT_TYPE_SCRIPTED); + assert_float_equal(UI_ELEMENTS[UI.root[0]].x, 7.0f, 0.001f); + + exec("UI.remove(el);"); + + assert_true(UI.root[0] == UI_ELEMENT_ID_INVALID); +} + +static void test_ui_add_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.add(child);" + "if(parent.childCount !== 0) {" + " throw new Error('expected 0 children after UI.add');" + "}" + "if(child.parent !== undefined) throw new Error('child.parent should be undefined');" + ); +} + +int main(void) { + assertInit(); + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_uielement_x_y_readback, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_construct_calls_init, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_render_no_override_is_noop_when_childless, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_render_dispatches_to_js_override, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_add_sets_parent_and_world_position, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_no_override_auto_renders_children, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_override_does_not_auto_render_children, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_render_children_explicit_opt_in, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_add_self_throws, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_add_cycle_throws, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_add_reparents, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_add_capacity_exceeded_throws, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_dispose_cascades, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_uielement_dispose_is_idempotent, + uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_label_text_default_and_set, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_rectangle_size_and_color, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_ui_add_and_remove, uielement_setup, uielement_teardown + ), + cmocka_unit_test_setup_teardown( + test_ui_add_detaches_from_parent, uielement_setup, uielement_teardown + ), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/types/index.d.ts b/types/index.d.ts index a22b0abb..7c3d6d1a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -15,4 +15,7 @@ /// /// /// +/// +/// +/// /// diff --git a/types/scene/scene.d.ts b/types/scene/scene.d.ts index 043f62ed..42dbf491 100644 --- a/types/scene/scene.d.ts +++ b/types/scene/scene.d.ts @@ -31,8 +31,10 @@ declare class Scene { * Scene.set() call installed (calling its dispose(), then destroying * the scene it owned), then creates and activates a fresh Scene and * calls the new module's init() with it active -- so init() can use - * `new Entity()`/etc. as usual. The module's update() (if defined) is - * then called once per engine frame until Scene.set() is called again. + * `new Entity()`/etc. as usual. The module's update() and lateUpdate() + * (if defined) are then each called once per engine frame -- update() + * first, lateUpdate() after everything else has updated -- until + * Scene.set() is called again. * * Typical usage (see require.d.ts): * ```js @@ -55,6 +57,14 @@ interface SceneModule { /** Called once per engine frame while this module is the active scene. */ update?(): void; + /** + * Called once per engine frame, after every other update (entities, + * physics, UI, etc.) has already run for that frame -- useful for + * things like camera follow that need to react to where everything + * else ended up this frame, rather than where it was at the start. + */ + lateUpdate?(): void; + /** Called once when this module is replaced by another Scene.set() call. */ dispose?(): void; } diff --git a/types/ui/label.d.ts b/types/ui/label.d.ts new file mode 100644 index 00000000..24513a34 --- /dev/null +++ b/types/ui/label.d.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/// + +/** + * A single-line text element. Extends UIElement -- x/y/worldX/worldY/ + * parent/render()/dispose() all work the same way. + */ +declare class Label extends UIElement { + constructor(); + + /** Display text. Rebuilds the label's glyph cache on assignment. */ + text: string; +} diff --git a/types/ui/rectangle.d.ts b/types/ui/rectangle.d.ts new file mode 100644 index 00000000..b55fdb1e --- /dev/null +++ b/types/ui/rectangle.d.ts @@ -0,0 +1,21 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/// + +/** + * A flat-colored rectangle element. Extends UIElement -- x/y/worldX/ + * worldY/parent/render()/dispose() all work the same way. + */ +declare class Rectangle extends UIElement { + constructor(); + + /** Width, in screen space. No-op to render at <= 0. */ + width: number; + /** Height, in screen space. No-op to render at <= 0. */ + height: number; + /** Fill color, 0-255 per channel. */ + color: { r: number; g: number; b: number; a: number }; +} diff --git a/types/ui/ui.d.ts b/types/ui/ui.d.ts index 5ba0c548..50f15ea0 100644 --- a/types/ui/ui.d.ts +++ b/types/ui/ui.d.ts @@ -3,13 +3,16 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT +/// + /** - * Stub for now: both methods just stringify and space-join their - * arguments (like console.log) and forward the result to the engine's - * console, prefixed with the called method's name. No real UI element - * system exists behind this yet. + * Adds/removes a UIElement (or Label/Rectangle/etc.) from the engine's + * render roots. Root elements get their render() called once per frame; + * everything else only renders as a child of a root (see + * UIElement.add()). A root and a child are mutually exclusive -- + * UI.add() detaches the element from any parent first. */ declare const UI: { - add(...args: any[]): void; - remove(...args: any[]): void; + add(element: UIElement): void; + remove(element: UIElement): void; }; diff --git a/types/ui/uielement.d.ts b/types/ui/uielement.d.ts new file mode 100644 index 00000000..692c43ba --- /dev/null +++ b/types/ui/uielement.d.ts @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +/** + * Base class for every UI element (Label, Rectangle, and any composite + * class you define). `new UIElement()` allocates a pool element and, if + * the (sub)class defines init(), calls it immediately. + * + * add()/remove() maintain a persistent parent/child link (up to 8 + * children per element) -- worldX/worldY/parent are always current, not + * just "as of last render()". Added children render automatically, in + * add-order, right after this element's own visuals -- unless render() + * is overridden, in which case the override takes full manual control + * and must call renderChildren() itself if it still wants them drawn. + * + * ```js + * // No render() needed -- children added via add() draw automatically. + * class HealthBar extends UIElement { + * init() { + * this.label = new Label(); + * this.label.x = 32; + * this.label.y = 32; + * this.add(this.label); + * this.x = 64; + * this.y = 64; + * // this.label.worldX is now 96, immediately -- no render() call needed + * } + * } + * + * // Overriding render() takes full manual control: added children are + * // NOT drawn automatically here -- renderChildren() opts back in. + * class PlayerHealth extends UIElement { + * init() { + * this.icon = new Rectangle(); + * this.add(this.icon); + * } + * render() { + * if(this.player) this.renderChildren(); + * } + * } + * ``` + */ +declare class UIElement { + constructor(); + + /** The engine-assigned numeric element ID. */ + readonly id: number; + + /** Local x position, relative to this element's parent (if any). */ + x: number; + /** Local y position, relative to this element's parent (if any). */ + y: number; + + /** Screen-space x. Always current -- recomputed on every read. */ + readonly worldX: number; + /** Screen-space y. Always current -- recomputed on every read. */ + readonly worldY: number; + + /** The UIElement this one was added to via add(), or undefined. */ + readonly parent: UIElement | undefined; + + /** Number of elements currently added to this one. */ + readonly childCount: number; + + /** + * Adds child as a child of this element, detaching it from any + * previous parent (or from UI's render roots) first. Returns child, + * so calls can be chained. + * + * @throws if child is this element itself, if it's already an + * ancestor of this element (which would create a cycle), or if this + * element already has 8 children. + */ + add(child: UIElement): UIElement; + + /** Removes child from this element's children, if it's currently one. + * No-op otherwise. */ + remove(child: UIElement): void; + + /** + * Called once, right after construction, if defined -- the usual + * place to build children and set initial properties. + */ + init?(): void; + + /** + * Draws this element. The base implementation draws whatever native + * visuals this element type has (Label/Rectangle), then calls + * renderChildren(). Override it to draw your own content instead -- + * an override does NOT get its children auto-rendered; call + * renderChildren() yourself if you still want them drawn. + */ + render(): void; + + /** Renders every added child, in add-order. Called automatically by + * the base render() -- only needed explicitly from inside an + * overridden render(). */ + renderChildren(): 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 + * once, or on an element a cascade already disposed. */ + dispose(): void; +}