diff --git a/src/dusk/rpg/entity/player.c b/src/dusk/rpg/entity/player.c index 9ea1fad6..632f726b 100644 --- a/src/dusk/rpg/entity/player.c +++ b/src/dusk/rpg/entity/player.c @@ -11,7 +11,7 @@ #include "util/memory.h" #include "time/time.h" #include "ui/focus/uifocus.h" -#include "ui/frame/uisettings.h" +#include "ui/frame/game/uigamemenu.h" #include "rpg/cutscene/cutscenesystem.h" void playerInit(entity_t *entity) { @@ -27,14 +27,14 @@ void playerInput(entity_t *entity) { assertNotNull(entity, "Entity pointer cannot be NULL"); if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return; - // Toggle settings on pause - if(uiSettingsIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) { - uiSettingsClose(); + // Toggle game menu on pause + if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) { + uiGameMenuClose(); } else if( inputPressed(INPUT_ACTION_PAUSE) && entity->animation == ENTITY_ANIM_IDLE ) { - uiSettingsOpen(); + uiGameMenuOpen(); } // Can player act? diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c index 49d86349..8ceb4b5a 100644 --- a/src/dusk/ui/focus/uifocus.c +++ b/src/dusk/ui/focus/uifocus.c @@ -31,6 +31,7 @@ uifocusitem_t * uiFocusPush( uifocusitemcallback_t selected, uifocusitemcallback_t changed, uifocusitemcallback_t closed, + uifocusitemdirectioncallback_t direction, void *user ) { assertTrue( @@ -47,6 +48,7 @@ uifocusitem_t * uiFocusPush( item->selected = selected; item->changed = changed; item->closed = closed; + item->direction = direction; item->user = user; UI_FOCUS.count++; UI_FOCUS.pushedThisTick = true; @@ -118,6 +120,14 @@ void uiFocusMoveDirection( } } +void uiFocusHandleDirection( + uifocusitem_t *item, + const uifocusdirection_t dir +) { + if(item->direction != NULL && item->direction(item, dir)) return; + uiFocusMoveDirection(item, dir); +} + void uiFocusUpdate(void) { if(UI_FOCUS.count == 0) return; @@ -150,7 +160,7 @@ void uiFocusUpdate(void) { if(!inputPressed(m->action)) continue; UI_FOCUS.direction = m->direction; UI_FOCUS.timeHeld = 0.0f; - uiFocusMoveDirection(item, m->direction); + uiFocusHandleDirection(item, m->direction); return; } @@ -176,5 +186,5 @@ void uiFocusUpdate(void) { if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return; if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return; UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY; - uiFocusMoveDirection(item, UI_FOCUS.direction); + uiFocusHandleDirection(item, UI_FOCUS.direction); } diff --git a/src/dusk/ui/focus/uifocus.h b/src/dusk/ui/focus/uifocus.h index 09379831..d68de640 100644 --- a/src/dusk/ui/focus/uifocus.h +++ b/src/dusk/ui/focus/uifocus.h @@ -22,14 +22,6 @@ */ #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; @@ -72,6 +64,8 @@ void uiFocusInit(void); * @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. + * @param direction Called on a direction press/hold before the default + * cell-to-cell movement is applied; may be NULL. * @param user Arbitrary pointer stored on the item before changed fires. * @returns Pointer to the newly pushed focus item. */ @@ -81,6 +75,7 @@ uifocusitem_t * uiFocusPush( uifocusitemcallback_t selected, uifocusitemcallback_t changed, uifocusitemcallback_t closed, + uifocusitemdirectioncallback_t direction, void *user ); @@ -118,6 +113,19 @@ void uiFocusMoveDirection( const uifocusdirection_t dir ); +/** + * Handles a direction press/hold for the given item: gives the item's + * direction callback (if any) first refusal, falling back to the + * default cell-to-cell movement if it's unset or returns false. + * + * @param item The focus item to move. + * @param dir Direction that was pressed or held. + */ +void uiFocusHandleDirection( + uifocusitem_t *item, + const uifocusdirection_t dir +); + /** * Updates the focus system. Handles first-press movement and * held-direction repeating. Called once per game tick. diff --git a/src/dusk/ui/focus/uifocusitem.h b/src/dusk/ui/focus/uifocusitem.h index 431835ac..413e89ee 100644 --- a/src/dusk/ui/focus/uifocusitem.h +++ b/src/dusk/ui/focus/uifocusitem.h @@ -10,6 +10,14 @@ typedef struct uifocusitem_s uifocusitem_t; +typedef enum { + UI_FOCUS_DIRECTION_NONE, + UI_FOCUS_DIRECTION_UP, + UI_FOCUS_DIRECTION_DOWN, + UI_FOCUS_DIRECTION_LEFT, + UI_FOCUS_DIRECTION_RIGHT +} uifocusdirection_t; + /** * Callback invoked when a focus item's selected cell changes. * @@ -17,6 +25,21 @@ typedef struct uifocusitem_s uifocusitem_t; */ typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item); +/** + * Callback invoked when a direction is pressed or held-repeated while + * this item is focused, before the default cell-to-cell movement is + * applied. + * + * @param item The focus item that is currently focused. + * @param direction The direction that was pressed. + * @returns True if the direction was fully handled and the default + * grid movement should be skipped; false to fall through to it. + */ +typedef bool_t (*uifocusitemdirectioncallback_t)( + const uifocusitem_t *item, + const uifocusdirection_t direction +); + /** * 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. @@ -29,5 +52,6 @@ struct uifocusitem_s { uifocusitemcallback_t selected; uifocusitemcallback_t changed; uifocusitemcallback_t closed; + uifocusitemdirectioncallback_t direction; void *user; }; diff --git a/src/dusk/ui/frame/CMakeLists.txt b/src/dusk/ui/frame/CMakeLists.txt index 98b01056..d23136c4 100644 --- a/src/dusk/ui/frame/CMakeLists.txt +++ b/src/dusk/ui/frame/CMakeLists.txt @@ -6,5 +6,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uiframe.c - uisettings.c ) + +add_subdirectory(game) +add_subdirectory(settings) diff --git a/src/dusk/ui/frame/game/CMakeLists.txt b/src/dusk/ui/frame/game/CMakeLists.txt new file mode 100644 index 00000000..4b79eda6 --- /dev/null +++ b/src/dusk/ui/frame/game/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 + uigamemenu.c +) diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c new file mode 100644 index 00000000..cbd79a2a --- /dev/null +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uigamemenu.h" +#include "ui/frame/uiframe.h" +#include "ui/frame/settings/uisettings.h" +#include "util/memory.h" +#include "display/spritebatch/spritebatch.h" +#include "display/screen/screen.h" +#include "assert/assert.h" + +#define UI_GAME_MENU_INDEX_CHARACTERS 0 +#define UI_GAME_MENU_INDEX_SETTINGS 1 + +uigamemenu_t UI_GAME_MENU; + +void uiGameMenuSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + if(index == UI_GAME_MENU_INDEX_SETTINGS) uiSettingsOpen(); +} + +errorret_t uiGameMenuInit(void) { + memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); + + uimenu_t *menu = &UI_GAME_MENU.menu; + uiMenuInit(menu, uiGameMenuSelected, NULL, NULL); + menu->items = UI_GAME_MENU.items; + + MENU_BEGIN + MENU_BUTTON("Characters"); + MENU_BUTTON("Settings"); + + assertTrue( + menuIndex <= UI_GAME_MENU_ITEM_COUNT, + "Game menu item count mismatch" + ); + + uiMenuSetItems(menu, UI_GAME_MENU.items, menuIndex, 1); + + errorOk(); +} + +errorret_t uiGameMenuDraw(void) { + if(!uiMenuIsActive(&UI_GAME_MENU.menu)) errorOk(); + + const float_t width = UI_GAME_MENU_WIDTH; + const float_t height = (float_t)SCREEN.scanHeight; + const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width; + const float_t y = (float_t)SCREEN.scanY; + + errorChain(uiFrameDraw(x, y, width, height)); + errorChain(uiMenuDraw( + &UI_GAME_MENU.menu, + x + UI_FRAME_START_X, + y + UI_FRAME_START_Y, + width - (UI_FRAME_START_X * 2), + height - (UI_FRAME_START_Y * 2) + )); + + errorChain(spriteBatchFlush()); + errorOk(); +} + +bool_t uiGameMenuIsOpen(void) { + return uiMenuIsActive(&UI_GAME_MENU.menu); +} + +void uiGameMenuOpen() { + uiMenuOpen(&UI_GAME_MENU.menu); +} + +void uiGameMenuClose() { + uiMenuClose(&UI_GAME_MENU.menu); +} + +errorret_t uiGameMenuDispose(void) { + errorOk(); +} diff --git a/src/dusk/ui/frame/game/uigamemenu.h b/src/dusk/ui/frame/game/uigamemenu.h new file mode 100644 index 00000000..45235f47 --- /dev/null +++ b/src/dusk/ui/frame/game/uigamemenu.h @@ -0,0 +1,58 @@ +/** + * 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 "ui/widget/uimenu.h" + +#define UI_GAME_MENU_ITEM_COUNT 2 +#define UI_GAME_MENU_WIDTH 150.0f + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT]; +} uigamemenu_t; + +extern uigamemenu_t UI_GAME_MENU; + +/** + * Initializes the game menu panel. + * + * @return Any error that occurs. + */ +errorret_t uiGameMenuInit(void); + +/** + * Draws the game menu panel. No-op when not visible. + * + * @return Any error that occurs. + */ +errorret_t uiGameMenuDraw(void); + +/** + * Returns true when the game menu panel is currently open. + * + * @returns True if open. + */ +bool_t uiGameMenuIsOpen(void); + +/** + * Opens the game menu panel. No-op when already open. + */ +void uiGameMenuOpen(); + +/** + * Closes the game menu panel. No-op when already closed. + */ +void uiGameMenuClose(); + +/** + * Disposes of the game menu panel. + * + * @return Any error that occurs. + */ +errorret_t uiGameMenuDispose(void); diff --git a/src/dusk/ui/frame/settings/CMakeLists.txt b/src/dusk/ui/frame/settings/CMakeLists.txt new file mode 100644 index 00000000..61e464ab --- /dev/null +++ b/src/dusk/ui/frame/settings/CMakeLists.txt @@ -0,0 +1,13 @@ +# 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 + uisettings.c + uisettingsgameplay.c + uisettingsinput.c + uisettingsdisplay.c + uisettingsaudio.c +) diff --git a/src/dusk/ui/frame/settings/uisettings.c b/src/dusk/ui/frame/settings/uisettings.c new file mode 100644 index 00000000..19572710 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettings.c @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uisettings.h" +#include "uisettingsgameplay.h" +#include "uisettingsinput.h" +#include "uisettingsdisplay.h" +#include "uisettingsaudio.h" +#include "ui/frame/uiframe.h" +#include "util/memory.h" +#include "display/spritebatch/spritebatch.h" +#include "display/text/text.h" +#include "display/screen/screen.h" +#include "assert/assert.h" + +uisettings_t UI_SETTINGS; + +void uiSettingsTabSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + switch(index) { + case UI_SETTINGS_TAB_GAMEPLAY: + uiSettingsGameplayOpen(); + break; + + case UI_SETTINGS_TAB_INPUT: + uiSettingsInputOpen(); + break; + + case UI_SETTINGS_TAB_DISPLAY: + uiSettingsDisplayOpen(); + break; + + case UI_SETTINGS_TAB_AUDIO: + uiSettingsAudioOpen(); + break; + + default: + break; + } +} + +errorret_t uiSettingsInit(void) { + memoryZero(&UI_SETTINGS, sizeof(uisettings_t)); + + uimenu_t *menu = &UI_SETTINGS.tabsMenu; + uiMenuInit(menu, uiSettingsTabSelected, NULL, NULL); + menu->items = UI_SETTINGS.tabs; + + MENU_BEGIN + MENU_TAB("Gameplay"); + MENU_TAB("Input"); + MENU_TAB("Display"); + MENU_TAB("Audio"); + + assertTrue( + menuIndex <= UI_SETTINGS_TAB_COUNT, + "Settings tab count mismatch" + ); + + uiMenuSetItems(menu, UI_SETTINGS.tabs, menuIndex, menuIndex); + + errorChain(uiSettingsGameplayInit()); + errorChain(uiSettingsInputInit()); + errorChain(uiSettingsDisplayInit()); + errorChain(uiSettingsAudioInit()); + + errorOk(); +} + +errorret_t uiSettingsDraw(void) { + if(!uiMenuIsActive(&UI_SETTINGS.tabsMenu)) errorOk(); + + const float_t width = 220.0f; + const float_t height = 150.0f; + const float_t x = (float_t)SCREEN.scanX + + ((float_t)SCREEN.scanWidth - width) * 0.5f; + const float_t y = (float_t)SCREEN.scanY + + ((float_t)SCREEN.scanHeight - height) * 0.5f; + + errorChain(uiFrameDraw(x, y, width, height)); + + const float_t contentX = x + UI_FRAME_START_X; + const float_t contentY = y + UI_FRAME_START_Y; + const float_t contentWidth = width - (UI_FRAME_START_X * 2); + const float_t contentHeight = height - (UI_FRAME_START_Y * 2); + + errorChain(uiMenuDraw( + &UI_SETTINGS.tabsMenu, + contentX, + contentY, + contentWidth, + contentHeight + )); + + const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + const float_t pageY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y; + const float_t pageHeight = contentHeight - tabsRowHeight - UI_FRAME_PADDING_Y; + + if(uiTabIsActive(&UI_SETTINGS.tabs[UI_SETTINGS_TAB_GAMEPLAY].tab)) { + errorChain(uiSettingsGameplayDraw(contentX, pageY, contentWidth, pageHeight)); + } else if(uiTabIsActive(&UI_SETTINGS.tabs[UI_SETTINGS_TAB_INPUT].tab)) { + errorChain(uiSettingsInputDraw(contentX, pageY, contentWidth, pageHeight)); + } else if(uiTabIsActive(&UI_SETTINGS.tabs[UI_SETTINGS_TAB_DISPLAY].tab)) { + errorChain(uiSettingsDisplayDraw(contentX, pageY, contentWidth, pageHeight)); + } else if(uiTabIsActive(&UI_SETTINGS.tabs[UI_SETTINGS_TAB_AUDIO].tab)) { + errorChain(uiSettingsAudioDraw(contentX, pageY, contentWidth, pageHeight)); + } + + errorChain(spriteBatchFlush()); + errorOk(); +} + +bool_t uiSettingsIsOpen(void) { + return uiMenuIsActive(&UI_SETTINGS.tabsMenu); +} + +void uiSettingsOpen() { + uiMenuOpen(&UI_SETTINGS.tabsMenu); +} + +void uiSettingsClose() { + uiMenuClose(&UI_SETTINGS.tabsMenu); +} + +errorret_t uiSettingsDispose(void) { + errorOk(); +} diff --git a/src/dusk/ui/frame/uisettings.h b/src/dusk/ui/frame/settings/uisettings.h similarity index 75% rename from src/dusk/ui/frame/uisettings.h rename to src/dusk/ui/frame/settings/uisettings.h index 928bb4ed..26f14a12 100644 --- a/src/dusk/ui/frame/uisettings.h +++ b/src/dusk/ui/frame/settings/uisettings.h @@ -9,17 +9,21 @@ #include "error/error.h" #include "ui/widget/uimenu.h" -#define UI_SETTINGS_ITEM_COUNT 6 +#define UI_SETTINGS_TAB_GAMEPLAY 0 +#define UI_SETTINGS_TAB_INPUT 1 +#define UI_SETTINGS_TAB_DISPLAY 2 +#define UI_SETTINGS_TAB_AUDIO 3 +#define UI_SETTINGS_TAB_COUNT 4 typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_SETTINGS_ITEM_COUNT]; + uimenu_t tabsMenu; + uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT]; } uisettings_t; extern uisettings_t UI_SETTINGS; /** - * Initializes the settings panel. + * Initializes the settings panel and all of its category pages. * * @return Any error that occurs. */ diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.c b/src/dusk/ui/frame/settings/uisettingsaudio.c new file mode 100644 index 00000000..699af56f --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsaudio.c @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uisettingsaudio.h" +#include "util/memory.h" + +uisettingsaudio_t UI_SETTINGS_AUDIO; + +errorret_t uiSettingsAudioInit(void) { + memoryZero(&UI_SETTINGS_AUDIO, sizeof(uisettingsaudio_t)); + + uimenu_t *menu = &UI_SETTINGS_AUDIO.menu; + uiMenuInit(menu, NULL, NULL, NULL); + menu->items = UI_SETTINGS_AUDIO.items; + + MENU_BEGIN + MENU_LABEL("No audio settings yet"); + + uiMenuSetItems(menu, UI_SETTINGS_AUDIO.items, menuIndex, 1); + + errorOk(); +} + +errorret_t uiSettingsAudioDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +) { + return uiMenuDraw(&UI_SETTINGS_AUDIO.menu, x, y, width, height); +} + +bool_t uiSettingsAudioIsOpen(void) { + return uiMenuIsActive(&UI_SETTINGS_AUDIO.menu); +} + +void uiSettingsAudioOpen(void) { + uiMenuOpen(&UI_SETTINGS_AUDIO.menu); +} + +void uiSettingsAudioClose(void) { + uiMenuClose(&UI_SETTINGS_AUDIO.menu); +} diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.h b/src/dusk/ui/frame/settings/uisettingsaudio.h new file mode 100644 index 00000000..bad154e0 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsaudio.h @@ -0,0 +1,59 @@ +/** + * 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 "ui/widget/uimenu.h" + +#define UI_SETTINGS_AUDIO_ITEM_COUNT 1 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT]; +} uisettingsaudio_t; + +extern uisettingsaudio_t UI_SETTINGS_AUDIO; + +/** + * Initializes the audio settings page. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsAudioInit(void); + +/** + * Draws the audio settings page at the given content rect. + * + * @param x Screen x position. + * @param y Screen y position. + * @param width Content width. + * @param height Content height. + * @return Any error that occurs. + */ +errorret_t uiSettingsAudioDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +/** + * Returns true when the audio settings page is currently open. + * + * @returns True if open. + */ +bool_t uiSettingsAudioIsOpen(void); + +/** + * Opens the audio settings page. No-op when already open. + */ +void uiSettingsAudioOpen(void); + +/** + * Closes the audio settings page. No-op when already closed. + */ +void uiSettingsAudioClose(void); diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.c b/src/dusk/ui/frame/settings/uisettingsdisplay.c new file mode 100644 index 00000000..9c28ec26 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsdisplay.c @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uisettingsdisplay.h" +#include "util/memory.h" + +uisettingsdisplay_t UI_SETTINGS_DISPLAY; + +errorret_t uiSettingsDisplayInit(void) { + memoryZero(&UI_SETTINGS_DISPLAY, sizeof(uisettingsdisplay_t)); + + uimenu_t *menu = &UI_SETTINGS_DISPLAY.menu; + uiMenuInit(menu, NULL, NULL, NULL); + menu->items = UI_SETTINGS_DISPLAY.items; + + MENU_BEGIN + MENU_LABEL("No display settings yet"); + + uiMenuSetItems(menu, UI_SETTINGS_DISPLAY.items, menuIndex, 1); + + errorOk(); +} + +errorret_t uiSettingsDisplayDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +) { + return uiMenuDraw(&UI_SETTINGS_DISPLAY.menu, x, y, width, height); +} + +bool_t uiSettingsDisplayIsOpen(void) { + return uiMenuIsActive(&UI_SETTINGS_DISPLAY.menu); +} + +void uiSettingsDisplayOpen(void) { + uiMenuOpen(&UI_SETTINGS_DISPLAY.menu); +} + +void uiSettingsDisplayClose(void) { + uiMenuClose(&UI_SETTINGS_DISPLAY.menu); +} diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.h b/src/dusk/ui/frame/settings/uisettingsdisplay.h new file mode 100644 index 00000000..d8f913f4 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsdisplay.h @@ -0,0 +1,59 @@ +/** + * 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 "ui/widget/uimenu.h" + +#define UI_SETTINGS_DISPLAY_ITEM_COUNT 1 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT]; +} uisettingsdisplay_t; + +extern uisettingsdisplay_t UI_SETTINGS_DISPLAY; + +/** + * Initializes the display settings page. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsDisplayInit(void); + +/** + * Draws the display settings page at the given content rect. + * + * @param x Screen x position. + * @param y Screen y position. + * @param width Content width. + * @param height Content height. + * @return Any error that occurs. + */ +errorret_t uiSettingsDisplayDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +/** + * Returns true when the display settings page is currently open. + * + * @returns True if open. + */ +bool_t uiSettingsDisplayIsOpen(void); + +/** + * Opens the display settings page. No-op when already open. + */ +void uiSettingsDisplayOpen(void); + +/** + * Closes the display settings page. No-op when already closed. + */ +void uiSettingsDisplayClose(void); diff --git a/src/dusk/ui/frame/settings/uisettingsgameplay.c b/src/dusk/ui/frame/settings/uisettingsgameplay.c new file mode 100644 index 00000000..7f6329be --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsgameplay.c @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uisettingsgameplay.h" +#include "assert/assert.h" +#include "util/memory.h" + +uisettingsgameplay_t UI_SETTINGS_GAMEPLAY; + +errorret_t uiSettingsGameplayInit(void) { + memoryZero(&UI_SETTINGS_GAMEPLAY, sizeof(uisettingsgameplay_t)); + + uimenu_t *menu = &UI_SETTINGS_GAMEPLAY.menu; + uiMenuInit(menu, NULL, NULL, NULL); + menu->items = UI_SETTINGS_GAMEPLAY.items; + + MENU_BEGIN + MENU_BUTTON("Do thing"); + MENU_BUTTON("Do other thing"); + MENU_CHECKBOX("Enable thing?"); + MENU_SLIDER_FLOAT("test", 1.0f, 1.0f, 4.0f, 0.5f); + + assertTrue( + menuIndex <= UI_SETTINGS_GAMEPLAY_ITEM_COUNT, + "Gameplay settings item count mismatch" + ); + + uiMenuSetItems(menu, UI_SETTINGS_GAMEPLAY.items, menuIndex, 1); + + errorOk(); +} + +errorret_t uiSettingsGameplayDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +) { + return uiMenuDraw(&UI_SETTINGS_GAMEPLAY.menu, x, y, width, height); +} + +bool_t uiSettingsGameplayIsOpen(void) { + return uiMenuIsActive(&UI_SETTINGS_GAMEPLAY.menu); +} + +void uiSettingsGameplayOpen(void) { + uiMenuOpen(&UI_SETTINGS_GAMEPLAY.menu); +} + +void uiSettingsGameplayClose(void) { + uiMenuClose(&UI_SETTINGS_GAMEPLAY.menu); +} diff --git a/src/dusk/ui/frame/settings/uisettingsgameplay.h b/src/dusk/ui/frame/settings/uisettingsgameplay.h new file mode 100644 index 00000000..d7f35390 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsgameplay.h @@ -0,0 +1,59 @@ +/** + * 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 "ui/widget/uimenu.h" + +#define UI_SETTINGS_GAMEPLAY_ITEM_COUNT 4 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_GAMEPLAY_ITEM_COUNT]; +} uisettingsgameplay_t; + +extern uisettingsgameplay_t UI_SETTINGS_GAMEPLAY; + +/** + * Initializes the gameplay settings page. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsGameplayInit(void); + +/** + * Draws the gameplay settings page at the given content rect. + * + * @param x Screen x position. + * @param y Screen y position. + * @param width Content width. + * @param height Content height. + * @return Any error that occurs. + */ +errorret_t uiSettingsGameplayDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +/** + * Returns true when the gameplay settings page is currently open. + * + * @returns True if open. + */ +bool_t uiSettingsGameplayIsOpen(void); + +/** + * Opens the gameplay settings page. No-op when already open. + */ +void uiSettingsGameplayOpen(void); + +/** + * Closes the gameplay settings page. No-op when already closed. + */ +void uiSettingsGameplayClose(void); diff --git a/src/dusk/ui/frame/settings/uisettingsinput.c b/src/dusk/ui/frame/settings/uisettingsinput.c new file mode 100644 index 00000000..7aedb892 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsinput.c @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uisettingsinput.h" +#include "util/memory.h" + +uisettingsinput_t UI_SETTINGS_INPUT; + +errorret_t uiSettingsInputInit(void) { + memoryZero(&UI_SETTINGS_INPUT, sizeof(uisettingsinput_t)); + + uimenu_t *menu = &UI_SETTINGS_INPUT.menu; + uiMenuInit(menu, NULL, NULL, NULL); + menu->items = UI_SETTINGS_INPUT.items; + + MENU_BEGIN + MENU_LABEL("No input settings yet"); + + uiMenuSetItems(menu, UI_SETTINGS_INPUT.items, menuIndex, 1); + + errorOk(); +} + +errorret_t uiSettingsInputDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +) { + return uiMenuDraw(&UI_SETTINGS_INPUT.menu, x, y, width, height); +} + +bool_t uiSettingsInputIsOpen(void) { + return uiMenuIsActive(&UI_SETTINGS_INPUT.menu); +} + +void uiSettingsInputOpen(void) { + uiMenuOpen(&UI_SETTINGS_INPUT.menu); +} + +void uiSettingsInputClose(void) { + uiMenuClose(&UI_SETTINGS_INPUT.menu); +} diff --git a/src/dusk/ui/frame/settings/uisettingsinput.h b/src/dusk/ui/frame/settings/uisettingsinput.h new file mode 100644 index 00000000..2976fb78 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsinput.h @@ -0,0 +1,59 @@ +/** + * 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 "ui/widget/uimenu.h" + +#define UI_SETTINGS_INPUT_ITEM_COUNT 1 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT]; +} uisettingsinput_t; + +extern uisettingsinput_t UI_SETTINGS_INPUT; + +/** + * Initializes the input settings page. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsInputInit(void); + +/** + * Draws the input settings page at the given content rect. + * + * @param x Screen x position. + * @param y Screen y position. + * @param width Content width. + * @param height Content height. + * @return Any error that occurs. + */ +errorret_t uiSettingsInputDraw( + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +/** + * Returns true when the input settings page is currently open. + * + * @returns True if open. + */ +bool_t uiSettingsInputIsOpen(void); + +/** + * Opens the input settings page. No-op when already open. + */ +void uiSettingsInputOpen(void); + +/** + * Closes the input settings page. No-op when already closed. + */ +void uiSettingsInputClose(void); diff --git a/src/dusk/ui/frame/uiframe.c b/src/dusk/ui/frame/uiframe.c index 0406265a..5202b3e1 100644 --- a/src/dusk/ui/frame/uiframe.c +++ b/src/dusk/ui/frame/uiframe.c @@ -19,8 +19,8 @@ errorret_t uiFrameInit(void) { memoryZero(&UI_FRAME, sizeof(uiframe_t)); // Init texture. - color_t border = color4b(0, 100, 220, 255); - color_t center = color4b(0, 100, 220, 200); + color_t border = color4b(0, 50, 255, 255); + color_t center = color4b(0, 100, 220, 240); for(uint8_t y = 0; y < UI_FRAME_TEXTURE_HEIGHT_POW2; y++) { for(uint8_t x = 0; x < UI_FRAME_TEXTURE_WIDTH_POW2; x++) { diff --git a/src/dusk/ui/frame/uiframe.h b/src/dusk/ui/frame/uiframe.h index 9fef7257..25fdc277 100644 --- a/src/dusk/ui/frame/uiframe.h +++ b/src/dusk/ui/frame/uiframe.h @@ -12,6 +12,10 @@ #define UI_FRAME_BORDER_WIDTH 6 #define UI_FRAME_BORDER_HEIGHT 6 +#define UI_FRAME_PADDING_X 4 +#define UI_FRAME_PADDING_Y 4 +#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X) +#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y) #define UI_FRAME_TILE_WIDTH 1 #define UI_FRAME_TILE_HEIGHT 1 #define UI_FRAME_TEXTURE_WIDTH (UI_FRAME_TILE_WIDTH * 3) diff --git a/src/dusk/ui/frame/uisettings.c b/src/dusk/ui/frame/uisettings.c deleted file mode 100644 index e95073e2..00000000 --- a/src/dusk/ui/frame/uisettings.c +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uisettings.h" -#include "ui/frame/uiframe.h" -#include "util/memory.h" -#include "display/spritebatch/spritebatch.h" -#include "display/text/text.h" -#include "display/screen/screen.h" -#include "assert/assert.h" - -uisettings_t UI_SETTINGS; - -void uiSettingsSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { - uiCheckboxToggle(&UI_SETTINGS.items[index].checkbox); - } -} - -void uiSettingsClosed(const uimenu_t *menu) { - // nothing extra needed; menu clears focusItem itself -} - -errorret_t uiSettingsInit(void) { - memoryZero(&UI_SETTINGS, sizeof(uisettings_t)); - - uint8_t i = 0; - - UI_SETTINGS.items[i] = (uimenuitem_t){ - .type = UI_MENU_WIDGET_TYPE_LABEL, - .label = "Settings" - }; - ++i; - - UI_SETTINGS.items[i].type = UI_MENU_WIDGET_TYPE_BUTTON; - uiButtonInit(&UI_SETTINGS.items[i].button, "Do thing"); - ++i; - - UI_SETTINGS.items[i].type = UI_MENU_WIDGET_TYPE_BUTTON; - uiButtonInit(&UI_SETTINGS.items[i].button, "Do other thing"); - ++i; - - UI_SETTINGS.items[i].type = UI_MENU_WIDGET_TYPE_SPACER; - ++i; - - UI_SETTINGS.items[i] = (uimenuitem_t){ - .type = UI_MENU_WIDGET_TYPE_LABEL, - .label = "Options" - }; - ++i; - - UI_SETTINGS.items[i].type = UI_MENU_WIDGET_TYPE_CHECKBOX; - uiCheckboxInit(&UI_SETTINGS.items[i].checkbox, "Enable thing?"); - ++i; - - assertTrue(i <= UI_SETTINGS_ITEM_COUNT, "Settings item count mismatch"); - - uiMenuInit(&UI_SETTINGS.menu, uiSettingsSelected, uiSettingsClosed, NULL); - uiMenuSetItems( - &UI_SETTINGS.menu, - UI_SETTINGS.items, - i, 1 - ); - - errorOk(); -} - -errorret_t uiSettingsDraw(void) { - if(!uiMenuIsActive(&UI_SETTINGS.menu)) errorOk(); - - const float_t width = 300.0f; - const float_t height = 300.0f; - const float_t x = (float_t)SCREEN.scanX + - ((float_t)SCREEN.scanWidth - width) * 0.5f; - const float_t y = (float_t)SCREEN.scanY + - ((float_t)SCREEN.scanHeight - height) * 0.5f; - - errorChain(uiFrameDraw(x, y, width, height)); - errorChain(uiMenuDraw( - &UI_SETTINGS.menu, - x + UI_FRAME_BORDER_WIDTH, - y + UI_FRAME_BORDER_HEIGHT, - width - (UI_FRAME_BORDER_WIDTH * 2), - height - (UI_FRAME_BORDER_HEIGHT * 2) - )); - - errorChain(spriteBatchFlush()); - errorOk(); -} - -bool_t uiSettingsIsOpen(void) { - return uiMenuIsActive(&UI_SETTINGS.menu); -} - -void uiSettingsOpen() { - uiMenuOpen(&UI_SETTINGS.menu); -} - -void uiSettingsClose() { - uiMenuClose(&UI_SETTINGS.menu); -} - -errorret_t uiSettingsDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/rpg/uitextbox.c b/src/dusk/ui/rpg/uitextbox.c index a08636fe..088ab652 100644 --- a/src/dusk/ui/rpg/uitextbox.c +++ b/src/dusk/ui/rpg/uitextbox.c @@ -144,12 +144,12 @@ errorret_t uiTextboxDraw( ) { assertNotNull(box, "Textbox cannot be NULL"); - float_t borderW = (float_t)UI_FRAME_BORDER_WIDTH; - float_t borderH = (float_t)UI_FRAME_BORDER_HEIGHT; - float_t contentX = x + borderW; - float_t contentY = y + borderH; - float_t contentW = width - 2.0f * borderW; - float_t contentH = height - 2.0f * borderH; + float_t startX = (float_t)UI_FRAME_START_X; + float_t startY = (float_t)UI_FRAME_START_Y; + float_t contentX = x + startX; + float_t contentY = y + startY; + float_t contentW = width - 2.0f * startX; + float_t contentH = height - 2.0f * startY; if(contentW != box->layoutWidth || contentH != box->layoutHeight) { uiTextboxBuildLayout(box, contentW, contentH); diff --git a/src/dusk/ui/rpg/uitextboxmain.c b/src/dusk/ui/rpg/uitextboxmain.c index 263f2d85..65c69644 100644 --- a/src/dusk/ui/rpg/uitextboxmain.c +++ b/src/dusk/ui/rpg/uitextboxmain.c @@ -27,6 +27,7 @@ void uiTextboxMainSetText(const char_t *text) { uiTextboxMainFocusSelected, NULL, uiTextboxMainFocusClosed, + NULL, NULL ); } @@ -41,7 +42,7 @@ errorret_t uiTextboxMainDraw(void) { float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH + (float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING + - 2.0f * (float_t)UI_FRAME_BORDER_HEIGHT; + 2.0f * (float_t)UI_FRAME_START_Y; float_t w = (float_t)SCREEN.scanWidth; float_t x = (float_t)SCREEN.scanX; float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h; diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 60eae45d..943ea614 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -16,7 +16,8 @@ #include "ui/overlay/uicrop.h" #include "ui/transition/uitransition.h" #include "ui/debug/uiconsole.h" -#include "ui/frame/uisettings.h" +#include "ui/frame/settings/uisettings.h" +#include "ui/frame/game/uigamemenu.h" #include "ui/rpg/uitextboxmain.h" uielement_t UI_ELEMENTS[] = { @@ -33,6 +34,12 @@ uielement_t UI_ELEMENTS[] = { .draw = uiFullboxUnderDraw }, + { + .init = uiGameMenuInit, + .draw = uiGameMenuDraw, + .dispose = uiGameMenuDispose + }, + { .init = uiSettingsInit, .draw = uiSettingsDraw, diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index 028e726e..c33d236f 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -7,5 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uibutton.c uicheckbox.c + uitab.c + uislider.c uimenu.c ) diff --git a/src/dusk/ui/widget/uibutton.c b/src/dusk/ui/widget/uibutton.c index 912f2946..732c122d 100644 --- a/src/dusk/ui/widget/uibutton.c +++ b/src/dusk/ui/widget/uibutton.c @@ -8,7 +8,6 @@ #include "uibutton.h" #include "assert/assert.h" #include "util/memory.h" -#include "util/string.h" #include "display/text/text.h" #include "display/color.h" diff --git a/src/dusk/ui/widget/uicheckbox.c b/src/dusk/ui/widget/uicheckbox.c index afbf34dc..59e6ad10 100644 --- a/src/dusk/ui/widget/uicheckbox.c +++ b/src/dusk/ui/widget/uicheckbox.c @@ -7,7 +7,6 @@ #include "uicheckbox.h" #include "util/memory.h" -#include "util/string.h" #include "display/text/text.h" #include "display/color.h" diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c index 5ff2e4b7..2de63ef2 100644 --- a/src/dusk/ui/widget/uimenu.c +++ b/src/dusk/ui/widget/uimenu.c @@ -11,6 +11,8 @@ #include "display/text/text.h" #include "display/color.h" #include "ui/widget/uibutton.h" +#include "ui/widget/uitab.h" +#include "ui/widget/uislider.h" void uiMenuInit( uimenu_t *menu, @@ -61,6 +63,7 @@ void uiMenuOpen(uimenu_t *menu) { uiMenuFocusSelected, uiMenuFocusChanged, uiMenuFocusClosed, + uiMenuFocusDirection, menu ); } @@ -127,6 +130,14 @@ errorret_t uiMenuDraw( errorChain(uiButtonDraw(&item->button, ix, iy)); break; + case UI_MENU_WIDGET_TYPE_TAB: + errorChain(uiTabDraw(&item->tab, ix, iy)); + break; + + case UI_MENU_WIDGET_TYPE_SLIDER: + errorChain(uiSliderDraw(&item->slider, ix, iy)); + break; + default: break; } @@ -166,12 +177,7 @@ bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem) { item->type == UI_MENU_WIDGET_TYPE_LABEL || item->type == UI_MENU_WIDGET_TYPE_SPACER ) continue; - bool_t isActive = (slot == focusSlot); - if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { - uiCheckboxSetHighlighted(&item->checkbox, isActive); - } else if(item->type == UI_MENU_WIDGET_TYPE_BUTTON) { - uiButtonSetHighlighted(&item->button, isActive); - } + uiMenuItemSetHighlighted(item, slot == focusSlot); slot++; } @@ -188,10 +194,50 @@ bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem) { assertNotNull(focusItem->user, "Focus item user cannot be NULL"); uimenu_t *menu = (uimenu_t *)focusItem->user; menu->focusItem = NULL; + + for(uint8_t i = 0; i < menu->itemCount; i++) { + uiMenuItemSetHighlighted(&menu->items[i], false); + } + if(menu->closed != NULL) menu->closed(menu); return true; } +bool_t uiMenuFocusDirection( + const uifocusitem_t *focusItem, + const uifocusdirection_t direction +) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + if(direction != UI_FOCUS_DIRECTION_LEFT && direction != UI_FOCUS_DIRECTION_RIGHT) { + return false; + } + + uimenu_t *menu = (uimenu_t *)focusItem->user; + uint8_t slot = focusItem->y * menu->columns + focusItem->x; + uint8_t index = uiMenuFocusSlotToIndex(menu, slot); + if(index == 0xFF) return false; + + uimenuitem_t *item = &menu->items[index]; + bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT; + + if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) { + if(right) { + uiSliderStepUp(&item->slider); + } else { + uiSliderStepDown(&item->slider); + } + return true; + } + + if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { + uiCheckboxToggle(&item->checkbox); + return true; + } + + return false; +} + uint8_t uiMenuFocusableCount(const uimenu_t *menu) { assertNotNull(menu, "Menu cannot be NULL"); uint8_t count = 0; @@ -213,3 +259,15 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) { } return 0xFF; } + +void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) { + if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { + uiCheckboxSetHighlighted(&item->checkbox, highlighted); + } else if(item->type == UI_MENU_WIDGET_TYPE_BUTTON) { + uiButtonSetHighlighted(&item->button, highlighted); + } else if(item->type == UI_MENU_WIDGET_TYPE_TAB) { + uiTabSetActive(&item->tab, highlighted); + } else if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) { + uiSliderSetHighlighted(&item->slider, highlighted); + } +} diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h index 0f8e0301..82a4e523 100644 --- a/src/dusk/ui/widget/uimenu.h +++ b/src/dusk/ui/widget/uimenu.h @@ -10,6 +10,8 @@ #include "ui/focus/uifocus.h" #include "ui/widget/uibutton.h" #include "ui/widget/uicheckbox.h" +#include "ui/widget/uitab.h" +#include "ui/widget/uislider.h" #define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX @@ -21,6 +23,8 @@ typedef enum { UI_MENU_WIDGET_TYPE_SPACER, UI_MENU_WIDGET_TYPE_CHECKBOX, UI_MENU_WIDGET_TYPE_BUTTON, + UI_MENU_WIDGET_TYPE_TAB, + UI_MENU_WIDGET_TYPE_SLIDER, } uimenuwidgettype_t; typedef struct { @@ -29,6 +33,8 @@ typedef struct { char_t *label; uicheckbox_t checkbox; uibutton_t button; + uitab_t tab; + uislider_t slider; }; } uimenuitem_t; @@ -157,6 +163,16 @@ uint8_t uiMenuFocusableCount(const uimenu_t *menu); */ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot); +/** + * Sets the highlighted/active state on a menu item, dispatching to the + * matching widget's setter based on its type. No-op for label/spacer + * items and any other type with no highlight concept. + * + * @param item The item to update. + * @param highlighted The new highlighted/active state. + */ +void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted); + /** * Internal focus callback - forwards selection to the menu's selected handler. * @@ -179,4 +195,59 @@ bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem); * @param focusItem The active focus item; user field must point to uimenu_t. * @returns True. */ -bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem); \ No newline at end of file +bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem); + +/** + * Internal focus callback - gives the focused item's widget a chance to + * handle LEFT/RIGHT itself (e.g. a slider adjusting its value) before + * falling back to the default cell-to-cell movement. + * + * @param focusItem The active focus item; user field must point to uimenu_t. + * @param direction The direction that was pressed or held. + * @returns True if the focused widget handled the direction. + */ +bool_t uiMenuFocusDirection( + const uifocusitem_t *focusItem, + const uifocusdirection_t direction +); + +// Helper macros +#define MENU_BEGIN uint8_t menuIndex = 0; + +#define MENU_LABEL(text) \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \ + menu->items[menuIndex].label = text; \ + ++menuIndex; + +#define MENU_SPACER() \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SPACER; \ + ++menuIndex; + +#define MENU_CHECKBOX(label) \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_CHECKBOX; \ + uiCheckboxInit(&menu->items[menuIndex].checkbox, label); \ + ++menuIndex; + +#define MENU_BUTTON(label) \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_BUTTON; \ + uiButtonInit(&menu->items[menuIndex].button, label); \ + ++menuIndex; + +#define MENU_TAB(label) \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_TAB; \ + uiTabInit(&menu->items[menuIndex].tab, label); \ + ++menuIndex; + +#define MENU_SLIDER_FLOAT(label, value, min, max, step) \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \ + uiSliderInitFloat( \ + &menu->items[menuIndex].slider, label, value, min, max, step \ + ); \ + ++menuIndex; + +#define MENU_SLIDER_INT(label, value, min, max, step) \ + menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \ + uiSliderInitInt( \ + &menu->items[menuIndex].slider, label, value, min, max, step \ + ); \ + ++menuIndex; diff --git a/src/dusk/ui/widget/uislider.c b/src/dusk/ui/widget/uislider.c new file mode 100644 index 00000000..95722b80 --- /dev/null +++ b/src/dusk/ui/widget/uislider.c @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uislider.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/math.h" +#include "util/string.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/texture/texture.h" +#include "display/shader/shaderunlit.h" + +void uiSliderInitFloat( + uislider_t *slider, + const char_t *label, + const float_t value, + const float_t min, + const float_t max, + const float_t step +) { + assertNotNull(slider, "Slider cannot be NULL"); + assertNotNull(label, "Label cannot be NULL"); + assertTrue(max > min, "Slider max must be greater than min"); + assertTrue(step > 0.0f, "Slider step must be greater than zero"); + + memoryZero(slider, sizeof(uislider_t)); + slider->label = label; + slider->type = UI_SLIDER_TYPE_FLOAT; + slider->min.f = min; + slider->max.f = max; + slider->step.f = step; + slider->value.f = mathClamp(value, min, max); +} + +void uiSliderInitInt( + uislider_t *slider, + const char_t *label, + const int32_t value, + const int32_t min, + const int32_t max, + const int32_t step +) { + assertNotNull(slider, "Slider cannot be NULL"); + assertNotNull(label, "Label cannot be NULL"); + assertTrue(max > min, "Slider max must be greater than min"); + assertTrue(step > 0, "Slider step must be greater than zero"); + + memoryZero(slider, sizeof(uislider_t)); + slider->label = label; + slider->type = UI_SLIDER_TYPE_INT; + slider->min.i = min; + slider->max.i = max; + slider->step.i = step; + slider->value.i = mathClamp(value, min, max); +} + +float_t uiSliderGetFloat(const uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + return slider->type == UI_SLIDER_TYPE_INT ? + (float_t)slider->value.i : slider->value.f; +} + +int32_t uiSliderGetInt(const uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + assertTrue( + slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider" + ); + return slider->value.i; +} + +void uiSliderSetFloat(uislider_t *slider, const float_t value) { + assertNotNull(slider, "Slider cannot be NULL"); + assertTrue( + slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider" + ); + slider->value.f = mathClamp(value, slider->min.f, slider->max.f); +} + +void uiSliderSetInt(uislider_t *slider, const int32_t value) { + assertNotNull(slider, "Slider cannot be NULL"); + assertTrue( + slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider" + ); + slider->value.i = mathClamp(value, slider->min.i, slider->max.i); +} + +void uiSliderStepUp(uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + if(slider->type == UI_SLIDER_TYPE_INT) { + int32_t next = slider->value.i + slider->step.i; + uiSliderSetInt(slider, next > slider->max.i ? slider->min.i : next); + } else { + float_t next = slider->value.f + slider->step.f; + uiSliderSetFloat(slider, next > slider->max.f ? slider->min.f : next); + } +} + +void uiSliderStepDown(uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + if(slider->type == UI_SLIDER_TYPE_INT) { + int32_t next = slider->value.i - slider->step.i; + uiSliderSetInt(slider, next < slider->min.i ? slider->max.i : next); + } else { + float_t next = slider->value.f - slider->step.f; + uiSliderSetFloat(slider, next < slider->min.f ? slider->max.f : next); + } +} + +float_t uiSliderGetRatio(const uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + if(slider->type == UI_SLIDER_TYPE_INT) { + if(slider->max.i == slider->min.i) return 0.0f; + return (float_t)(slider->value.i - slider->min.i) / + (float_t)(slider->max.i - slider->min.i); + } + + if(slider->max.f == slider->min.f) return 0.0f; + return (slider->value.f - slider->min.f) / (slider->max.f - slider->min.f); +} + +int32_t uiSliderGetStepCount(const uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + if(slider->type != UI_SLIDER_TYPE_INT || slider->step.i <= 0) return 0; + return (slider->max.i - slider->min.i) / slider->step.i; +} + +bool_t uiSliderIsHighlighted(const uislider_t *slider) { + assertNotNull(slider, "Slider cannot be NULL"); + return slider->highlighted; +} + +void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) { + assertNotNull(slider, "Slider cannot be NULL"); + slider->highlighted = highlighted; +} + +errorret_t uiSliderDraw( + const uislider_t *slider, + const float_t x, + const float_t y +) { + assertNotNull(slider, "Slider cannot be NULL"); + + color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE; + + errorChain(textDraw(x, y, slider->label, color, &FONT_DEFAULT)); + + int32_t labelW, labelH; + textMeasure(slider->label, &FONT_DEFAULT, &labelW, &labelH); + + float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP; + float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f; + + spritebatchsprite_t trackSprite = { + .min = { trackX, trackY, 0.0f }, + .max = { trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t trackMaterial = { + .unlit = { + .color = COLOR_DARK_GRAY, + .texture = &TEXTURE_WHITE + } + }; + errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial)); + errorChain(spriteBatchFlush()); + + float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider); + if(fillWidth > 0.0f) { + spritebatchsprite_t fillSprite = { + .min = { trackX, trackY, 0.0f }, + .max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t fillMaterial = { + .unlit = { + .color = color, + .texture = &TEXTURE_WHITE + } + }; + errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial)); + errorChain(spriteBatchFlush()); + } + + int32_t stepCount = uiSliderGetStepCount(slider); + if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) { + for(int32_t i = 0; i <= stepCount; i++) { + float_t markerX = trackX + + UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount; + spritebatchsprite_t markerSprite = { + .min = { + markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f, + trackY - UI_SLIDER_STEP_MARKER_OVERHANG, + 0.0f + }, + .max = { + markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f, + trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG, + 0.0f + }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t markerMaterial = { + .unlit = { + .color = COLOR_WHITE, + .texture = &TEXTURE_WHITE + } + }; + errorChain( + spriteBatchBuffer(&markerSprite, 1, &SHADER_UNLIT, markerMaterial) + ); + errorChain(spriteBatchFlush()); + } + } + + char_t valueText[UI_SLIDER_VALUE_TEXT_MAX]; + if(slider->type == UI_SLIDER_TYPE_INT) { + stringFormat( + valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%d", slider->value.i + ); + } else { + stringFormat( + valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%.2f", slider->value.f + ); + } + + errorChain(textDraw( + trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y, + valueText, color, &FONT_DEFAULT + )); + + errorOk(); +} diff --git a/src/dusk/ui/widget/uislider.h b/src/dusk/ui/widget/uislider.h new file mode 100644 index 00000000..d53dee00 --- /dev/null +++ b/src/dusk/ui/widget/uislider.h @@ -0,0 +1,185 @@ +/** + * 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_SLIDER_TRACK_WIDTH 80.0f +#define UI_SLIDER_TRACK_HEIGHT 4.0f +#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f +#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f +#define UI_SLIDER_GAP 4.0f +#define UI_SLIDER_VALUE_TEXT_MAX 16 + +/** + * Number of discrete steps below which an int slider renders individual + * step markers on its track instead of a smooth fill. + */ +#define UI_SLIDER_STEP_MARKERS_MAX 10 + +typedef enum { + UI_SLIDER_TYPE_FLOAT, + UI_SLIDER_TYPE_INT, + + UI_SLIDER_TYPE_COUNT +} uislidertype_t; + +typedef union { + float_t f; + int32_t i; +} uislidervalue_t; + +typedef struct { + const char_t *label; + uislidertype_t type; + uislidervalue_t value; + uislidervalue_t min; + uislidervalue_t max; + uislidervalue_t step; + bool_t highlighted; +} uislider_t; + +/** + * Initializes a floating point slider. + * + * @param slider The slider to initialize. + * @param label Display label. + * @param value Initial value, clamped to [min, max]. + * @param min Minimum value. + * @param max Maximum value. + * @param step The amount a single step moves the value by. + */ +void uiSliderInitFloat( + uislider_t *slider, + const char_t *label, + const float_t value, + const float_t min, + const float_t max, + const float_t step +); + +/** + * Initializes an integer slider. + * + * @param slider The slider to initialize. + * @param label Display label. + * @param value Initial value, clamped to [min, max]. + * @param min Minimum value. + * @param max Maximum value. + * @param step The amount a single step moves the value by. + */ +void uiSliderInitInt( + uislider_t *slider, + const char_t *label, + const int32_t value, + const int32_t min, + const int32_t max, + const int32_t step +); + +/** + * Returns the slider's current value as a float. Works for both slider + * types; int values are widened to float. + * + * @param slider The slider to query. + * @returns The current value. + */ +float_t uiSliderGetFloat(const uislider_t *slider); + +/** + * Returns the slider's current value as an int. Only valid for + * UI_SLIDER_TYPE_INT sliders. + * + * @param slider The slider to query. + * @returns The current value. + */ +int32_t uiSliderGetInt(const uislider_t *slider); + +/** + * Sets the slider's value, clamped to [min, max]. Only valid for + * UI_SLIDER_TYPE_FLOAT sliders. + * + * @param slider The slider to update. + * @param value The new value. + */ +void uiSliderSetFloat(uislider_t *slider, const float_t value); + +/** + * Sets the slider's value, clamped to [min, max]. Only valid for + * UI_SLIDER_TYPE_INT sliders. + * + * @param slider The slider to update. + * @param value The new value. + */ +void uiSliderSetInt(uislider_t *slider, const int32_t value); + +/** + * Moves the slider's value up by one step, wrapping around to its min + * if the step would exceed its max. + * + * @param slider The slider to update. + */ +void uiSliderStepUp(uislider_t *slider); + +/** + * Moves the slider's value down by one step, wrapping around to its + * max if the step would go below its min. + * + * @param slider The slider to update. + */ +void uiSliderStepDown(uislider_t *slider); + +/** + * Returns the slider's current value normalized to a 0..1 range based + * on its min/max. + * + * @param slider The slider to query. + * @returns The normalized value. + */ +float_t uiSliderGetRatio(const uislider_t *slider); + +/** + * Returns the number of discrete steps between min and max. Always 0 + * for float sliders. + * + * @param slider The slider to query. + * @returns The step count. + */ +int32_t uiSliderGetStepCount(const uislider_t *slider); + +/** + * Returns whether the slider is highlighted. + * + * @param slider The slider to query. + * @returns True if highlighted. + */ +bool_t uiSliderIsHighlighted(const uislider_t *slider); + +/** + * Sets the highlighted state of the slider. + * + * @param slider The slider to update. + * @param highlighted The new highlighted state. + */ +void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted); + +/** + * Draws the slider at the given screen position: label, then track, + * then the current value as text. Int sliders with fewer than + * UI_SLIDER_STEP_MARKERS_MAX discrete steps render a marker per step + * instead of a plain track. + * + * @param slider The slider to draw. + * @param x Screen x position. + * @param y Screen y position. + * @return Any error that occurs. + */ +errorret_t uiSliderDraw( + const uislider_t *slider, + const float_t x, + const float_t y +); diff --git a/src/dusk/ui/widget/uitab.c b/src/dusk/ui/widget/uitab.c new file mode 100644 index 00000000..12073ef2 --- /dev/null +++ b/src/dusk/ui/widget/uitab.c @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uitab.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/texture/texture.h" +#include "display/shader/shaderunlit.h" + +void uiTabInit(uitab_t *tab, const char_t *label) { + assertNotNull(tab, "Tab cannot be NULL"); + assertNotNull(label, "Label cannot be NULL"); + memoryZero(tab, sizeof(uitab_t)); + tab->label = label; +} + +bool_t uiTabIsActive(const uitab_t *tab) { + assertNotNull(tab, "Tab cannot be NULL"); + return tab->active; +} + +void uiTabSetActive(uitab_t *tab, const bool_t active) { + assertNotNull(tab, "Tab cannot be NULL"); + tab->active = active; +} + +errorret_t uiTabDraw( + const uitab_t *tab, + const float_t x, + const float_t y +) { + assertNotNull(tab, "Tab cannot be NULL"); + + int32_t labelW, labelH; + textMeasure(tab->label, &FONT_DEFAULT, &labelW, &labelH); + + spritebatchsprite_t sprite = { + .min = { x, y, 0.0f }, + .max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t material = { + .unlit = { + .color = tab->active ? COLOR_GREEN : COLOR_RED, + .texture = &TEXTURE_WHITE + } + }; + errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); + errorChain(spriteBatchFlush()); + + errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT)); + + errorOk(); +} diff --git a/src/dusk/ui/widget/uitab.h b/src/dusk/ui/widget/uitab.h new file mode 100644 index 00000000..678f54b6 --- /dev/null +++ b/src/dusk/ui/widget/uitab.h @@ -0,0 +1,53 @@ +/** + * 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 { + const char_t *label; + bool_t active; +} uitab_t; + +/** + * Initializes a tab. + * + * @param tab The tab to initialize. + * @param label Display label. + */ +void uiTabInit(uitab_t *tab, const char_t *label); + +/** + * Returns whether the tab is active. + * + * @param tab The tab to query. + * @returns True if active. + */ +bool_t uiTabIsActive(const uitab_t *tab); + +/** + * Sets the active state of the tab. + * + * @param tab The tab to update. + * @param active The new active state. + */ +void uiTabSetActive(uitab_t *tab, const bool_t active); + +/** + * Draws the tab at the given screen position. Active tabs draw with a + * green background; inactive tabs draw with a red background. + * + * @param tab The tab to draw. + * @param x Screen x position. + * @param y Screen y position. + * @return Any error that occurs. + */ +errorret_t uiTabDraw( + const uitab_t *tab, + const float_t x, + const float_t y +);