Add save slot picker (uiselectsave/uisaveslot), fix focus stack reentrancy
uiselectsave is a Save/Load/Delete slot picker built on save/save.h's SAVE_SLOT_COUNT: a scrolling list of framed uisaveslot_t rows (slot number + file name, or an "Empty Slot" placeholder), navigated via the same uimenu_t/focus-stack convention as every other menu. Load and Delete modes add a trailing action row (switch to delete / cancel) fixed below the scrollable area. Selecting a slot in Delete mode routes through uiConfirmOpen before actually blanking the slot via saveSlotInit + saveDeviceSlotWrite. uiScrollingEnsureVisible in uiscrolling.c is now a real implementation instead of the earlier placeholder. Main menu collapses New Game/Load Game into a single Start Game entry that opens uiselectsave in Load mode; backing out reopens the main menu via uiselectsave's result callback. Also fixes a real reentrancy bug in uiFocusPop: it invoked the popped item's closed callback before decrementing UI_FOCUS.count, so a push triggered from within that callback (e.g. reopening the main menu once select-save reports no result) landed one slot past the true stack top and got silently dropped, stranding the new item outside navigation. Count is now decremented first. Adds a uikeyboard placeholder widget, registered in uielementlist with no behavior yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+22
-5
@@ -26,11 +26,8 @@ msgstr "Continue without saving"
|
|||||||
|
|
||||||
|
|
||||||
# Main Menu Scene
|
# Main Menu Scene
|
||||||
msgid "main_menu.new_game"
|
msgid "main_menu.start_game"
|
||||||
msgstr "New Game"
|
msgstr "Start Game"
|
||||||
|
|
||||||
msgid "main_menu.load_game"
|
|
||||||
msgstr "Load Game"
|
|
||||||
|
|
||||||
msgid "main_menu.options"
|
msgid "main_menu.options"
|
||||||
msgstr "Options"
|
msgstr "Options"
|
||||||
@@ -42,6 +39,26 @@ msgid "main_menu.quit_confirm"
|
|||||||
msgstr "Are you sure you want to quit?"
|
msgstr "Are you sure you want to quit?"
|
||||||
|
|
||||||
|
|
||||||
|
# Select Save Screen
|
||||||
|
msgid "ui.select_save.title"
|
||||||
|
msgstr "Select Save"
|
||||||
|
|
||||||
|
msgid "ui.select_save.empty"
|
||||||
|
msgstr "Empty Slot"
|
||||||
|
|
||||||
|
msgid "ui.select_save.slot_format"
|
||||||
|
msgstr "%s Lv.%d %s"
|
||||||
|
|
||||||
|
msgid "ui.select_save.delete_mode"
|
||||||
|
msgstr "Delete a Save"
|
||||||
|
|
||||||
|
msgid "ui.select_save.delete_confirm"
|
||||||
|
msgstr "Are you sure you want to delete this save?"
|
||||||
|
|
||||||
|
msgid "ui.save_slot.number_format"
|
||||||
|
msgstr "Slot %d"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
#include "yyjson.h"
|
#include "yyjson.h"
|
||||||
|
|
||||||
#define SAVE_SLOT_NAME_LENGTH 8
|
#define SAVE_SLOT_NAME_LENGTH 8
|
||||||
#define SAVE_SLOT_COUNT 3
|
#ifndef SAVE_SLOT_COUNT
|
||||||
|
#define SAVE_SLOT_COUNT 3
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
||||||
|
|||||||
@@ -59,9 +59,14 @@ uifocusitem_t * uiFocusPush(
|
|||||||
void uiFocusPop(void) {
|
void uiFocusPop(void) {
|
||||||
assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
|
assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
|
||||||
|
|
||||||
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1];
|
// Count is decremented before the closed callback fires so that a
|
||||||
if(item->closed != NULL) item->closed(item);
|
// push triggered from within it (e.g. reopening a menu once another
|
||||||
|
// one reports it closed with no result) lands in the now-free slot
|
||||||
|
// instead of one past it, which would silently strand the new item
|
||||||
|
// above the tracked stack top.
|
||||||
UI_FOCUS.count--;
|
UI_FOCUS.count--;
|
||||||
|
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count];
|
||||||
|
if(item->closed != NULL) item->closed(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiFocusPopItem(uifocusitem_t *item) {
|
void uiFocusPopItem(uifocusitem_t *item) {
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ add_subdirectory(game)
|
|||||||
add_subdirectory(mainmenu)
|
add_subdirectory(mainmenu)
|
||||||
add_subdirectory(battle)
|
add_subdirectory(battle)
|
||||||
add_subdirectory(backpack)
|
add_subdirectory(backpack)
|
||||||
|
add_subdirectory(save)
|
||||||
@@ -16,11 +16,12 @@
|
|||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "ui/frame/uiconfirm.h"
|
#include "ui/frame/uiconfirm.h"
|
||||||
|
#include "ui/frame/save/uiselectsave.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
|
||||||
#define UI_MAIN_MENU_INDEX_NEW_GAME 0
|
#define UI_MAIN_MENU_INDEX_START_GAME 0
|
||||||
#define UI_MAIN_MENU_INDEX_LOAD_GAME 1
|
#define UI_MAIN_MENU_INDEX_OPTIONS 1
|
||||||
#define UI_MAIN_MENU_INDEX_OPTIONS 2
|
#define UI_MAIN_MENU_INDEX_QUIT 2
|
||||||
#define UI_MAIN_MENU_INDEX_QUIT 3
|
|
||||||
|
|
||||||
uimainmenu_t UI_MAIN_MENU;
|
uimainmenu_t UI_MAIN_MENU;
|
||||||
|
|
||||||
@@ -28,6 +29,16 @@ void uiMainMenuQuitConfirmed(const bool_t result, void *user) {
|
|||||||
if(result) ENGINE.running = false;
|
if(result) ENGINE.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
|
||||||
|
if(slotIndex == UI_SELECT_SAVE_RESULT_NONE) {
|
||||||
|
uiMainMenuOpen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: load/start the game using the chosen save slot.
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
}
|
||||||
|
|
||||||
void uiMainMenuSelected(
|
void uiMainMenuSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
const uint8_t index,
|
const uint8_t index,
|
||||||
@@ -35,13 +46,11 @@ void uiMainMenuSelected(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
switch(index) {
|
switch(index) {
|
||||||
case UI_MAIN_MENU_INDEX_NEW_GAME:
|
case UI_MAIN_MENU_INDEX_START_GAME:
|
||||||
uiMenuClose(&UI_MAIN_MENU.menu);
|
uiMenuClose(&UI_MAIN_MENU.menu);
|
||||||
testBattleStart();
|
uiSelectSaveOpen(
|
||||||
break;
|
UI_SELECT_SAVE_TYPE_LOAD, uiMainMenuSelectSaveResult, NULL
|
||||||
|
);
|
||||||
case UI_MAIN_MENU_INDEX_LOAD_GAME:
|
|
||||||
// TODO: load game.
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UI_MAIN_MENU_INDEX_OPTIONS:
|
case UI_MAIN_MENU_INDEX_OPTIONS:
|
||||||
@@ -62,16 +71,9 @@ errorret_t uiMainMenuInit(void) {
|
|||||||
|
|
||||||
errorChain(assetLocaleGetString(
|
errorChain(assetLocaleGetString(
|
||||||
&LOCALE.entry->data.locale,
|
&LOCALE.entry->data.locale,
|
||||||
"main_menu.new_game",
|
"main_menu.start_game",
|
||||||
0,
|
0,
|
||||||
UI_MAIN_MENU.newGameLabel,
|
UI_MAIN_MENU.startGameLabel,
|
||||||
UI_MAIN_MENU_LABEL_MAX
|
|
||||||
));
|
|
||||||
errorChain(assetLocaleGetString(
|
|
||||||
&LOCALE.entry->data.locale,
|
|
||||||
"main_menu.load_game",
|
|
||||||
0,
|
|
||||||
UI_MAIN_MENU.loadGameLabel,
|
|
||||||
UI_MAIN_MENU_LABEL_MAX
|
UI_MAIN_MENU_LABEL_MAX
|
||||||
));
|
));
|
||||||
errorChain(assetLocaleGetString(
|
errorChain(assetLocaleGetString(
|
||||||
@@ -92,8 +94,7 @@ errorret_t uiMainMenuInit(void) {
|
|||||||
MENU_BEGIN(
|
MENU_BEGIN(
|
||||||
&UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL
|
&UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL
|
||||||
);
|
);
|
||||||
MENU_BUTTON(UI_MAIN_MENU.newGameLabel);
|
MENU_BUTTON(UI_MAIN_MENU.startGameLabel);
|
||||||
MENU_BUTTON(UI_MAIN_MENU.loadGameLabel);
|
|
||||||
MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
|
MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
|
||||||
MENU_BUTTON(UI_MAIN_MENU.quitLabel);
|
MENU_BUTTON(UI_MAIN_MENU.quitLabel);
|
||||||
MENU_END(UI_MAIN_MENU.items, 1);
|
MENU_END(UI_MAIN_MENU.items, 1);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
|
|
||||||
#define UI_MAIN_MENU_ITEM_COUNT 4
|
#define UI_MAIN_MENU_ITEM_COUNT 3
|
||||||
#define UI_MAIN_MENU_WIDTH 200.0f
|
#define UI_MAIN_MENU_WIDTH 200.0f
|
||||||
#define UI_MAIN_MENU_HEIGHT 160.0f
|
#define UI_MAIN_MENU_HEIGHT 160.0f
|
||||||
#define UI_MAIN_MENU_LABEL_MAX 32
|
#define UI_MAIN_MENU_LABEL_MAX 32
|
||||||
@@ -17,8 +17,7 @@
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
uimenu_t menu;
|
uimenu_t menu;
|
||||||
uimenuitem_t items[UI_MAIN_MENU_ITEM_COUNT];
|
uimenuitem_t items[UI_MAIN_MENU_ITEM_COUNT];
|
||||||
char_t newGameLabel[UI_MAIN_MENU_LABEL_MAX];
|
char_t startGameLabel[UI_MAIN_MENU_LABEL_MAX];
|
||||||
char_t loadGameLabel[UI_MAIN_MENU_LABEL_MAX];
|
|
||||||
char_t optionsLabel[UI_MAIN_MENU_LABEL_MAX];
|
char_t optionsLabel[UI_MAIN_MENU_LABEL_MAX];
|
||||||
char_t quitLabel[UI_MAIN_MENU_LABEL_MAX];
|
char_t quitLabel[UI_MAIN_MENU_LABEL_MAX];
|
||||||
} uimainmenu_t;
|
} uimainmenu_t;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 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
|
||||||
|
uisaveslot.c
|
||||||
|
uiselectsave.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uisaveslot.h"
|
||||||
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
|
void uiSaveSlotInit(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uint8_t slotIndex,
|
||||||
|
const char_t *fileName
|
||||||
|
) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
assertNotNull(fileName, "File name cannot be NULL");
|
||||||
|
|
||||||
|
memoryZero(saveSlot, sizeof(uisaveslot_t));
|
||||||
|
saveSlot->slotIndex = slotIndex;
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&saveSlot->numberLabel, saveSlot->numberText,
|
||||||
|
saveSlot->numberSprites, UI_SAVE_SLOT_NUMBER_SPRITES_MAX
|
||||||
|
);
|
||||||
|
errorret_t result = assetLocaleGetStringWithVA(
|
||||||
|
&LOCALE.entry->data.locale, "ui.save_slot.number_format", 0,
|
||||||
|
saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX,
|
||||||
|
slotIndex + 1
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
saveSlot->numberLabel.dirty = true;
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&saveSlot->nameLabel, saveSlot->nameText,
|
||||||
|
saveSlot->nameSprites, UI_SAVE_SLOT_NAME_SPRITES_MAX
|
||||||
|
);
|
||||||
|
stringCopy(saveSlot->nameText, fileName, UI_SAVE_SLOT_NAME_TEXT_MAX);
|
||||||
|
saveSlot->nameLabel.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
return saveSlot->highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
saveSlot->highlighted = highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
return saveSlot->displayType;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSaveSlotSetDisplayType(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uisaveslotdisplaytype_t displayType
|
||||||
|
) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
saveSlot->displayType = displayType;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSaveSlotDraw(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
const float_t contentX = x + UI_FRAME_START_X;
|
||||||
|
const float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
|
||||||
|
color_t color;
|
||||||
|
if(saveSlot->highlighted) {
|
||||||
|
color = COLOR_RED;
|
||||||
|
} else if(saveSlot->displayType == UI_SAVE_SLOT_DISPLAY_TYPE_DELETE) {
|
||||||
|
color = COLOR_ORANGE;
|
||||||
|
} else {
|
||||||
|
color = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiLabelSetX(&saveSlot->numberLabel, contentX);
|
||||||
|
uiLabelSetY(&saveSlot->numberLabel, contentY);
|
||||||
|
errorChain(uiLabelRender(&saveSlot->numberLabel, color));
|
||||||
|
|
||||||
|
const float_t nameY = contentY + (float_t)saveSlot->numberLabel.height +
|
||||||
|
UI_FRAME_PADDING_Y;
|
||||||
|
uiLabelSetX(&saveSlot->nameLabel, contentX);
|
||||||
|
uiLabelSetY(&saveSlot->nameLabel, nameY);
|
||||||
|
errorChain(uiLabelRender(&saveSlot->nameLabel, color));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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/uilabel.h"
|
||||||
|
|
||||||
|
#define UI_SAVE_SLOT_NUMBER_TEXT_MAX 16
|
||||||
|
#define UI_SAVE_SLOT_NUMBER_SPRITES_MAX UI_SAVE_SLOT_NUMBER_TEXT_MAX
|
||||||
|
#define UI_SAVE_SLOT_NAME_TEXT_MAX 64
|
||||||
|
#define UI_SAVE_SLOT_NAME_SPRITES_MAX UI_SAVE_SLOT_NAME_TEXT_MAX
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a save slot entry should be styled: NORMAL for everyday
|
||||||
|
* save/load browsing, DELETE to visually flag the row as part of a
|
||||||
|
* destructive delete flow.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL,
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE
|
||||||
|
} uisaveslotdisplaytype_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single framed save slot entry: a slot number and a file name. Later
|
||||||
|
* this will grow to show more per-slot save data (level, playtime,
|
||||||
|
* etc).
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t slotIndex;
|
||||||
|
uisaveslotdisplaytype_t displayType;
|
||||||
|
|
||||||
|
uilabel_t numberLabel;
|
||||||
|
char_t numberText[UI_SAVE_SLOT_NUMBER_TEXT_MAX];
|
||||||
|
spritebatchsprite_t numberSprites[UI_SAVE_SLOT_NUMBER_SPRITES_MAX];
|
||||||
|
|
||||||
|
uilabel_t nameLabel;
|
||||||
|
char_t nameText[UI_SAVE_SLOT_NAME_TEXT_MAX];
|
||||||
|
spritebatchsprite_t nameSprites[UI_SAVE_SLOT_NAME_SPRITES_MAX];
|
||||||
|
|
||||||
|
bool_t highlighted;
|
||||||
|
} uisaveslot_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a save slot widget with the given slot index and display
|
||||||
|
* file name.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to initialize.
|
||||||
|
* @param slotIndex The save slot index this widget represents.
|
||||||
|
* @param fileName Display file name; copied internally, safe to be
|
||||||
|
* transient.
|
||||||
|
*/
|
||||||
|
void uiSaveSlotInit(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uint8_t slotIndex,
|
||||||
|
const char_t *fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the save slot widget is highlighted.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to query.
|
||||||
|
* @returns True if highlighted.
|
||||||
|
*/
|
||||||
|
bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the highlighted state of the save slot widget.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to update.
|
||||||
|
* @param highlighted The new highlighted state.
|
||||||
|
*/
|
||||||
|
void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the save slot widget's display type.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to query.
|
||||||
|
* @returns The display type.
|
||||||
|
*/
|
||||||
|
uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the save slot widget's display type.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to update.
|
||||||
|
* @param displayType The new display type.
|
||||||
|
*/
|
||||||
|
void uiSaveSlotSetDisplayType(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uisaveslotdisplaytype_t displayType
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the save slot widget as a framed box at the given position and
|
||||||
|
* size, showing its slot number and file name.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to draw.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Frame width.
|
||||||
|
* @param height Frame height.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSaveSlotDraw(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiselectsave.h"
|
||||||
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "ui/frame/uiconfirm.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
#include "time/timeepoch.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
|
uiselectsave_t UI_SELECT_SAVE;
|
||||||
|
|
||||||
|
void uiSelectSaveFormatSlot(
|
||||||
|
const uint8_t index,
|
||||||
|
char_t *buffer,
|
||||||
|
const size_t bufferSize
|
||||||
|
) {
|
||||||
|
saveslotcache_t *cache = &SAVE.caches[index];
|
||||||
|
|
||||||
|
if(!saveSlotInUse(cache)) {
|
||||||
|
errorret_t result = assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.empty", 0,
|
||||||
|
buffer, bufferSize
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t dateText[32];
|
||||||
|
timeEpochFormat(cache->time, "%Y-%m-%d %H:%M", dateText, sizeof(dateText));
|
||||||
|
|
||||||
|
errorret_t result = assetLocaleGetStringWithVA(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.slot_format", 0,
|
||||||
|
buffer, bufferSize,
|
||||||
|
cache->name, cache->playerLevel, dateText
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveRefreshSlot(const uint8_t index) {
|
||||||
|
char_t fileName[UI_SAVE_SLOT_NAME_TEXT_MAX];
|
||||||
|
uiSelectSaveFormatSlot(index, fileName, sizeof(fileName));
|
||||||
|
uiSaveSlotInit(&UI_SELECT_SAVE.slots[index], index, fileName);
|
||||||
|
|
||||||
|
uisaveslotdisplaytype_t displayType =
|
||||||
|
UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE ?
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
|
||||||
|
uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[index], displayType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) {
|
||||||
|
if(!result) return;
|
||||||
|
|
||||||
|
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||||
|
const uint8_t index = UI_SELECT_SAVE.pendingDeleteIndex;
|
||||||
|
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
errorret_t writeResult = saveDeviceSlotWrite(
|
||||||
|
&SAVE.devices[SAVE.deviceCurrent], &slot, index
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(writeResult)) {
|
||||||
|
errorCatch(errorPrint(writeResult));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVE.caches[index] = slot.cachedData;
|
||||||
|
|
||||||
|
uiSelectSaveRefreshSlot(index);
|
||||||
|
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[index], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveConfirmDelete(const uint8_t index) {
|
||||||
|
UI_SELECT_SAVE.pendingDeleteIndex = index;
|
||||||
|
uiConfirmOpen(
|
||||||
|
"ui.select_save.delete_confirm", uiSelectSaveDeleteConfirmed, NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSetActionLabel(const uiselectsavetype_t type) {
|
||||||
|
const char_t *labelKey = type == UI_SELECT_SAVE_TYPE_LOAD ?
|
||||||
|
"ui.select_save.delete_mode" : "ui.confirm.cancel";
|
||||||
|
|
||||||
|
errorret_t result = assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, labelKey, 0,
|
||||||
|
UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button.label.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSwitchType(const uiselectsavetype_t type) {
|
||||||
|
UI_SELECT_SAVE.type = type;
|
||||||
|
|
||||||
|
uisaveslotdisplaytype_t displayType = type == UI_SELECT_SAVE_TYPE_DELETE ?
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[i], displayType);
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSelectSaveSetActionLabel(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSlotSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
if(UI_SELECT_SAVE.hasAction && index == UI_SELECT_SAVE_ACTION_INDEX) {
|
||||||
|
uiSelectSaveSwitchType(
|
||||||
|
UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_LOAD ?
|
||||||
|
UI_SELECT_SAVE_TYPE_DELETE : UI_SELECT_SAVE_TYPE_LOAD
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE) {
|
||||||
|
uiSelectSaveConfirmDelete(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.result = index;
|
||||||
|
uiSelectSaveClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSlotChanged(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], i == index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The action row sits fixed below the scrollable list, so it never
|
||||||
|
// needs to be scrolled into view.
|
||||||
|
if(index >= SAVE_SLOT_COUNT) return;
|
||||||
|
|
||||||
|
uiScrollingEnsureVisible(
|
||||||
|
&UI_SELECT_SAVE.scroll, index, SAVE_SLOT_COUNT, UI_SELECT_SAVE_VISIBLE_ROWS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSlotClosed(const uimenu_t *menu) {
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_SELECT_SAVE.callback != NULL) {
|
||||||
|
UI_SELECT_SAVE.callback(UI_SELECT_SAVE.result, UI_SELECT_SAVE.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSelectSaveInit(void) {
|
||||||
|
memoryZero(&UI_SELECT_SAVE, sizeof(uiselectsave_t));
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&UI_SELECT_SAVE.titleLabel, UI_SELECT_SAVE.titleText,
|
||||||
|
UI_SELECT_SAVE.titleSprites, UI_SELECT_SAVE_TITLE_SPRITES_MAX
|
||||||
|
);
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.title", 0,
|
||||||
|
UI_SELECT_SAVE.titleText, UI_SELECT_SAVE_TITLE_TEXT_MAX
|
||||||
|
));
|
||||||
|
UI_SELECT_SAVE.titleLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_SELECT_SAVE.titleLabel);
|
||||||
|
|
||||||
|
uiMenuInit(
|
||||||
|
&UI_SELECT_SAVE.slotMenu, uiSelectSaveSlotSelected, uiSelectSaveSlotClosed,
|
||||||
|
uiSelectSaveSlotChanged
|
||||||
|
);
|
||||||
|
uiMenuSetItems(
|
||||||
|
&UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, SAVE_SLOT_COUNT, 1
|
||||||
|
);
|
||||||
|
UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].type =
|
||||||
|
UI_MENU_WIDGET_TYPE_BUTTON;
|
||||||
|
uiButtonInit(
|
||||||
|
&UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
|
||||||
|
UI_SELECT_SAVE.actionText
|
||||||
|
);
|
||||||
|
|
||||||
|
uiScrollingInit(&UI_SELECT_SAVE.scroll);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSelectSaveDraw(void) {
|
||||||
|
if(!uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) errorOk();
|
||||||
|
|
||||||
|
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
const float_t rowFrameHeight =
|
||||||
|
(UI_FRAME_START_Y * 2) + (rowHeight * 2) + UI_FRAME_PADDING_Y;
|
||||||
|
const float_t listHeight =
|
||||||
|
(rowFrameHeight * (float_t)UI_SELECT_SAVE_VISIBLE_ROWS) +
|
||||||
|
(UI_SELECT_SAVE_ROW_GAP * (float_t)(UI_SELECT_SAVE_VISIBLE_ROWS - 1));
|
||||||
|
|
||||||
|
const float_t width = UI_SELECT_SAVE_WIDTH;
|
||||||
|
const float_t height = (UI_FRAME_START_Y * 2)
|
||||||
|
+ (float_t)UI_SELECT_SAVE.titleLabel.height + UI_FRAME_PADDING_Y
|
||||||
|
+ listHeight
|
||||||
|
+ (UI_SELECT_SAVE.hasAction ? UI_FRAME_PADDING_Y + rowHeight : 0.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);
|
||||||
|
|
||||||
|
uiLabelSetX(&UI_SELECT_SAVE.titleLabel, contentX);
|
||||||
|
uiLabelSetY(&UI_SELECT_SAVE.titleLabel, contentY);
|
||||||
|
errorChain(uiLabelRender(&UI_SELECT_SAVE.titleLabel, COLOR_WHITE));
|
||||||
|
|
||||||
|
const float_t listY = contentY + (float_t)UI_SELECT_SAVE.titleLabel.height +
|
||||||
|
UI_FRAME_PADDING_Y;
|
||||||
|
|
||||||
|
const uint8_t offset = UI_SELECT_SAVE.scroll.offset;
|
||||||
|
uint8_t visibleEnd = (uint8_t)(offset + UI_SELECT_SAVE_VISIBLE_ROWS);
|
||||||
|
if(visibleEnd > SAVE_SLOT_COUNT) visibleEnd = SAVE_SLOT_COUNT;
|
||||||
|
|
||||||
|
for(uint8_t i = offset; i < visibleEnd; i++) {
|
||||||
|
const float_t iy =
|
||||||
|
listY + (float_t)(i - offset) * (rowFrameHeight + UI_SELECT_SAVE_ROW_GAP);
|
||||||
|
errorChain(uiSaveSlotDraw(
|
||||||
|
&UI_SELECT_SAVE.slots[i], contentX, iy, contentWidth, rowFrameHeight
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_SELECT_SAVE.hasAction) {
|
||||||
|
const float_t actionY = listY + listHeight + UI_FRAME_PADDING_Y;
|
||||||
|
errorChain(uiButtonDraw(
|
||||||
|
&UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
|
||||||
|
contentX, actionY
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiSelectSaveIsOpen(void) {
|
||||||
|
return uiMenuIsActive(&UI_SELECT_SAVE.slotMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveOpen(
|
||||||
|
const uiselectsavetype_t type,
|
||||||
|
uiselectsaveresultcallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(callback, "Callback cannot be NULL");
|
||||||
|
if(uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) return;
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.type = type;
|
||||||
|
UI_SELECT_SAVE.callback = callback;
|
||||||
|
UI_SELECT_SAVE.user = user;
|
||||||
|
UI_SELECT_SAVE.result = UI_SELECT_SAVE_RESULT_NONE;
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSelectSaveRefreshSlot(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.hasAction =
|
||||||
|
type == UI_SELECT_SAVE_TYPE_LOAD || type == UI_SELECT_SAVE_TYPE_DELETE;
|
||||||
|
uint8_t itemCount = SAVE_SLOT_COUNT;
|
||||||
|
if(UI_SELECT_SAVE.hasAction) {
|
||||||
|
uiSelectSaveSetActionLabel(type);
|
||||||
|
itemCount = UI_SELECT_SAVE_ITEM_CAPACITY;
|
||||||
|
}
|
||||||
|
uiMenuSetItems(&UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, itemCount, 1);
|
||||||
|
|
||||||
|
uiScrollingInit(&UI_SELECT_SAVE.scroll);
|
||||||
|
uiMenuOpen(&UI_SELECT_SAVE.slotMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveClose(void) {
|
||||||
|
uiMenuClose(&UI_SELECT_SAVE.slotMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSelectSaveDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* 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/uilabel.h"
|
||||||
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "ui/widget/uiscrolling.h"
|
||||||
|
#include "ui/frame/save/uisaveslot.h"
|
||||||
|
#include "save/saveslot.h"
|
||||||
|
|
||||||
|
#define UI_SELECT_SAVE_TITLE_TEXT_MAX 64
|
||||||
|
#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
|
||||||
|
#define UI_SELECT_SAVE_ACTION_LABEL_MAX 32
|
||||||
|
#define UI_SELECT_SAVE_VISIBLE_ROWS (SAVE_SLOT_COUNT < 3 ? SAVE_SLOT_COUNT : 3)
|
||||||
|
#define UI_SELECT_SAVE_WIDTH 260.0f
|
||||||
|
#define UI_SELECT_SAVE_ROW_GAP 4.0f
|
||||||
|
#define UI_SELECT_SAVE_RESULT_NONE 0xFF
|
||||||
|
|
||||||
|
// Slot rows plus one trailing action row (switch-to-delete/cancel), only
|
||||||
|
// present for the LOAD and DELETE types - see uiSelectSaveOpen.
|
||||||
|
#define UI_SELECT_SAVE_ITEM_CAPACITY (SAVE_SLOT_COUNT + 1)
|
||||||
|
#define UI_SELECT_SAVE_ACTION_INDEX SAVE_SLOT_COUNT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action the select-save panel is being used for. The caller
|
||||||
|
* chooses this when calling uiSelectSaveOpen.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
UI_SELECT_SAVE_TYPE_SAVE,
|
||||||
|
UI_SELECT_SAVE_TYPE_LOAD,
|
||||||
|
UI_SELECT_SAVE_TYPE_DELETE
|
||||||
|
} uiselectsavetype_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked once the select-save panel closes: slotIndex is the
|
||||||
|
* chosen slot, or UI_SELECT_SAVE_RESULT_NONE if backed out of without
|
||||||
|
* choosing.
|
||||||
|
*
|
||||||
|
* @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE.
|
||||||
|
* @param user Arbitrary pointer passed to uiSelectSaveOpen.
|
||||||
|
*/
|
||||||
|
typedef void (*uiselectsaveresultcallback_t)(
|
||||||
|
const uint8_t slotIndex, void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uilabel_t titleLabel;
|
||||||
|
char_t titleText[UI_SELECT_SAVE_TITLE_TEXT_MAX];
|
||||||
|
spritebatchsprite_t titleSprites[UI_SELECT_SAVE_TITLE_SPRITES_MAX];
|
||||||
|
|
||||||
|
uiselectsavetype_t type;
|
||||||
|
bool_t hasAction;
|
||||||
|
char_t actionText[UI_SELECT_SAVE_ACTION_LABEL_MAX];
|
||||||
|
|
||||||
|
uimenu_t slotMenu;
|
||||||
|
uimenuitem_t slotItems[UI_SELECT_SAVE_ITEM_CAPACITY];
|
||||||
|
uisaveslot_t slots[SAVE_SLOT_COUNT];
|
||||||
|
uiscrolling_t scroll;
|
||||||
|
uint8_t pendingDeleteIndex;
|
||||||
|
|
||||||
|
uiselectsaveresultcallback_t callback;
|
||||||
|
void *user;
|
||||||
|
uint8_t result;
|
||||||
|
} uiselectsave_t;
|
||||||
|
|
||||||
|
extern uiselectsave_t UI_SELECT_SAVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the select-save panel.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSelectSaveInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the select-save panel: a centered frame with a title, a
|
||||||
|
* scrolling list of save/save.h's slots (each shown as a framed
|
||||||
|
* uisaveslot_t, showing at most UI_SELECT_SAVE_VISIBLE_ROWS at a time),
|
||||||
|
* and - when UI_SELECT_SAVE.hasAction is set - the switch-to-delete/
|
||||||
|
* cancel action row fixed below the scrollable list, outside it. No-op
|
||||||
|
* when not open.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSelectSaveDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the select-save panel is currently open.
|
||||||
|
*
|
||||||
|
* @returns True if open.
|
||||||
|
*/
|
||||||
|
bool_t uiSelectSaveIsOpen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the select-save panel for the given action, refreshing each row
|
||||||
|
* from the current SAVE.caches state. callback is invoked exactly once
|
||||||
|
* with the result, whether a slot was chosen or the panel was backed
|
||||||
|
* out of without choosing.
|
||||||
|
*
|
||||||
|
* UI_SELECT_SAVE_TYPE_LOAD adds a trailing row to switch to
|
||||||
|
* UI_SELECT_SAVE_TYPE_DELETE in place, and UI_SELECT_SAVE_TYPE_DELETE
|
||||||
|
* adds a trailing "Cancel" row to switch back to
|
||||||
|
* UI_SELECT_SAVE_TYPE_LOAD - neither closes the panel or invokes
|
||||||
|
* callback, they just toggle UI_SELECT_SAVE.type and the slot rows'
|
||||||
|
* styling. UI_SELECT_SAVE_TYPE_SAVE has no trailing row.
|
||||||
|
*
|
||||||
|
* @param type The action the caller is opening the panel for.
|
||||||
|
* @param callback Called with the result once the panel closes. Cannot
|
||||||
|
* be NULL.
|
||||||
|
* @param user Arbitrary pointer passed through to callback.
|
||||||
|
*/
|
||||||
|
void uiSelectSaveOpen(
|
||||||
|
const uiselectsavetype_t type,
|
||||||
|
uiselectsaveresultcallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the select-save panel. No-op when already closed.
|
||||||
|
*/
|
||||||
|
void uiSelectSaveClose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the select-save panel.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSelectSaveDispose(void);
|
||||||
+20
-10
@@ -21,6 +21,8 @@
|
|||||||
#include "ui/frame/backpack/uibackpack.h"
|
#include "ui/frame/backpack/uibackpack.h"
|
||||||
#include "ui/frame/uiconfirm.h"
|
#include "ui/frame/uiconfirm.h"
|
||||||
#include "ui/widget/uimodal.h"
|
#include "ui/widget/uimodal.h"
|
||||||
|
#include "ui/widget/uikeyboard.h"
|
||||||
|
#include "ui/frame/save/uiselectsave.h"
|
||||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||||
#include "ui/rpg/uiemoji.h"
|
#include "ui/rpg/uiemoji.h"
|
||||||
@@ -85,25 +87,33 @@ uielement_t UI_ELEMENTS[] = {
|
|||||||
// Modals, Popups and Dialogs, above most other UI things.
|
// Modals, Popups and Dialogs, above most other UI things.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
{
|
{
|
||||||
.init = uiConfirmInit,
|
.init = uiSelectSaveInit,
|
||||||
.dispose = uiConfirmDispose
|
.draw = uiSelectSaveDraw,
|
||||||
|
.dispose = uiSelectSaveDispose
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.init = uiTextboxMainInit,
|
||||||
|
.update = uiTextboxMainUpdate,
|
||||||
|
.draw = uiTextboxMainDraw
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.init = uiTextboxMiniListInit,
|
||||||
|
.update = uiTextboxMiniListUpdate,
|
||||||
|
.draw = uiTextboxMiniListDraw
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.init = uiModalInit,
|
.init = uiModalInit,
|
||||||
.draw = uiModalDraw,
|
.draw = uiModalDraw,
|
||||||
.dispose = uiModalDispose
|
.dispose = uiModalDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
.init = uiTextboxMainInit,
|
.init = uiConfirmInit,
|
||||||
.update = uiTextboxMainUpdate,
|
.dispose = uiConfirmDispose
|
||||||
.draw = uiTextboxMainDraw
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
.init = uiTextboxMiniListInit,
|
.init = uiKeyboardInit,
|
||||||
.update = uiTextboxMiniListUpdate,
|
.draw = uiKeyboardDraw,
|
||||||
.draw = uiTextboxMiniListDraw
|
.dispose = uiKeyboardDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
uiscrolling.c
|
uiscrolling.c
|
||||||
uiitem.c
|
uiitem.c
|
||||||
uiitemlist.c
|
uiitemlist.c
|
||||||
|
uikeyboard.c
|
||||||
uimenu.c
|
uimenu.c
|
||||||
uimodal.c
|
uimodal.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uikeyboard.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
uikeyboard_t UI_KEYBOARD;
|
||||||
|
|
||||||
|
errorret_t uiKeyboardInit(void) {
|
||||||
|
memoryZero(&UI_KEYBOARD, sizeof(uikeyboard_t));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiKeyboardDraw(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiKeyboardDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
} uikeyboard_t;
|
||||||
|
|
||||||
|
extern uikeyboard_t UI_KEYBOARD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the on-screen keyboard. Currently a placeholder -- no
|
||||||
|
* behavior implemented yet.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiKeyboardInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the on-screen keyboard. Currently a placeholder -- no-op.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiKeyboardDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the on-screen keyboard.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiKeyboardDispose(void);
|
||||||
@@ -7,9 +7,28 @@
|
|||||||
|
|
||||||
#include "uiscrolling.h"
|
#include "uiscrolling.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
void uiScrollingInit(uiscrolling_t *scrolling) {
|
void uiScrollingInit(uiscrolling_t *scrolling) {
|
||||||
assertNotNull(scrolling, "Scrolling container cannot be NULL");
|
assertNotNull(scrolling, "Scrolling container cannot be NULL");
|
||||||
// Nothing to initialize yet -- uiscrolling_t is currently a
|
memoryZero(scrolling, sizeof(uiscrolling_t));
|
||||||
// placeholder with no fields.
|
}
|
||||||
|
|
||||||
|
void uiScrollingEnsureVisible(
|
||||||
|
uiscrolling_t *scrolling,
|
||||||
|
const uint8_t index,
|
||||||
|
const uint8_t itemCount,
|
||||||
|
const uint8_t visibleCount
|
||||||
|
) {
|
||||||
|
assertNotNull(scrolling, "Scrolling container cannot be NULL");
|
||||||
|
assertTrue(visibleCount > 0, "Visible count must be > 0");
|
||||||
|
|
||||||
|
if(index < scrolling->offset) {
|
||||||
|
scrolling->offset = index;
|
||||||
|
} else if(index >= (uint8_t)(scrolling->offset + visibleCount)) {
|
||||||
|
scrolling->offset = (uint8_t)(index - visibleCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t maxOffset = itemCount > visibleCount ? itemCount - visibleCount : 0;
|
||||||
|
if(scrolling->offset > maxOffset) scrolling->offset = maxOffset;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,29 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
uint8_t offset;
|
||||||
} uiscrolling_t;
|
} uiscrolling_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a scrolling container. Currently a placeholder -- no
|
* Initializes a scrolling container with a zero offset.
|
||||||
* scrolling behavior is implemented yet.
|
|
||||||
*
|
*
|
||||||
* @param scrolling The scrolling container to initialize.
|
* @param scrolling The scrolling container to initialize.
|
||||||
*/
|
*/
|
||||||
void uiScrollingInit(uiscrolling_t *scrolling);
|
void uiScrollingInit(uiscrolling_t *scrolling);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls by the minimum amount needed to bring index within the visible
|
||||||
|
* window of visibleCount rows, then clamps so the window never scrolls
|
||||||
|
* past the end of itemCount rows.
|
||||||
|
*
|
||||||
|
* @param scrolling The scrolling container to update.
|
||||||
|
* @param index Row index that must end up visible.
|
||||||
|
* @param itemCount Total number of rows in the list.
|
||||||
|
* @param visibleCount Number of rows visible at once.
|
||||||
|
*/
|
||||||
|
void uiScrollingEnsureVisible(
|
||||||
|
uiscrolling_t *scrolling,
|
||||||
|
const uint8_t index,
|
||||||
|
const uint8_t itemCount,
|
||||||
|
const uint8_t visibleCount
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user