diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 9ce6efd5..9e110efb 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -16,7 +16,6 @@ #include "cutscene/cutscene.h" #include "asset/asset.h" #include "ui/ui.h" -#include "ui/uitextbox.h" #include "assert/assert.h" #include "network/network.h" #include "system/system.h" @@ -43,7 +42,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(localeManagerInit()); errorChain(displayInit()); errorChain(uiInit()); - errorChain(uiTextboxInit()); errorChain(cutsceneInit()); errorChain(rpgInit()); errorChain(networkInit()); @@ -63,8 +61,7 @@ errorret_t engineUpdate(void) { inputUpdate(); consoleUpdate(); errorChain(rpgUpdate()); - uiUpdate(); - errorChain(uiTextboxUpdate()); + errorChain(uiUpdate()); errorChain(cutsceneUpdate()); errorChain(sceneUpdate()); errorChain(assetUpdate()); @@ -80,13 +77,12 @@ void engineExit(void) { } errorret_t engineDispose(void) { - uiTextboxDispose(); cutsceneDispose(); errorChain(sceneDispose()); errorChain(networkDispose()); errorChain(rpgDispose()); localeManagerDispose(); - uiDispose(); + errorChain(uiDispose()); consoleDispose(); errorChain(displayDispose()); // errorChain(saveDispose()); diff --git a/src/dusk/input/input.c b/src/dusk/input/input.c index 9c65586c..060b9962 100644 --- a/src/dusk/input/input.c +++ b/src/dusk/input/input.c @@ -139,6 +139,62 @@ float_t inputGetLastValue(const inputaction_t action) { assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds"); return INPUT.actions[action].lastDynamicValue; } + + bool_t inputIsDownDynamic(const inputaction_t action) { + return inputGetCurrentValueDynamic(action) > 0.0f; + } + + bool_t inputWasDownDynamic(const inputaction_t action) { + return inputGetLastValueDynamic(action) > 0.0f; + } + + bool_t inputPressedDynamic(const inputaction_t action) { + return inputIsDownDynamic(action) && !inputWasDownDynamic(action); + } + + bool_t inputReleasedDynamic(const inputaction_t action) { + return !inputIsDownDynamic(action) && inputWasDownDynamic(action); + } + + float_t inputAxisDynamic( + const inputaction_t neg, + const inputaction_t pos + ) { + return inputGetCurrentValueDynamic(pos) - + inputGetCurrentValueDynamic(neg); + } + + void inputAxis2DDynamic( + const inputaction_t negX, const inputaction_t posX, + const inputaction_t negY, const inputaction_t posY, + vec2 result + ) { + assertNotNull(result, "Result vector cannot be null"); + result[0] = inputAxisDynamic(negX, posX); + result[1] = inputAxisDynamic(negY, posY); + } + + void inputAngle2DDynamic( + const inputaction_t negX, const inputaction_t posX, + const inputaction_t negY, const inputaction_t posY, + vec2 result + ) { + assertNotNull(result, "Result vector cannot be null"); + float_t x = inputAxisDynamic(negX, posX); + float_t y = inputAxisDynamic(negY, posY); + float_t mag = sqrtf(x * x + y * y); + if(mag <= 0.0f) { + result[0] = 0.0f; + result[1] = 0.0f; + return; + } + if(mag > 1.0f) { + x /= mag; + y /= mag; + } + result[0] = x; + result[1] = y; + } #endif bool_t inputIsDown(const inputaction_t action) { diff --git a/src/dusk/input/input.csv b/src/dusk/input/input.csv index 42555928..2fa6a46e 100644 --- a/src/dusk/input/input.csv +++ b/src/dusk/input/input.csv @@ -5,6 +5,7 @@ LEFT, RIGHT, ACCEPT, CANCEL, +PAUSE, RAGEQUIT, CONSOLE, POINTERX, diff --git a/src/dusk/input/input.h b/src/dusk/input/input.h index 2ecbbdc8..bf3238a8 100644 --- a/src/dusk/input/input.h +++ b/src/dusk/input/input.h @@ -52,7 +52,7 @@ float_t inputGetLastValue(const inputaction_t action); #ifdef DUSK_TIME_DYNAMIC /** * Gets the current value of a specific input action (dynamic timestep). - * + * * @param action The input action to get the value for. * @return The current value of the action (0.0f to 1.0f). */ @@ -60,11 +60,85 @@ float_t inputGetLastValue(const inputaction_t action); /** * Gets the last value of a specific input action (dynamic timestep). - * + * * @param action The input action to get the value for. * @return The last value of the action (0.0f to 1.0f). */ float_t inputGetLastValueDynamic(const inputaction_t action); + + /** + * Checks if an action is currently down on the dynamic timestep. + * + * @param action The input action to check. + * @return true if the action is currently down. + */ + bool_t inputIsDownDynamic(const inputaction_t action); + + /** + * Checks if an action was down on the previous dynamic frame. + * + * @param action The input action to check. + * @return true if the action was down last dynamic frame. + */ + bool_t inputWasDownDynamic(const inputaction_t action); + + /** + * Checks if an action was pressed this dynamic frame (down now, not before). + * + * @param action The input action to check. + * @return true if the action was just pressed this dynamic frame. + */ + bool_t inputPressedDynamic(const inputaction_t action); + + /** + * Checks if an action was released this dynamic frame (up now, down before). + * + * @param action The input action to check. + * @return true if the action was just released this dynamic frame. + */ + bool_t inputReleasedDynamic(const inputaction_t action); + + /** + * Gets the value of an input axis on the dynamic timestep. + * + * @param neg The action representing the negative direction. + * @param pos The action representing the positive direction. + * @return The current axis value (-1.0f to 1.0f). + */ + float_t inputAxisDynamic( + const inputaction_t neg, + const inputaction_t pos + ); + + /** + * Gets the values of a 2D input axis on the dynamic timestep. + * + * @param negX Negative X action. + * @param posX Positive X action. + * @param negY Negative Y action. + * @param posY Positive Y action. + * @param result vec2 to store the result (-1.0f to 1.0f per axis). + */ + void inputAxis2DDynamic( + const inputaction_t negX, const inputaction_t posX, + const inputaction_t negY, const inputaction_t posY, + vec2 result + ); + + /** + * Gets an angled 2D unit vector from four actions on the dynamic timestep. + * + * @param negX Negative X action. + * @param posX Positive X action. + * @param negY Negative Y action. + * @param posY Positive Y action. + * @param result vec2 to store the result. + */ + void inputAngle2DDynamic( + const inputaction_t negX, const inputaction_t posX, + const inputaction_t negY, const inputaction_t posY, + vec2 result + ); #endif /** diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index 73801f42..9fe2e9c8 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -3,17 +3,17 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT +add_subdirectory(debug) +add_subdirectory(uifocus) + # Sources target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ui.c - uiconsole.c uicrop.c - uifps.c uielement.c uiframe.c uifullbox.c uiloading.c uitextbox.c - uiplayerpos.c ) \ No newline at end of file diff --git a/src/dusk/ui/debug/CMakeLists.txt b/src/dusk/ui/debug/CMakeLists.txt new file mode 100644 index 00000000..74e31662 --- /dev/null +++ b/src/dusk/ui/debug/CMakeLists.txt @@ -0,0 +1,11 @@ +# 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 + uiconsole.c + uifps.c + uiplayerpos.c +) diff --git a/src/dusk/ui/uiconsole.c b/src/dusk/ui/debug/uiconsole.c similarity index 100% rename from src/dusk/ui/uiconsole.c rename to src/dusk/ui/debug/uiconsole.c diff --git a/src/dusk/ui/uiconsole.h b/src/dusk/ui/debug/uiconsole.h similarity index 100% rename from src/dusk/ui/uiconsole.h rename to src/dusk/ui/debug/uiconsole.h diff --git a/src/dusk/ui/uifps.c b/src/dusk/ui/debug/uifps.c similarity index 100% rename from src/dusk/ui/uifps.c rename to src/dusk/ui/debug/uifps.c diff --git a/src/dusk/ui/uifps.h b/src/dusk/ui/debug/uifps.h similarity index 100% rename from src/dusk/ui/uifps.h rename to src/dusk/ui/debug/uifps.h diff --git a/src/dusk/ui/uiplayerpos.c b/src/dusk/ui/debug/uiplayerpos.c similarity index 100% rename from src/dusk/ui/uiplayerpos.c rename to src/dusk/ui/debug/uiplayerpos.c diff --git a/src/dusk/ui/uiplayerpos.h b/src/dusk/ui/debug/uiplayerpos.h similarity index 100% rename from src/dusk/ui/uiplayerpos.h rename to src/dusk/ui/debug/uiplayerpos.h diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c index 182e9e4a..f069dfd5 100644 --- a/src/dusk/ui/ui.c +++ b/src/dusk/ui/ui.c @@ -11,23 +11,17 @@ #include "display/spritebatch/spritebatch.h" #include "display/screen/screen.h" #include "ui/uielement.h" -#include "ui/uifullbox.h" -#include "ui/uiloading.h" -#include "ui/uicrop.h" -#include "time/time.h" +#include "ui/uifocus/uifocus.h" #include "log/log.h" ui_t UI; errorret_t uiInit(void) { memoryZero(&UI, sizeof(ui_t)); - uiCropInit(); - uiFullboxInit(&UI_FULLBOX_UNDER); - uiFullboxInit(&UI_FULLBOX_OVER); - uiLoadingInit(); + uiFocusInit(); uielement_t *element = &UI_ELEMENTS[0]; - while(element->type != UI_ELEMENT_TYPE_NULL) { + while(element->draw != NULL) { errorChain(uiElementInit(element)); element++; } @@ -35,15 +29,21 @@ errorret_t uiInit(void) { errorOk(); } -void uiUpdate(void) { - uiFullboxUpdate(&UI_FULLBOX_UNDER, TIME.delta); - uiFullboxUpdate(&UI_FULLBOX_OVER, TIME.delta); - uiLoadingUpdate(TIME.delta); +errorret_t uiUpdate(void) { + uiFocusUpdate(); + + uielement_t *element = &UI_ELEMENTS[0]; + while(element->draw != NULL) { + errorChain(uiElementUpdate(element)); + element++; + } + + errorOk(); } errorret_t uiRender(void) { const uielement_t *element = &UI_ELEMENTS[0]; - while(element->type != UI_ELEMENT_TYPE_NULL) { + while(element->draw != NULL) { errorChain(uiElementDraw(element)); if(SPRITEBATCH.spriteCount > 0) { @@ -56,5 +56,11 @@ errorret_t uiRender(void) { errorOk(); } -void uiDispose(void) { +errorret_t uiDispose(void) { + uielement_t *element = &UI_ELEMENTS[0]; + while(element->draw != NULL) { + 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 1d5ab848..2887a262 100644 --- a/src/dusk/ui/ui.h +++ b/src/dusk/ui/ui.h @@ -21,8 +21,10 @@ errorret_t uiInit(void); /** * Updates the UI system. + * + * @return Any error that occurs. */ -void uiUpdate(void); +errorret_t uiUpdate(void); /** * Renders the UI system. @@ -33,5 +35,7 @@ errorret_t uiRender(void); /** * Disposes of the UI system. + * + * @return Any error that occurs. */ -void uiDispose(void); \ No newline at end of file +errorret_t uiDispose(void); \ No newline at end of file diff --git a/src/dusk/ui/uicrop.c b/src/dusk/ui/uicrop.c index c3a2f016..f37bf310 100644 --- a/src/dusk/ui/uicrop.c +++ b/src/dusk/ui/uicrop.c @@ -13,8 +13,9 @@ uicrop_t UI_CROP; -void uiCropInit(void) { +errorret_t uiCropInit(void) { UI_CROP.color = COLOR_BLACK; + errorOk(); } errorret_t uiCropDraw(void) { diff --git a/src/dusk/ui/uicrop.h b/src/dusk/ui/uicrop.h index c0458583..ef822633 100644 --- a/src/dusk/ui/uicrop.h +++ b/src/dusk/ui/uicrop.h @@ -17,8 +17,10 @@ extern uicrop_t UI_CROP; /** * Initializes the crop bars to opaque black. + * + * @return Any error that occurs. */ -void uiCropInit(void); +errorret_t uiCropInit(void); /** * Renders solid-color bars covering every area outside the diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 2849e4ff..ce88bc61 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -7,54 +7,79 @@ #include "uielement.h" #include "assert/assert.h" -#include "ui/uifps.h" +#include "ui/debug/uifps.h" #include "engine/engine.h" #include "ui/uitextbox.h" #include "ui/uifullbox.h" #include "ui/uiloading.h" -#include "ui/uiplayerpos.h" +#include "ui/debug/uiplayerpos.h" #include "ui/uicrop.h" -#include "ui/uiconsole.h" +#include "ui/debug/uiconsole.h" uielement_t UI_ELEMENTS[] = { - // Crop bars: black bars outside the scan-safe area. - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiCropDraw }, // Fullbox under: above scene, below system UI. - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiFullboxUnderDraw }, + { + .init = uiFullboxUnderInit, + .update = uiFullboxUnderUpdate, + .draw = uiFullboxUnderDraw + }, // { .type = UI_ELEMENT_TYPE_SCRIPT, .script = { .script = "ui/test.js" } }, - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiTextboxDraw }, + { + .init = uiTextboxInit, + .update = uiTextboxUpdate, + .draw = uiTextboxDraw, + .dispose = uiTextboxDispose + }, + // Fullbox over: above absolutely everything. - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiFullboxOverDraw }, + { + .init = uiFullboxOverInit, + .update = uiFullboxOverUpdate, + .draw = uiFullboxOverDraw + }, + { + .init = uiLoadingInit, + .update = uiLoadingUpdate, + .draw = uiLoadingDraw + }, - // These render above the fullbox overlay. - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiConsoleDraw }, - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiFPSDraw }, - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiPlayerPosDraw }, - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiLoadingDraw }, + { + .init = uiCropInit, + .draw = uiCropDraw + }, - - { .type = UI_ELEMENT_TYPE_NULL }, + // Debug items + { .draw = uiConsoleDraw }, + { .draw = uiFPSDraw }, + { .draw = uiPlayerPosDraw }, + + { 0 } // Null terminator }; 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) { - switch(element->type) { - case UI_ELEMENT_TYPE_NATIVE: - errorChain(element->draw()); - break; - - default: - assertUnreachable("Invalid UI element type"); - } + assertNotNull(element, "element must not be NULL"); + assertNotNull(element->draw, "element draw callback must not be NULL"); + return element->draw(); +} +errorret_t uiElementDispose(uielement_t *element) { + assertNotNull(element, "element must not be NULL"); + if(element->dispose != NULL) errorChain(element->dispose()); errorOk(); } \ No newline at end of file diff --git a/src/dusk/ui/uielement.h b/src/dusk/ui/uielement.h index faee742e..43322116 100644 --- a/src/dusk/ui/uielement.h +++ b/src/dusk/ui/uielement.h @@ -8,28 +8,43 @@ #pragma once #include "error/error.h" -typedef enum { - UI_ELEMENT_TYPE_NULL, - UI_ELEMENT_TYPE_NATIVE, - UI_ELEMENT_TYPE_COUNT -} uielementtype_t; - typedef struct { - uielementtype_t type; + errorret_t (*init)(); + errorret_t (*update)(); errorret_t (*draw)(); + errorret_t (*dispose)(); } uielement_t; extern uielement_t UI_ELEMENTS[]; /** - * Initializes a UI element. + * Initializes a UI element, calling its init callback if set. + * + * @param element The element to initialize. + * @return Any error that occurs. */ errorret_t uiElementInit(uielement_t *element); /** - * Draws a UI 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); \ No newline at end of file +errorret_t uiElementDraw(const uielement_t *element); + +/** + * Disposes of a UI element, calling its dispose callback if set. + * + * @param element The element to dispose. + * @return Any error that occurs. + */ +errorret_t uiElementDispose(uielement_t *element); \ No newline at end of file diff --git a/src/dusk/ui/uifocus/CMakeLists.txt b/src/dusk/ui/uifocus/CMakeLists.txt new file mode 100644 index 00000000..819217b7 --- /dev/null +++ b/src/dusk/ui/uifocus/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + uifocus.c +) diff --git a/src/dusk/ui/uifocus/uifocus.c b/src/dusk/ui/uifocus/uifocus.c new file mode 100644 index 00000000..c28a878b --- /dev/null +++ b/src/dusk/ui/uifocus/uifocus.c @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uifocus.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "input/input.h" +#include "time/time.h" + +const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = { + { INPUT_ACTION_UP, UI_FOCUS_DIRECTION_UP, 0, -1 }, + { INPUT_ACTION_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 }, + { INPUT_ACTION_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 }, + { INPUT_ACTION_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 }, + { INPUT_ACTION_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 }, +}; + +uifocus_t UI_FOCUS; + +void uiFocusInit(void) { + memoryZero(&UI_FOCUS, sizeof(uifocus_t)); +} + +void uiFocusPush( + const uint8_t cols, + const uint8_t rows, + uifocusitemcallback_t selected, + uifocusitemcallback_t changed, + uifocusitemcallback_t closed +) { + assertTrue( + UI_FOCUS.count < UI_FOCUS_STACK_MAX, + "UI focus stack overflow" + ); + + uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count]; + memoryZero(item, sizeof(uifocusitem_t)); + item->cols = cols; + item->rows = rows; + item->selected = selected; + item->changed = changed; + item->closed = closed; + UI_FOCUS.count++; +} + +void uiFocusPop(void) { + assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow"); + + uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; + if(item->closed != NULL) item->closed(item); + UI_FOCUS.count--; +} + +void uiFocusPopTo(const int32_t depth) { + assertTrue(depth >= 0, "Focus depth must be >= 0"); + assertTrue(depth <= UI_FOCUS.count, "Focus depth exceeds current depth"); + while(UI_FOCUS.count > depth) uiFocusPop(); +} + +void uiFocusSelect(const uint8_t x, const uint8_t y) { + assertTrue(UI_FOCUS.count > 0, "No active focus item"); + uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; + assertTrue(item->cols > 0, "Focus item cols must be > 0"); + assertTrue(item->rows > 0, "Focus item rows must be > 0"); + uint8_t newX = x % item->cols; + uint8_t newY = y % item->rows; + + if(item->changed != NULL) { + uint8_t oldX = item->x; + uint8_t oldY = item->y; + item->x = newX; + item->y = newY; + if(!item->changed(item)) { + item->x = oldX; + item->y = oldY; + } + return; + } + + item->x = newX; + item->y = newY; +} + +void uiFocusMoveDirection( + uifocusitem_t *item, + const uifocusdirection_t dir +) { + const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; + while(m->action != INPUT_ACTION_NULL) { + if(m->direction == dir) { + uint8_t x = (uint8_t)( + (item->x + item->cols + m->dx) % item->cols + ); + uint8_t y = (uint8_t)( + (item->y + item->rows + m->dy) % item->rows + ); + uiFocusSelect(x, y); + return; + } + m++; + } +} + +void uiFocusUpdate(void) { + if(UI_FOCUS.count == 0) return; + uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; + + #ifdef DUSK_TIME_DYNAMIC + #define PRESSED(a) inputPressedDynamic(a) + #define IS_DOWN(a) inputIsDownDynamic(a) + #else + #define PRESSED(a) inputPressed(a) + #define IS_DOWN(a) inputIsDown(a) + #endif + + if(PRESSED(INPUT_ACTION_ACCEPT)) { + if(item->selected != NULL) item->selected(item); + goto done; + } + + if(PRESSED(INPUT_ACTION_CANCEL)) { + if(item->closed != NULL && item->closed(item)) { + UI_FOCUS.count--; + } + goto done; + } + + { + const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; + while(m->action != INPUT_ACTION_NULL) { + if(PRESSED(m->action)) { + UI_FOCUS.direction = m->direction; + UI_FOCUS.timeHeld = 0.0f; + uiFocusMoveDirection(item, m->direction); + goto done; + } + m++; + } + } + + { + bool_t held = false; + const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; + while(m->action != INPUT_ACTION_NULL) { + if(m->direction == UI_FOCUS.direction) { + held = IS_DOWN(m->action); + break; + } + m++; + } + + if(!held) { + UI_FOCUS.direction = UI_FOCUS_DIRECTION_NONE; + UI_FOCUS.timeHeld = 0.0f; + goto done; + } + } + + #ifdef DUSK_TIME_DYNAMIC + UI_FOCUS.timeHeld += TIME.dynamicDelta; + #else + UI_FOCUS.timeHeld += TIME.delta; + #endif + + if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) goto done; + + if(UI_FOCUS.timeHeld >= UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) { + UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY; + uiFocusMoveDirection(item, UI_FOCUS.direction); + } + +done: + #undef PRESSED + #undef IS_DOWN +} diff --git a/src/dusk/ui/uifocus/uifocus.h b/src/dusk/ui/uifocus/uifocus.h new file mode 100644 index 00000000..24dc6ed7 --- /dev/null +++ b/src/dusk/ui/uifocus/uifocus.h @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "uifocusitem.h" +#include "input/inputaction.h" + +/** Maximum depth of the focus stack. */ +#define UI_FOCUS_STACK_MAX 8 + +/** + * How long a direction must be held before repeating begins, in seconds. + */ +#define UI_FOCUS_HOLD_DELAY 0.5f + +/** + * Interval between repeated moves while a direction is held, in seconds. + */ +#define UI_FOCUS_HOLD_REPEAT 0.1f + +typedef enum { + UI_FOCUS_DIRECTION_NONE, + UI_FOCUS_DIRECTION_UP, + UI_FOCUS_DIRECTION_DOWN, + UI_FOCUS_DIRECTION_LEFT, + UI_FOCUS_DIRECTION_RIGHT +} uifocusdirection_t; + +typedef struct { + inputaction_t action; + uifocusdirection_t direction; + int8_t dx; + int8_t dy; +} uifocusdirmap_t; + +/** + * Mapping of input actions to focus directions, terminated by an + * entry with action == INPUT_ACTION_NULL. + */ +extern const uifocusdirmap_t UI_FOCUS_DIR_MAP[]; + +/** + * A stack of focused UI items. Push an item when a widget captures + * focus; pop it when focus is released. The topmost item is always + * the active focus context. + */ +typedef struct { + uifocusitem_t items[UI_FOCUS_STACK_MAX]; + int32_t count; + uifocusdirection_t direction; + float_t timeHeld; +} uifocus_t; + +extern uifocus_t UI_FOCUS; + +/** + * Initializes the focus system, zeroing all state. + */ +void uiFocusInit(void); + +/** + * Pushes a new focus item onto the stack with the given grid dimensions + * and callbacks. x and y are initialized to 0. + * + * @param cols Number of columns in the focus grid. + * @param rows Number of rows in the focus grid. + * @param selected Called when the user selects the focused cell. + * @param changed Called when the focused cell position changes. + * @param closed Called when this focus item is popped. + */ +void uiFocusPush( + const uint8_t cols, + const uint8_t rows, + uifocusitemcallback_t selected, + uifocusitemcallback_t changed, + uifocusitemcallback_t closed +); + +/** + * Pops the topmost focus item from the stack, invoking its closed + * callback if one is set. + */ +void uiFocusPop(void); + +/** + * Pops focus items until the stack depth reaches the given value. + * Each popped item has its closed callback invoked as normal. + * + * @param depth Target stack depth to pop back to. + */ +void uiFocusPopTo(const int32_t depth); + +/** + * Manually sets the cursor position of the topmost focus item and + * fires its changed callback. + * + * @param x Column to move to. + * @param y Row to move to. + */ +void uiFocusSelect(const uint8_t x, const uint8_t y); + +/** + * Moves the topmost focus item one step in the given direction, + * wrapping at the grid edges, and fires its changed callback. + * + * @param item The focus item to move. + * @param dir Direction to move. + */ +void uiFocusMoveDirection( + uifocusitem_t *item, + const uifocusdirection_t dir +); + +/** + * Updates the focus system. Handles first-press movement and + * held-direction repeating. Called once per game tick. + */ +void uiFocusUpdate(void); diff --git a/src/dusk/ui/uifocus/uifocusitem.h b/src/dusk/ui/uifocus/uifocusitem.h new file mode 100644 index 00000000..ebe44167 --- /dev/null +++ b/src/dusk/ui/uifocus/uifocusitem.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct uifocusitem_s uifocusitem_t; + +/** + * Callback invoked when a focus item's selected cell changes. + * + * @param item The focus item that changed. + */ +typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item); + +/** + * A single entry on the UI focus stack. Tracks the focused cell + * within a grid of cols x rows, and the current x/y position. + */ +struct uifocusitem_s { + uint8_t cols; + uint8_t rows; + uint8_t x; + uint8_t y; + uifocusitemcallback_t selected; + uifocusitemcallback_t changed; + uifocusitemcallback_t closed; +}; diff --git a/src/dusk/ui/uifullbox.c b/src/dusk/ui/uifullbox.c index 490306ef..0fdb6e74 100644 --- a/src/dusk/ui/uifullbox.c +++ b/src/dusk/ui/uifullbox.c @@ -8,6 +8,7 @@ #include "uifullbox.h" #include "assert/assert.h" #include "util/memory.h" +#include "time/time.h" #include "display/screen/screen.h" #include "display/texture/texture.h" #include "display/spritebatch/spritebatch.h" @@ -92,10 +93,30 @@ void uiFullboxTransition( fullbox->easing = easing; } +errorret_t uiFullboxUnderInit(void) { + uiFullboxInit(&UI_FULLBOX_UNDER); + errorOk(); +} + +errorret_t uiFullboxUnderUpdate(void) { + uiFullboxUpdate(&UI_FULLBOX_UNDER, TIME.delta); + errorOk(); +} + errorret_t uiFullboxUnderDraw(void) { return uiFullboxDraw(&UI_FULLBOX_UNDER); } +errorret_t uiFullboxOverInit(void) { + uiFullboxInit(&UI_FULLBOX_OVER); + errorOk(); +} + +errorret_t uiFullboxOverUpdate(void) { + uiFullboxUpdate(&UI_FULLBOX_OVER, TIME.delta); + errorOk(); +} + errorret_t uiFullboxOverDraw(void) { return uiFullboxDraw(&UI_FULLBOX_OVER); } diff --git a/src/dusk/ui/uifullbox.h b/src/dusk/ui/uifullbox.h index c1f172cc..5c1accd3 100644 --- a/src/dusk/ui/uifullbox.h +++ b/src/dusk/ui/uifullbox.h @@ -66,12 +66,44 @@ void uiFullboxTransition( easingtype_t easing ); +/** + * Init function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). + * + * @return Any error that occurs. + */ +errorret_t uiFullboxUnderInit(void); + +/** + * Update function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). + * + * @return Any error that occurs. + */ +errorret_t uiFullboxUnderUpdate(void); + /** * Draw function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). + * + * @return Any error that occurs. */ errorret_t uiFullboxUnderDraw(void); +/** + * Init function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). + * + * @return Any error that occurs. + */ +errorret_t uiFullboxOverInit(void); + +/** + * Update function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). + * + * @return Any error that occurs. + */ +errorret_t uiFullboxOverUpdate(void); + /** * Draw function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). + * + * @return Any error that occurs. */ errorret_t uiFullboxOverDraw(void); diff --git a/src/dusk/ui/uiloading.c b/src/dusk/ui/uiloading.c index 89b04309..776869d5 100644 --- a/src/dusk/ui/uiloading.c +++ b/src/dusk/ui/uiloading.c @@ -8,6 +8,7 @@ #include "uiloading.h" #include "assert/assert.h" #include "util/memory.h" +#include "time/time.h" #include "display/text/text.h" #include "display/screen/screen.h" #include "display/spritebatch/spritebatch.h" @@ -17,7 +18,7 @@ uiloading_t UI_LOADING; -void uiLoadingInit(void) { +errorret_t uiLoadingInit(void) { memoryZero(&UI_LOADING, sizeof(uiloading_t)); eventInit( &UI_LOADING.onTransitionEnd, @@ -25,16 +26,18 @@ void uiLoadingInit(void) { UI_LOADING.onTransitionEndUsers, 4 ); + errorOk(); } -void uiLoadingUpdate(float_t delta) { - if(UI_LOADING.duration <= 0.0f || UI_LOADING.time >= UI_LOADING.duration) - return; - UI_LOADING.time += delta; - if(UI_LOADING.time >= UI_LOADING.duration) { - UI_LOADING.time = UI_LOADING.duration; - eventInvoke(&UI_LOADING.onTransitionEnd, &UI_LOADING); +errorret_t uiLoadingUpdate(void) { + if(UI_LOADING.duration > 0.0f && UI_LOADING.time < UI_LOADING.duration) { + UI_LOADING.time += TIME.delta; + if(UI_LOADING.time >= UI_LOADING.duration) { + UI_LOADING.time = UI_LOADING.duration; + eventInvoke(&UI_LOADING.onTransitionEnd, &UI_LOADING); + } } + errorOk(); } errorret_t uiLoadingDraw(void) { diff --git a/src/dusk/ui/uiloading.h b/src/dusk/ui/uiloading.h index 60bf9414..af67db6c 100644 --- a/src/dusk/ui/uiloading.h +++ b/src/dusk/ui/uiloading.h @@ -26,16 +26,18 @@ extern uiloading_t UI_LOADING; /** * Initializes the loading indicator. + * + * @return Any error that occurs. */ -void uiLoadingInit(void); +errorret_t uiLoadingInit(void); /** * Advances the loading indicator fade transition. Fires onTransitionEnd once * when the transition completes. * - * @param delta Seconds elapsed since last update. + * @return Any error that occurs. */ -void uiLoadingUpdate(float_t delta); +errorret_t uiLoadingUpdate(void); /** * Draws the loading indicator. No-op when fully transparent. diff --git a/src/dusk/ui/uitextbox.c b/src/dusk/ui/uitextbox.c index 487c1e9a..5b144ed8 100644 --- a/src/dusk/ui/uitextbox.c +++ b/src/dusk/ui/uitextbox.c @@ -271,7 +271,8 @@ errorret_t uiTextboxDraw(void) { errorOk(); } -void uiTextboxDispose(void) { +errorret_t uiTextboxDispose(void) { uiFrameDispose(&UI_TEXTBOX.frame); UI_TEXTBOX.font = NULL; + errorOk(); } diff --git a/src/dusk/ui/uitextbox.h b/src/dusk/ui/uitextbox.h index 51e73d68..60e45a90 100644 --- a/src/dusk/ui/uitextbox.h +++ b/src/dusk/ui/uitextbox.h @@ -115,5 +115,7 @@ void uiTextboxNextPage(void); /** * Disposes of UI_TEXTBOX, nulling out texture pointers. + * + * @return Any error that occurs. */ -void uiTextboxDispose(void); +errorret_t uiTextboxDispose(void); diff --git a/src/duskdolphin/input/inputdolphin.c b/src/duskdolphin/input/inputdolphin.c index 8dfc7403..c9fb4869 100644 --- a/src/duskdolphin/input/inputdolphin.c +++ b/src/duskdolphin/input/inputdolphin.c @@ -115,6 +115,7 @@ errorret_t inputInitDolphin(void) { X("b", INPUT_ACTION_CANCEL); X("z", INPUT_ACTION_CONSOLE); X("start", INPUT_ACTION_RAGEQUIT); + X("start", INPUT_ACTION_PAUSE); #elif defined(DUSK_WII) X("up", INPUT_ACTION_UP); @@ -130,6 +131,7 @@ errorret_t inputInitDolphin(void) { X("b", INPUT_ACTION_CANCEL); X("z", INPUT_ACTION_CONSOLE); X("start", INPUT_ACTION_RAGEQUIT); + X("start", INPUT_ACTION_PAUSE); // TODO: Wiimote, USB Keyboard, probably more. diff --git a/src/dusklinux/input/inputlinux.c b/src/dusklinux/input/inputlinux.c index 42ec3359..fb952efa 100644 --- a/src/dusklinux/input/inputlinux.c +++ b/src/dusklinux/input/inputlinux.c @@ -517,6 +517,7 @@ errorret_t inputInitLinux(void) { X("tab", INPUT_ACTION_CANCEL); X("q", INPUT_ACTION_CANCEL); X("escape", INPUT_ACTION_RAGEQUIT); + X("enter", INPUT_ACTION_PAUSE); X("`", INPUT_ACTION_CONSOLE); #endif @@ -528,6 +529,7 @@ errorret_t inputInitLinux(void) { X("gamepad_a", INPUT_ACTION_ACCEPT); X("gamepad_b", INPUT_ACTION_CANCEL); X("gamepad_back", INPUT_ACTION_RAGEQUIT); + X("gamepad_start", INPUT_ACTION_PAUSE); X("gamepad_lstick_up", INPUT_ACTION_UP); X("gamepad_lstick_down", INPUT_ACTION_DOWN); X("gamepad_lstick_left", INPUT_ACTION_LEFT); diff --git a/src/duskpsp/input/inputpsp.c b/src/duskpsp/input/inputpsp.c index 313ceb95..6bc7ef62 100644 --- a/src/duskpsp/input/inputpsp.c +++ b/src/duskpsp/input/inputpsp.c @@ -83,6 +83,7 @@ errorret_t inputInitPSP(void) { X("cancel", INPUT_ACTION_CANCEL); X("triangle", INPUT_ACTION_CONSOLE); X("select", INPUT_ACTION_RAGEQUIT); + X("start", INPUT_ACTION_PAUSE); X("lstick_up", INPUT_ACTION_UP); X("lstick_down", INPUT_ACTION_DOWN); X("lstick_left", INPUT_ACTION_LEFT);