diff --git a/assets/locale/en_US.po b/assets/locale/en_US.po index 152d257d..56b8c588 100644 --- a/assets/locale/en_US.po +++ b/assets/locale/en_US.po @@ -24,4 +24,11 @@ msgstr "Display" #: src/dusk/ui/frame/settings/uisettings.c msgid "ui.settings.tabs.audio" -msgstr "Audio" \ No newline at end of file +msgstr "Audio" + +msgid "ui.settings.input.deadzone" +msgstr "Deadzone" + +#: src/dusk/ui/frame/uiconfirm.c +msgid "ui.confirm.discard_changes" +msgstr "Discard unsaved changes?" \ No newline at end of file diff --git a/src/dusk/input/input.c b/src/dusk/input/input.c index 060b9962..80a5d83d 100644 --- a/src/dusk/input/input.c +++ b/src/dusk/input/input.c @@ -17,6 +17,7 @@ input_t INPUT; errorret_t inputInit(void) { memoryZero(&INPUT, sizeof(input_t)); + INPUT.deadzone = INPUT_DEADZONE_DEFAULT; for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) { INPUT.actions[i].action = (inputaction_t)i; diff --git a/src/dusk/input/input.h b/src/dusk/input/input.h index b1545979..06093eef 100644 --- a/src/dusk/input/input.h +++ b/src/dusk/input/input.h @@ -12,11 +12,15 @@ #define INPUT_LISTENER_PRESSED_MAX 16 #define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX +#define INPUT_DEADZONE_DEFAULT 0.1f typedef struct { inputactiondata_t actions[INPUT_ACTION_COUNT]; inputplatform_t platform; + + /** User-configured gamepad axis deadzone (0.0f to 1.0f). */ + float_t deadzone; } input_t; extern input_t INPUT; diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index c8d78d60..4d5d7c96 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -29,20 +29,13 @@ void uiGameMenuSelected( 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_BEGIN( + &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL + ); 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); + MENU_END(UI_GAME_MENU.items, 1); errorOk(); } diff --git a/src/dusk/ui/frame/settings/CMakeLists.txt b/src/dusk/ui/frame/settings/CMakeLists.txt index 61e464ab..4a8bf2f6 100644 --- a/src/dusk/ui/frame/settings/CMakeLists.txt +++ b/src/dusk/ui/frame/settings/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uisettings.c + uisettingsdiscard.c uisettingsgameplay.c uisettingsinput.c uisettingsdisplay.c diff --git a/src/dusk/ui/frame/settings/uisettings.c b/src/dusk/ui/frame/settings/uisettings.c index 9e2e9902..a562173d 100644 --- a/src/dusk/ui/frame/settings/uisettings.c +++ b/src/dusk/ui/frame/settings/uisettings.c @@ -19,6 +19,68 @@ #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" +typedef struct { + const char_t *localeId; + errorret_t (*init)(void); + errorret_t (*draw)( + const float_t x, + const float_t y, + const float_t width, + const float_t height + ); + errorret_t (*update)(void); + void (*open)(void); + void (*load)(void); + void (*apply)(void); + bool_t (*hasChanges)(void); +} uisettingstabdef_t; + +static const uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = { + [UI_SETTINGS_TAB_GAMEPLAY] = { + .localeId = "ui.settings.tabs.gameplay", + .init = uiSettingsGameplayInit, + .draw = uiSettingsGameplayDraw, + .update = uiSettingsGameplayUpdate, + .open = uiSettingsGameplayOpen, + .load = uiSettingsGameplayLoad, + .apply = uiSettingsGameplayApply, + .hasChanges = uiSettingsGameplayHasChanges + }, + + [UI_SETTINGS_TAB_INPUT] = { + .localeId = "ui.settings.tabs.input", + .init = uiSettingsInputInit, + .draw = uiSettingsInputDraw, + .update = uiSettingsInputUpdate, + .open = uiSettingsInputOpen, + .load = uiSettingsInputLoad, + .apply = uiSettingsInputApply, + .hasChanges = uiSettingsInputHasChanges + }, + + [UI_SETTINGS_TAB_DISPLAY] = { + .localeId = "ui.settings.tabs.display", + .init = uiSettingsDisplayInit, + .draw = uiSettingsDisplayDraw, + .update = uiSettingsDisplayUpdate, + .open = uiSettingsDisplayOpen, + .load = uiSettingsDisplayLoad, + .apply = uiSettingsDisplayApply, + .hasChanges = uiSettingsDisplayHasChanges + }, + + [UI_SETTINGS_TAB_AUDIO] = { + .localeId = "ui.settings.tabs.audio", + .init = uiSettingsAudioInit, + .draw = uiSettingsAudioDraw, + .update = uiSettingsAudioUpdate, + .open = uiSettingsAudioOpen, + .load = uiSettingsAudioLoad, + .apply = uiSettingsAudioApply, + .hasChanges = uiSettingsAudioHasChanges + } +}; + uisettings_t UI_SETTINGS; void uiSettingsTabSelected( @@ -26,81 +88,44 @@ void uiSettingsTabSelected( const uint8_t index, const uimenuitem_t *item ) { - switch(index) { - case UI_SETTINGS_TAB_GAMEPLAY: - uiSettingsGameplayOpen(); - break; + if(index >= UI_SETTINGS_TAB_COUNT) return; + UI_SETTINGS_TAB_DEFS[index].open(); +} - case UI_SETTINGS_TAB_INPUT: - uiSettingsInputOpen(); - break; - - case UI_SETTINGS_TAB_DISPLAY: - uiSettingsDisplayOpen(); - break; - - case UI_SETTINGS_TAB_AUDIO: - uiSettingsAudioOpen(); - break; - - default: - break; - } +void uiSettingsTabsClosed(const uimenu_t *menu) { + uiSettingsDiscardClosed(&UI_SETTINGS.discard); } errorret_t uiSettingsInit(void) { memoryZero(&UI_SETTINGS, sizeof(uisettings_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, - "ui.settings.tabs.gameplay", - 0, - UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_GAMEPLAY], - UI_SETTINGS_TAB_LABEL_MAX - )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, - "ui.settings.tabs.input", - 0, - UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_INPUT], - UI_SETTINGS_TAB_LABEL_MAX - )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, - "ui.settings.tabs.display", - 0, - UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_DISPLAY], - UI_SETTINGS_TAB_LABEL_MAX - )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, - "ui.settings.tabs.audio", - 0, - UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_AUDIO], - UI_SETTINGS_TAB_LABEL_MAX - )); + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + UI_SETTINGS_TAB_DEFS[i].localeId, + 0, + UI_SETTINGS.tabLabels[i], + UI_SETTINGS_TAB_LABEL_MAX + )); + } - uimenu_t *menu = &UI_SETTINGS.tabsMenu; - uiMenuInit(menu, uiSettingsTabSelected, NULL, NULL); - menu->items = UI_SETTINGS.tabs; + MENU_BEGIN( + &UI_SETTINGS.tabsMenu, UI_SETTINGS.tabs, uiSettingsTabSelected, + uiSettingsTabsClosed, NULL + ); + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + MENU_TAB(UI_SETTINGS.tabLabels[i]); + } + MENU_END(UI_SETTINGS.tabs, menuIndex); - MENU_BEGIN(); - MENU_TAB(UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_GAMEPLAY]); - MENU_TAB(UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_INPUT]); - MENU_TAB(UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_DISPLAY]); - MENU_TAB(UI_SETTINGS.tabLabels[UI_SETTINGS_TAB_AUDIO]); - - assertTrue( - menuIndex <= UI_SETTINGS_TAB_COUNT, - "Settings tab count mismatch" + uiSettingsDiscardInit( + &UI_SETTINGS.discard, &UI_SETTINGS.tabsMenu, uiSettingsHasChanges, + uiSettingsLoad ); - uiMenuSetItems(menu, UI_SETTINGS.tabs, menuIndex, menuIndex); - - errorChain(uiSettingsGameplayInit()); - errorChain(uiSettingsInputInit()); - errorChain(uiSettingsDisplayInit()); - errorChain(uiSettingsAudioInit()); + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + errorChain(UI_SETTINGS_TAB_DEFS[i].init()); + } errorOk(); } @@ -108,8 +133,8 @@ errorret_t uiSettingsInit(void) { 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 width = SCREEN.width; + const float_t height = SCREEN.height; const float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f; const float_t y = (float_t)SCREEN.scanY + @@ -134,25 +159,32 @@ errorret_t uiSettingsDraw(void) { 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)); + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + if(!uiTabIsActive(&UI_SETTINGS.tabs[i].tab)) continue; + errorChain(UI_SETTINGS_TAB_DEFS[i].draw( + contentX, pageY, contentWidth, pageHeight + )); + break; } errorChain(spriteBatchFlush()); errorOk(); } +errorret_t uiSettingsUpdate(void) { + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + errorChain(UI_SETTINGS_TAB_DEFS[i].update()); + } + + return uiSettingsDiscardUpdate(&UI_SETTINGS.discard); +} + bool_t uiSettingsIsOpen(void) { return uiMenuIsActive(&UI_SETTINGS.tabsMenu); } void uiSettingsOpen() { + uiSettingsLoad(); uiMenuOpen(&UI_SETTINGS.tabsMenu); } @@ -163,3 +195,22 @@ void uiSettingsClose() { errorret_t uiSettingsDispose(void) { errorOk(); } + +void uiSettingsLoad(void) { + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + UI_SETTINGS_TAB_DEFS[i].load(); + } +} + +void uiSettingsApply(void) { + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + UI_SETTINGS_TAB_DEFS[i].apply(); + } +} + +bool_t uiSettingsHasChanges(void) { + for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { + if(UI_SETTINGS_TAB_DEFS[i].hasChanges()) return true; + } + return false; +} diff --git a/src/dusk/ui/frame/settings/uisettings.h b/src/dusk/ui/frame/settings/uisettings.h index 97f9158d..e00466ed 100644 --- a/src/dusk/ui/frame/settings/uisettings.h +++ b/src/dusk/ui/frame/settings/uisettings.h @@ -8,6 +8,7 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "uisettingsdiscard.h" #define UI_SETTINGS_TAB_GAMEPLAY 0 #define UI_SETTINGS_TAB_INPUT 1 @@ -20,6 +21,7 @@ typedef struct { uimenu_t tabsMenu; uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT]; char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX]; + uisettingsdiscard_t discard; } uisettings_t; extern uisettings_t UI_SETTINGS; @@ -38,6 +40,14 @@ errorret_t uiSettingsInit(void); */ errorret_t uiSettingsDraw(void); +/** + * Updates the settings panel. Shows a discard-changes confirmation if + * the panel was just closed with unsaved changes. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsUpdate(void); + /** * Returns true when the settings panel is currently open. * @@ -61,3 +71,23 @@ void uiSettingsClose(); * @return Any error that occurs. */ errorret_t uiSettingsDispose(void); + +/** + * Loads every category page's widgets from the current engine settings. + * Called automatically by uiSettingsOpen. + */ +void uiSettingsLoad(void); + +/** + * Applies every category page's widget values to the engine settings. + * Called automatically when a page's own Apply button is pressed. + */ +void uiSettingsApply(void); + +/** + * Returns whether any category page has a widget value that differs + * from what was loaded. + * + * @returns True if there are unsaved changes. + */ +bool_t uiSettingsHasChanges(void); diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.c b/src/dusk/ui/frame/settings/uisettingsaudio.c index 4a870d4a..92a864e3 100644 --- a/src/dusk/ui/frame/settings/uisettingsaudio.c +++ b/src/dusk/ui/frame/settings/uisettingsaudio.c @@ -6,21 +6,40 @@ */ #include "uisettingsaudio.h" +#include "assert/assert.h" #include "util/memory.h" uisettingsaudio_t UI_SETTINGS_AUDIO; +void uiSettingsAudioSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + if(index != UI_SETTINGS_AUDIO_INDEX_APPLY) return; + uiSettingsAudioApply(); +} + +void uiSettingsAudioClosed(const uimenu_t *menu) { + uiSettingsDiscardClosed(&UI_SETTINGS_AUDIO.discard); +} + 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_BEGIN( + &UI_SETTINGS_AUDIO.menu, UI_SETTINGS_AUDIO.items, + uiSettingsAudioSelected, uiSettingsAudioClosed, NULL + ); MENU_LABEL("No audio settings yet"); + MENU_BUTTON("Apply"); - uiMenuSetItems(menu, UI_SETTINGS_AUDIO.items, menuIndex, 1); + MENU_END(UI_SETTINGS_AUDIO.items, 1); + + uiSettingsDiscardInit( + &UI_SETTINGS_AUDIO.discard, &UI_SETTINGS_AUDIO.menu, + uiSettingsAudioHasChanges, uiSettingsAudioLoad + ); errorOk(); } @@ -45,3 +64,17 @@ void uiSettingsAudioOpen(void) { void uiSettingsAudioClose(void) { uiMenuClose(&UI_SETTINGS_AUDIO.menu); } + +errorret_t uiSettingsAudioUpdate(void) { + return uiSettingsDiscardUpdate(&UI_SETTINGS_AUDIO.discard); +} + +void uiSettingsAudioLoad(void) { +} + +void uiSettingsAudioApply(void) { +} + +bool_t uiSettingsAudioHasChanges(void) { + return false; +} diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.h b/src/dusk/ui/frame/settings/uisettingsaudio.h index bad154e0..17594d9d 100644 --- a/src/dusk/ui/frame/settings/uisettingsaudio.h +++ b/src/dusk/ui/frame/settings/uisettingsaudio.h @@ -8,12 +8,15 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "uisettingsdiscard.h" -#define UI_SETTINGS_AUDIO_ITEM_COUNT 1 +#define UI_SETTINGS_AUDIO_ITEM_COUNT 2 +#define UI_SETTINGS_AUDIO_INDEX_APPLY 1 typedef struct { uimenu_t menu; uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT]; + uisettingsdiscard_t discard; } uisettingsaudio_t; extern uisettingsaudio_t UI_SETTINGS_AUDIO; @@ -41,6 +44,14 @@ errorret_t uiSettingsAudioDraw( const float_t height ); +/** + * Updates the audio settings page. Shows a discard-changes + * confirmation if the page was just backed out of with unsaved changes. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsAudioUpdate(void); + /** * Returns true when the audio settings page is currently open. * @@ -57,3 +68,20 @@ void uiSettingsAudioOpen(void); * Closes the audio settings page. No-op when already closed. */ void uiSettingsAudioClose(void); + +/** + * Loads the page's widgets from the current engine audio settings. + */ +void uiSettingsAudioLoad(void); + +/** + * Applies the page's widget values to the engine audio settings. + */ +void uiSettingsAudioApply(void); + +/** + * Returns whether any widget's value differs from what was loaded. + * + * @returns True if there are unsaved changes. + */ +bool_t uiSettingsAudioHasChanges(void); diff --git a/src/dusk/ui/frame/settings/uisettingsdiscard.c b/src/dusk/ui/frame/settings/uisettingsdiscard.c new file mode 100644 index 00000000..2f89a96e --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsdiscard.c @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uisettingsdiscard.h" +#include "ui/frame/uiconfirm.h" +#include "locale/localemanager.h" +#include "asset/loader/locale/assetlocaleloader.h" + +void uiSettingsDiscardInit( + uisettingsdiscard_t *discard, + uimenu_t *menu, + uisettingshaschangesfn_t hasChanges, + uisettingsloadfn_t load +) { + discard->menu = menu; + discard->hasChanges = hasChanges; + discard->load = load; + discard->pending = false; +} + +void uiSettingsDiscardClosed(uisettingsdiscard_t *discard) { + if(discard->hasChanges()) discard->pending = true; +} + +errorret_t uiSettingsDiscardUpdate(uisettingsdiscard_t *discard) { + if(!discard->pending) errorOk(); + discard->pending = false; + + char_t text[UI_SETTINGS_DISCARD_TEXT_MAX]; + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + "ui.confirm.discard_changes", + 0, + text, + UI_SETTINGS_DISCARD_TEXT_MAX + )); + + uiConfirmOpen(text, uiSettingsDiscardConfirmResult, discard); + + errorOk(); +} + +void uiSettingsDiscardConfirmResult(const bool_t result, void *user) { + uisettingsdiscard_t *discard = (uisettingsdiscard_t *)user; + + if(!result) { + // Cancel means "keep editing" - reopen without reloading so the + // unsaved widget values the user had are preserved. + uiMenuOpen(discard->menu); + return; + } + + // Confirmed - reload the widgets back to the actual engine values. + discard->load(); +} diff --git a/src/dusk/ui/frame/settings/uisettingsdiscard.h b/src/dusk/ui/frame/settings/uisettingsdiscard.h new file mode 100644 index 00000000..ebc1d1e6 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsdiscard.h @@ -0,0 +1,77 @@ +/** + * 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_DISCARD_TEXT_MAX 128 + +/** + * Returns true if the owning settings page/panel has unsaved changes. + * + * @returns True if there are unsaved changes. + */ +typedef bool_t (*uisettingshaschangesfn_t)(void); + +/** + * Reloads the owning settings page/panel's widgets from the current + * engine settings. + */ +typedef void (*uisettingsloadfn_t)(void); + +typedef struct { + uimenu_t *menu; + uisettingshaschangesfn_t hasChanges; + uisettingsloadfn_t load; + bool_t pending; +} uisettingsdiscard_t; + +/** + * Initializes a discard-changes confirmation helper for a settings + * page or panel. + * + * @param discard The helper state to initialize. + * @param menu The menu to reopen if the user cancels discarding. + * @param hasChanges Callback returning true if there are unsaved + * changes. + * @param load Callback that reloads the widgets from the current + * engine settings, called if the user confirms discarding changes. + */ +void uiSettingsDiscardInit( + uisettingsdiscard_t *discard, + uimenu_t *menu, + uisettingshaschangesfn_t hasChanges, + uisettingsloadfn_t load +); + +/** + * Call from the owning menu's closed callback. Flags a pending + * confirmation if hasChanges() reports unsaved changes. + * + * @param discard The helper state. + */ +void uiSettingsDiscardClosed(uisettingsdiscard_t *discard); + +/** + * Call every frame from the owning page/panel's update function. Shows + * the discard-changes confirmation dialog if one is pending. + * + * @param discard The helper state. + * @return Any error that occurs. + */ +errorret_t uiSettingsDiscardUpdate(uisettingsdiscard_t *discard); + +/** + * Callback passed to uiConfirmOpen by uiSettingsDiscardUpdate. Reopens + * the menu if the user chose to keep editing. + * + * @param result True if the user confirmed discarding changes, false + * to keep editing. On true, the widgets are reloaded via load(). + * @param user The uisettingsdiscard_t* this confirmation belongs to. + */ +void uiSettingsDiscardConfirmResult(const bool_t result, void *user); diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.c b/src/dusk/ui/frame/settings/uisettingsdisplay.c index 7f26c74a..1dd110cb 100644 --- a/src/dusk/ui/frame/settings/uisettingsdisplay.c +++ b/src/dusk/ui/frame/settings/uisettingsdisplay.c @@ -6,21 +6,40 @@ */ #include "uisettingsdisplay.h" +#include "assert/assert.h" #include "util/memory.h" uisettingsdisplay_t UI_SETTINGS_DISPLAY; +void uiSettingsDisplaySelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + if(index != UI_SETTINGS_DISPLAY_INDEX_APPLY) return; + uiSettingsDisplayApply(); +} + +void uiSettingsDisplayClosed(const uimenu_t *menu) { + uiSettingsDiscardClosed(&UI_SETTINGS_DISPLAY.discard); +} + 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_BEGIN( + &UI_SETTINGS_DISPLAY.menu, UI_SETTINGS_DISPLAY.items, + uiSettingsDisplaySelected, uiSettingsDisplayClosed, NULL + ); MENU_LABEL("No display settings yet"); + MENU_BUTTON("Apply"); - uiMenuSetItems(menu, UI_SETTINGS_DISPLAY.items, menuIndex, 1); + MENU_END(UI_SETTINGS_DISPLAY.items, 1); + + uiSettingsDiscardInit( + &UI_SETTINGS_DISPLAY.discard, &UI_SETTINGS_DISPLAY.menu, + uiSettingsDisplayHasChanges, uiSettingsDisplayLoad + ); errorOk(); } @@ -45,3 +64,17 @@ void uiSettingsDisplayOpen(void) { void uiSettingsDisplayClose(void) { uiMenuClose(&UI_SETTINGS_DISPLAY.menu); } + +errorret_t uiSettingsDisplayUpdate(void) { + return uiSettingsDiscardUpdate(&UI_SETTINGS_DISPLAY.discard); +} + +void uiSettingsDisplayLoad(void) { +} + +void uiSettingsDisplayApply(void) { +} + +bool_t uiSettingsDisplayHasChanges(void) { + return false; +} diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.h b/src/dusk/ui/frame/settings/uisettingsdisplay.h index d8f913f4..cda4d0f9 100644 --- a/src/dusk/ui/frame/settings/uisettingsdisplay.h +++ b/src/dusk/ui/frame/settings/uisettingsdisplay.h @@ -8,12 +8,15 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "uisettingsdiscard.h" -#define UI_SETTINGS_DISPLAY_ITEM_COUNT 1 +#define UI_SETTINGS_DISPLAY_ITEM_COUNT 2 +#define UI_SETTINGS_DISPLAY_INDEX_APPLY 1 typedef struct { uimenu_t menu; uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT]; + uisettingsdiscard_t discard; } uisettingsdisplay_t; extern uisettingsdisplay_t UI_SETTINGS_DISPLAY; @@ -41,6 +44,14 @@ errorret_t uiSettingsDisplayDraw( const float_t height ); +/** + * Updates the display settings page. Shows a discard-changes + * confirmation if the page was just backed out of with unsaved changes. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsDisplayUpdate(void); + /** * Returns true when the display settings page is currently open. * @@ -57,3 +68,20 @@ void uiSettingsDisplayOpen(void); * Closes the display settings page. No-op when already closed. */ void uiSettingsDisplayClose(void); + +/** + * Loads the page's widgets from the current engine display settings. + */ +void uiSettingsDisplayLoad(void); + +/** + * Applies the page's widget values to the engine display settings. + */ +void uiSettingsDisplayApply(void); + +/** + * Returns whether any widget's value differs from what was loaded. + * + * @returns True if there are unsaved changes. + */ +bool_t uiSettingsDisplayHasChanges(void); diff --git a/src/dusk/ui/frame/settings/uisettingsgameplay.c b/src/dusk/ui/frame/settings/uisettingsgameplay.c index f5c1255f..ce07e5df 100644 --- a/src/dusk/ui/frame/settings/uisettingsgameplay.c +++ b/src/dusk/ui/frame/settings/uisettingsgameplay.c @@ -22,33 +22,44 @@ void uiSettingsGameplaySelected( const uint8_t index, const uimenuitem_t *item ) { - if(index != UI_SETTINGS_GAMEPLAY_INDEX_SHOW_CONFIRM) return; - uiConfirmOpen( - "Are you sure?", uiSettingsGameplayShowConfirmResult, NULL - ); + if(index == UI_SETTINGS_GAMEPLAY_INDEX_SHOW_CONFIRM) { + uiConfirmOpen( + "Are you sure?", uiSettingsGameplayShowConfirmResult, NULL + ); + return; + } + + if(index == UI_SETTINGS_GAMEPLAY_INDEX_APPLY) { + uiSettingsGameplayApply(); + return; + } +} + +void uiSettingsGameplayClosed(const uimenu_t *menu) { + uiSettingsDiscardClosed(&UI_SETTINGS_GAMEPLAY.discard); } errorret_t uiSettingsGameplayInit(void) { memoryZero(&UI_SETTINGS_GAMEPLAY, sizeof(uisettingsgameplay_t)); - uimenu_t *menu = &UI_SETTINGS_GAMEPLAY.menu; - uiMenuInit(menu, uiSettingsGameplaySelected, NULL, NULL); - menu->items = UI_SETTINGS_GAMEPLAY.items; - - MENU_BEGIN(); + MENU_BEGIN( + &UI_SETTINGS_GAMEPLAY.menu, UI_SETTINGS_GAMEPLAY.items, + uiSettingsGameplaySelected, uiSettingsGameplayClosed, NULL + ); 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); MENU_BUTTON("Show confirm"); + MENU_BUTTON("Apply"); - assertTrue( - menuIndex <= UI_SETTINGS_GAMEPLAY_ITEM_COUNT, - "Gameplay settings item count mismatch" + MENU_END(UI_SETTINGS_GAMEPLAY.items, 1); + + uiSettingsDiscardInit( + &UI_SETTINGS_GAMEPLAY.discard, &UI_SETTINGS_GAMEPLAY.menu, + uiSettingsGameplayHasChanges, uiSettingsGameplayLoad ); - uiMenuSetItems(menu, UI_SETTINGS_GAMEPLAY.items, menuIndex, 1); - errorOk(); } @@ -72,3 +83,41 @@ void uiSettingsGameplayOpen(void) { void uiSettingsGameplayClose(void) { uiMenuClose(&UI_SETTINGS_GAMEPLAY.menu); } + +errorret_t uiSettingsGameplayUpdate(void) { + return uiSettingsDiscardUpdate(&UI_SETTINGS_GAMEPLAY.discard); +} + +void uiSettingsGameplayLoad(void) { + UI_SETTINGS_GAMEPLAY.loadedEnableThing = uiCheckboxIsChecked( + &UI_SETTINGS_GAMEPLAY.items[UI_SETTINGS_GAMEPLAY_INDEX_ENABLE_THING].checkbox + ); + UI_SETTINGS_GAMEPLAY.loadedTestValue = uiSliderGetFloat( + &UI_SETTINGS_GAMEPLAY.items[UI_SETTINGS_GAMEPLAY_INDEX_TEST_SLIDER].slider + ); +} + +void uiSettingsGameplayApply(void) { + UI_SETTINGS_GAMEPLAY.loadedEnableThing = uiCheckboxIsChecked( + &UI_SETTINGS_GAMEPLAY.items[UI_SETTINGS_GAMEPLAY_INDEX_ENABLE_THING].checkbox + ); + UI_SETTINGS_GAMEPLAY.loadedTestValue = uiSliderGetFloat( + &UI_SETTINGS_GAMEPLAY.items[UI_SETTINGS_GAMEPLAY_INDEX_TEST_SLIDER].slider + ); +} + +bool_t uiSettingsGameplayHasChanges(void) { + if( + uiCheckboxIsChecked( + &UI_SETTINGS_GAMEPLAY.items[UI_SETTINGS_GAMEPLAY_INDEX_ENABLE_THING].checkbox + ) != UI_SETTINGS_GAMEPLAY.loadedEnableThing + ) return true; + + if( + uiSliderGetFloat( + &UI_SETTINGS_GAMEPLAY.items[UI_SETTINGS_GAMEPLAY_INDEX_TEST_SLIDER].slider + ) != UI_SETTINGS_GAMEPLAY.loadedTestValue + ) return true; + + return false; +} diff --git a/src/dusk/ui/frame/settings/uisettingsgameplay.h b/src/dusk/ui/frame/settings/uisettingsgameplay.h index 8d7af472..4f5f4282 100644 --- a/src/dusk/ui/frame/settings/uisettingsgameplay.h +++ b/src/dusk/ui/frame/settings/uisettingsgameplay.h @@ -8,13 +8,20 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "uisettingsdiscard.h" -#define UI_SETTINGS_GAMEPLAY_ITEM_COUNT 5 +#define UI_SETTINGS_GAMEPLAY_ITEM_COUNT 6 +#define UI_SETTINGS_GAMEPLAY_INDEX_ENABLE_THING 2 +#define UI_SETTINGS_GAMEPLAY_INDEX_TEST_SLIDER 3 #define UI_SETTINGS_GAMEPLAY_INDEX_SHOW_CONFIRM 4 +#define UI_SETTINGS_GAMEPLAY_INDEX_APPLY 5 typedef struct { uimenu_t menu; uimenuitem_t items[UI_SETTINGS_GAMEPLAY_ITEM_COUNT]; + uisettingsdiscard_t discard; + bool_t loadedEnableThing; + float_t loadedTestValue; } uisettingsgameplay_t; extern uisettingsgameplay_t UI_SETTINGS_GAMEPLAY; @@ -42,6 +49,14 @@ errorret_t uiSettingsGameplayDraw( const float_t height ); +/** + * Updates the gameplay settings page. Shows a discard-changes + * confirmation if the page was just backed out of with unsaved changes. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsGameplayUpdate(void); + /** * Returns true when the gameplay settings page is currently open. * @@ -58,3 +73,20 @@ void uiSettingsGameplayOpen(void); * Closes the gameplay settings page. No-op when already closed. */ void uiSettingsGameplayClose(void); + +/** + * Loads the page's widgets from the current engine gameplay settings. + */ +void uiSettingsGameplayLoad(void); + +/** + * Applies the page's widget values to the engine gameplay settings. + */ +void uiSettingsGameplayApply(void); + +/** + * Returns whether any widget's value differs from what was loaded. + * + * @returns True if there are unsaved changes. + */ +bool_t uiSettingsGameplayHasChanges(void); diff --git a/src/dusk/ui/frame/settings/uisettingsinput.c b/src/dusk/ui/frame/settings/uisettingsinput.c index a128e7c0..8657890d 100644 --- a/src/dusk/ui/frame/settings/uisettingsinput.c +++ b/src/dusk/ui/frame/settings/uisettingsinput.c @@ -6,21 +6,55 @@ */ #include "uisettingsinput.h" +#include "assert/assert.h" #include "util/memory.h" +#include "locale/localemanager.h" +#include "asset/loader/locale/assetlocaleloader.h" +#include "input/input.h" uisettingsinput_t UI_SETTINGS_INPUT; +void uiSettingsInputSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + if(index != UI_SETTINGS_INPUT_INDEX_APPLY) return; + uiSettingsInputApply(); +} + +void uiSettingsInputClosed(const uimenu_t *menu) { + uiSettingsDiscardClosed(&UI_SETTINGS_INPUT.discard); +} + 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( + &UI_SETTINGS_INPUT.menu, UI_SETTINGS_INPUT.items, + uiSettingsInputSelected, uiSettingsInputClosed, NULL + ); + #ifdef DUSK_INPUT_GAMEPAD + errorChain(assetLocaleGetString( + &LOCALE.entry->data.locale, + "ui.settings.input.deadzone", + 0, + UI_SETTINGS_INPUT.deadzoneLabel, + UI_SETTINGS_INPUT_LABEL_MAX + )); + MENU_SLIDER_FLOAT( + UI_SETTINGS_INPUT.deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f + ); + #else + MENU_LABEL("No input settings yet"); + #endif + MENU_BUTTON("Apply"); + MENU_END(UI_SETTINGS_INPUT.items, 1); - MENU_BEGIN(); - MENU_LABEL("No input settings yet"); - - uiMenuSetItems(menu, UI_SETTINGS_INPUT.items, menuIndex, 1); + uiSettingsDiscardInit( + &UI_SETTINGS_INPUT.discard, &UI_SETTINGS_INPUT.menu, + uiSettingsInputHasChanges, uiSettingsInputLoad + ); errorOk(); } @@ -45,3 +79,33 @@ void uiSettingsInputOpen(void) { void uiSettingsInputClose(void) { uiMenuClose(&UI_SETTINGS_INPUT.menu); } + +errorret_t uiSettingsInputUpdate(void) { + return uiSettingsDiscardUpdate(&UI_SETTINGS_INPUT.discard); +} + +void uiSettingsInputLoad(void) { + #ifdef DUSK_INPUT_GAMEPAD + uiSliderSetFloat( + &UI_SETTINGS_INPUT.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider, + INPUT.deadzone + ); + #endif +} + +void uiSettingsInputApply(void) { + #ifdef DUSK_INPUT_GAMEPAD + INPUT.deadzone = uiSliderGetFloat( + &UI_SETTINGS_INPUT.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider + ); + #endif +} + +bool_t uiSettingsInputHasChanges(void) { + #ifdef DUSK_INPUT_GAMEPAD + if(uiSliderGetFloat( + &UI_SETTINGS_INPUT.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider + ) != INPUT.deadzone) return true; + #endif + return false; +} diff --git a/src/dusk/ui/frame/settings/uisettingsinput.h b/src/dusk/ui/frame/settings/uisettingsinput.h index 2976fb78..c27b198d 100644 --- a/src/dusk/ui/frame/settings/uisettingsinput.h +++ b/src/dusk/ui/frame/settings/uisettingsinput.h @@ -8,12 +8,18 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "uisettingsdiscard.h" -#define UI_SETTINGS_INPUT_ITEM_COUNT 1 +#define UI_SETTINGS_INPUT_ITEM_COUNT 2 +#define UI_SETTINGS_INPUT_LABEL_MAX 32 +#define UI_SETTINGS_INPUT_INDEX_DEADZONE 0 +#define UI_SETTINGS_INPUT_INDEX_APPLY 1 typedef struct { uimenu_t menu; uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT]; + uisettingsdiscard_t discard; + char_t deadzoneLabel[UI_SETTINGS_INPUT_LABEL_MAX]; } uisettingsinput_t; extern uisettingsinput_t UI_SETTINGS_INPUT; @@ -41,6 +47,14 @@ errorret_t uiSettingsInputDraw( const float_t height ); +/** + * Updates the input settings page. Shows a discard-changes confirmation + * if the page was just backed out of with unsaved changes. + * + * @return Any error that occurs. + */ +errorret_t uiSettingsInputUpdate(void); + /** * Returns true when the input settings page is currently open. * @@ -57,3 +71,20 @@ void uiSettingsInputOpen(void); * Closes the input settings page. No-op when already closed. */ void uiSettingsInputClose(void); + +/** + * Loads the page's widgets from the current engine input settings. + */ +void uiSettingsInputLoad(void); + +/** + * Applies the page's widget values to the engine input settings. + */ +void uiSettingsInputApply(void); + +/** + * Returns whether any widget's value differs from what was loaded. + * + * @returns True if there are unsaved changes. + */ +bool_t uiSettingsInputHasChanges(void); diff --git a/src/dusk/ui/frame/uiconfirm.c b/src/dusk/ui/frame/uiconfirm.c index 863ba9dd..2fded7ce 100644 --- a/src/dusk/ui/frame/uiconfirm.c +++ b/src/dusk/ui/frame/uiconfirm.c @@ -40,20 +40,13 @@ void uiConfirmClosed(const uimenu_t *menu) { errorret_t uiConfirmInit(void) { memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t)); - uimenu_t *menu = &UI_CONFIRM.menu; - uiMenuInit(menu, uiConfirmSelected, uiConfirmClosed, NULL); - menu->items = UI_CONFIRM.items; - - MENU_BEGIN(); + MENU_BEGIN( + &UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL + ); MENU_BUTTON("Confirm"); MENU_BUTTON("Cancel"); - assertTrue( - menuIndex <= UI_CONFIRM_ITEM_COUNT, - "Confirm item count mismatch" - ); - - uiMenuSetItems(menu, UI_CONFIRM.items, menuIndex, menuIndex); + MENU_END(UI_CONFIRM.items, menuIndex); errorOk(); } diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index b75d9a7d..7ddf2cba 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -43,6 +43,7 @@ uielement_t UI_ELEMENTS[] = { { .init = uiSettingsInit, + .update = uiSettingsUpdate, .draw = uiSettingsDraw, .dispose = uiSettingsDispose }, diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h index a6478db0..3c653dce 100644 --- a/src/dusk/ui/widget/uimenu.h +++ b/src/dusk/ui/widget/uimenu.h @@ -212,33 +212,44 @@ bool_t uiMenuFocusDirection( ); // Helper macros -#define MENU_BEGIN() uint8_t menuIndex = 0 +#define MENU_BEGIN(menuPtr, itemsArray, selected, closed, changed) \ + uimenu_t *menu = (menuPtr); \ + uiMenuInit(menu, selected, closed, changed); \ + menu->items = (itemsArray); \ + uint8_t menuIndex = 0; \ + uint8_t menuCapacity = sizeof(itemsArray) / sizeof((itemsArray)[0]) #define MENU_LABEL(text) \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \ menu->items[menuIndex].label = text; \ ++menuIndex #define MENU_SPACER() \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SPACER; \ ++menuIndex #define MENU_CHECKBOX(label) \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_CHECKBOX; \ uiCheckboxInit(&menu->items[menuIndex].checkbox, label); \ ++menuIndex #define MENU_BUTTON(label) \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_BUTTON; \ uiButtonInit(&menu->items[menuIndex].button, label); \ ++menuIndex #define MENU_TAB(label) \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ 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) \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \ uiSliderInitFloat( \ &menu->items[menuIndex].slider, label, value, min, max, step \ @@ -246,10 +257,14 @@ bool_t uiMenuFocusDirection( ++menuIndex #define MENU_SLIDER_INT(label, value, min, max, step) \ + assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \ uiSliderInitInt( \ &menu->items[menuIndex].slider, label, value, min, max, step \ ); \ ++menuIndex +#define MENU_END(itemsArray, columns) \ + uiMenuSetItems(menu, (itemsArray), menuIndex, (columns)) + //EOF