mGUI: Refactor menu code into separate functions

This commit is contained in:
Vicki Pfau 2021-11-29 23:11:17 -08:00
parent c9e1b78426
commit 603de394d8
2 changed files with 242 additions and 151 deletions

View File

@ -10,6 +10,7 @@
CXX_GUARD_START
#include <mgba-util/gui.h>
#include <mgba-util/vector.h>
#define GUI_V_V (struct GUIVariant) { .type = GUI_VARIANT_VOID }
@ -73,10 +74,29 @@ struct GUIMenu {
struct GUIBackground* background;
};
struct GUIMenuSavedState {
struct GUIMenu* menu;
size_t start;
};
DECLARE_VECTOR(GUIMenuSavedList, struct GUIMenuSavedState);
struct GUIMenuState {
size_t start;
int cursorOverItem;
enum GUICursorState cursor;
unsigned cx, cy;
struct GUIMenuSavedList stack;
struct GUIMenuItem* resultItem;
};
enum GUIMenuExitReason {
GUI_MENU_CONTINUE = 0,
GUI_MENU_EXIT_ACCEPT,
GUI_MENU_EXIT_BACK,
GUI_MENU_EXIT_CANCEL,
GUI_MENU_ENTER,
};
enum GUIMessageBoxButtons {
@ -85,7 +105,11 @@ enum GUIMessageBoxButtons {
};
struct GUIParams;
void GUIMenuStateInit(struct GUIMenuState*);
void GUIMenuStateDeinit(struct GUIMenuState*);
enum GUIMenuExitReason GUIShowMenu(struct GUIParams* params, struct GUIMenu* menu, struct GUIMenuItem** item);
enum GUIMenuExitReason GUIMenuRun(struct GUIParams* params, struct GUIMenu* menu, struct GUIMenuState* state);
ATTRIBUTE_FORMAT(printf, 4, 5)
enum GUIMenuExitReason GUIShowMessageBox(struct GUIParams* params, int buttons, int frames, const char* format, ...);

View File

@ -15,6 +15,7 @@
#endif
DEFINE_VECTOR(GUIMenuItemList, struct GUIMenuItem);
DEFINE_VECTOR(GUIMenuSavedList, struct GUIMenuSavedState);
void _itemNext(struct GUIMenuItem* item, bool wrap) {
if (item->state < item->nStates - 1) {
@ -44,17 +45,21 @@ void _itemPrev(struct GUIMenuItem* item, bool wrap) {
}
}
enum GUIMenuExitReason GUIShowMenu(struct GUIParams* params, struct GUIMenu* menu, struct GUIMenuItem** item) {
size_t start = 0;
size_t lineHeight = GUIFontHeight(params->font);
size_t pageSize = params->height / lineHeight;
if (pageSize > 4) {
pageSize -= 4;
} else {
pageSize = 1;
}
int cursorOverItem = 0;
void GUIMenuStateInit(struct GUIMenuState* state) {
state->start = 0;
state->cursorOverItem = 0;
state->cursor = GUI_CURSOR_NOT_PRESENT;
state->resultItem = NULL;
GUIMenuSavedListInit(&state->stack, 0);
}
void GUIMenuStateDeinit(struct GUIMenuState* state) {
GUIMenuSavedListDeinit(&state->stack);
}
enum GUIMenuExitReason GUIShowMenu(struct GUIParams* params, struct GUIMenu* menu, struct GUIMenuItem** item) {
struct GUIMenuState state;
GUIMenuStateInit(&state);
GUIInvalidateKeys(params);
while (true) {
#ifdef _3DS
@ -66,151 +71,213 @@ enum GUIMenuExitReason GUIShowMenu(struct GUIParams* params, struct GUIMenu* men
return GUI_MENU_EXIT_CANCEL;
}
#endif
uint32_t newInput = 0;
GUIPollInput(params, &newInput, 0);
unsigned cx, cy;
enum GUICursorState cursor = GUIPollCursor(params, &cx, &cy);
if (newInput & (1 << GUI_INPUT_UP) && menu->index > 0) {
--menu->index;
}
if (newInput & (1 << GUI_INPUT_DOWN) && menu->index < GUIMenuItemListSize(&menu->items) - 1) {
++menu->index;
}
if (newInput & (1 << GUI_INPUT_LEFT)) {
struct GUIMenuItem* item = GUIMenuItemListGetPointer(&menu->items, menu->index);
if (item->validStates) {
_itemPrev(item, false);
} else if (menu->index >= pageSize) {
menu->index -= pageSize;
} else {
menu->index = 0;
}
}
if (newInput & (1 << GUI_INPUT_RIGHT)) {
struct GUIMenuItem* item = GUIMenuItemListGetPointer(&menu->items, menu->index);
if (item->validStates) {
_itemNext(item, false);
} else if (menu->index + pageSize < GUIMenuItemListSize(&menu->items)) {
menu->index += pageSize;
} else {
menu->index = GUIMenuItemListSize(&menu->items) - 1;
}
}
if (cursor != GUI_CURSOR_NOT_PRESENT) {
if (cx < params->width - 16) {
int index = (cy / lineHeight) - 2;
if (index >= 0 && index + start < GUIMenuItemListSize(&menu->items)) {
if (menu->index != index + start || !cursorOverItem) {
cursorOverItem = 1;
}
menu->index = index + start;
} else {
cursorOverItem = 0;
}
} else if (cursor == GUI_CURSOR_DOWN || cursor == GUI_CURSOR_DRAGGING) {
if (cy <= 2 * lineHeight && cy > lineHeight && menu->index > 0) {
--menu->index;
} else if (cy <= params->height && cy > params->height - lineHeight && menu->index < GUIMenuItemListSize(&menu->items) - 1) {
++menu->index;
} else if (cy <= params->height - lineHeight && cy > 2 * lineHeight) {
size_t location = cy - 2 * lineHeight;
location *= GUIMenuItemListSize(&menu->items) - 1;
menu->index = location / (params->height - 3 * lineHeight);
}
}
}
if (menu->index < start) {
start = menu->index;
}
while ((menu->index - start + 4) * lineHeight > params->height) {
++start;
}
if (newInput & (1 << GUI_INPUT_CANCEL)) {
break;
}
if (newInput & (1 << GUI_INPUT_SELECT) || (cursorOverItem == 2 && cursor == GUI_CURSOR_CLICKED)) {
*item = GUIMenuItemListGetPointer(&menu->items, menu->index);
if ((*item)->submenu) {
enum GUIMenuExitReason reason = GUIShowMenu(params, (*item)->submenu, item);
if (reason != GUI_MENU_EXIT_BACK) {
return reason;
}
} else if ((*item)->validStates && GUIVariantIsString((*item)->data)) {
_itemNext(*item, true);
} else {
return GUI_MENU_EXIT_ACCEPT;
}
}
if (cursorOverItem == 1 && (cursor == GUI_CURSOR_UP || cursor == GUI_CURSOR_NOT_PRESENT)) {
cursorOverItem = 2;
}
if (newInput & (1 << GUI_INPUT_BACK)) {
return GUI_MENU_EXIT_BACK;
}
params->drawStart();
if (menu->background) {
menu->background->draw(menu->background, GUIMenuItemListGetPointer(&menu->items, menu->index)->data.v.p);
}
if (params->guiPrepare) {
params->guiPrepare();
}
unsigned y = lineHeight;
GUIFontPrint(params->font, 0, y, GUI_ALIGN_LEFT, 0xFFFFFFFF, menu->title);
if (menu->subtitle) {
GUIFontPrint(params->font, 0, y * 2, GUI_ALIGN_LEFT, 0xFFFFFFFF, menu->subtitle);
}
y += 2 * lineHeight;
unsigned right;
GUIFontIconMetrics(params->font, GUI_ICON_SCROLLBAR_BUTTON, &right, 0);
size_t itemsPerScreen = (params->height - y) / lineHeight;
size_t i;
for (i = start; i < GUIMenuItemListSize(&menu->items); ++i) {
int color = 0xE0A0A0A0;
if (i == menu->index) {
color = 0xFFFFFFFF;
GUIFontDrawIcon(params->font, lineHeight * 0.8f, y, GUI_ALIGN_BOTTOM | GUI_ALIGN_RIGHT, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_POINTER);
}
struct GUIMenuItem* item = GUIMenuItemListGetPointer(&menu->items, i);
GUIFontPrint(params->font, lineHeight, y, GUI_ALIGN_LEFT, color, item->title);
if (item->validStates && item->validStates[item->state]) {
GUIFontPrintf(params->font, params->width - right - 8, y, GUI_ALIGN_RIGHT, color, "%s ", item->validStates[item->state]);
}
y += lineHeight;
if (y + lineHeight > params->height) {
enum GUIMenuExitReason reason = GUIMenuRun(params, menu, &state);
switch (reason) {
case GUI_MENU_EXIT_BACK:
if (GUIMenuSavedListSize(&state.stack) > 0) {
struct GUIMenuSavedState* last = GUIMenuSavedListGetPointer(&state.stack, GUIMenuSavedListSize(&state.stack) - 1);
state.start = last->start;
menu = last->menu;
GUIMenuSavedListResize(&state.stack, -1);
break;
}
// Fall through
case GUI_MENU_EXIT_CANCEL:
case GUI_MENU_EXIT_ACCEPT:
*item = state.resultItem;
GUIMenuStateDeinit(&state);
return reason;
case GUI_MENU_ENTER:
*GUIMenuSavedListAppend(&state.stack) = (struct GUIMenuSavedState) {
.start = state.start,
.menu = menu
};
menu = state.resultItem->submenu;
state.start = 0;
break;
case GUI_MENU_CONTINUE:
break;
}
if (itemsPerScreen < GUIMenuItemListSize(&menu->items)) {
size_t top = 2 * lineHeight;
size_t bottom = params->height - 8;
unsigned w;
GUIFontIconMetrics(params->font, GUI_ICON_SCROLLBAR_TRACK, &w, 0);
right = (right - w) / 2;
GUIFontDrawIconSize(params->font, params->width - right - 8, top, 0, bottom - top, 0xA0FFFFFF, GUI_ICON_SCROLLBAR_TRACK);
GUIFontDrawIcon(params->font, params->width - 8, top, GUI_ALIGN_HCENTER | GUI_ALIGN_BOTTOM, GUI_ORIENT_VMIRROR, 0xFFFFFFFF, GUI_ICON_SCROLLBAR_BUTTON);
GUIFontDrawIcon(params->font, params->width - 8, bottom, GUI_ALIGN_HCENTER | GUI_ALIGN_TOP, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_SCROLLBAR_BUTTON);
y = menu->index * (bottom - top - 16) / GUIMenuItemListSize(&menu->items);
GUIFontDrawIcon(params->font, params->width - 8, top + y, GUI_ALIGN_HCENTER | GUI_ALIGN_TOP, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_SCROLLBAR_THUMB);
}
GUIDrawBattery(params);
GUIDrawClock(params);
if (cursor != GUI_CURSOR_NOT_PRESENT) {
GUIFontDrawIcon(params->font, cx, cy, GUI_ALIGN_HCENTER | GUI_ALIGN_TOP, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_CURSOR);
}
if (params->guiFinish) {
params->guiFinish();
}
params->drawEnd();
}
return GUI_MENU_EXIT_CANCEL;
}
static enum GUIMenuExitReason GUIMenuPollInput(struct GUIParams* params, struct GUIMenu* menu, struct GUIMenuState* state) {
size_t lineHeight = GUIFontHeight(params->font);
size_t pageSize = params->height / lineHeight;
if (pageSize > 4) {
pageSize -= 4;
} else {
pageSize = 1;
}
uint32_t newInput = 0;
GUIPollInput(params, &newInput, NULL);
state->cursor = GUIPollCursor(params, &state->cx, &state->cy);
// Check for new direction presses
if (newInput & (1 << GUI_INPUT_UP) && menu->index > 0) {
--menu->index;
}
if (newInput & (1 << GUI_INPUT_DOWN) && menu->index < GUIMenuItemListSize(&menu->items) - 1) {
++menu->index;
}
if (newInput & (1 << GUI_INPUT_LEFT)) {
struct GUIMenuItem* item = GUIMenuItemListGetPointer(&menu->items, menu->index);
if (item->validStates) {
_itemPrev(item, false);
} else if (menu->index >= pageSize) {
menu->index -= pageSize;
} else {
menu->index = 0;
}
}
if (newInput & (1 << GUI_INPUT_RIGHT)) {
struct GUIMenuItem* item = GUIMenuItemListGetPointer(&menu->items, menu->index);
if (item->validStates) {
_itemNext(item, false);
} else if (menu->index + pageSize < GUIMenuItemListSize(&menu->items)) {
menu->index += pageSize;
} else {
menu->index = GUIMenuItemListSize(&menu->items) - 1;
}
}
// Handle cursor movement
if (state->cursor != GUI_CURSOR_NOT_PRESENT) {
unsigned cx = state->cx;
unsigned cy = state->cy;
if (cx < params->width - 16) {
int index = (cy / lineHeight) - 2;
if (index >= 0 && index + state->start < GUIMenuItemListSize(&menu->items)) {
if (menu->index != index + state->start || !state->cursorOverItem) {
state->cursorOverItem = 1;
}
menu->index = index + state->start;
} else {
state->cursorOverItem = 0;
}
} else if (state->cursor == GUI_CURSOR_DOWN || state->cursor == GUI_CURSOR_DRAGGING) {
if (cy <= 2 * lineHeight && cy > lineHeight && menu->index > 0) {
--menu->index;
} else if (cy <= params->height && cy > params->height - lineHeight && menu->index < GUIMenuItemListSize(&menu->items) - 1) {
++menu->index;
} else if (cy <= params->height - lineHeight && cy > 2 * lineHeight) {
size_t location = cy - 2 * lineHeight;
location *= GUIMenuItemListSize(&menu->items) - 1;
menu->index = location / (params->height - 3 * lineHeight);
}
}
}
// Move view up if the active item is before the top of the view
if (menu->index < state->start) {
state->start = menu->index;
}
// Move the view down if the active item is after the bottom of the view
while ((menu->index - state->start + 4) * lineHeight > params->height) {
// TODO: Should this loop be replaced with division?
++state->start;
}
// Handle action inputs
if (newInput & (1 << GUI_INPUT_CANCEL)) {
return GUI_MENU_EXIT_CANCEL;
}
if (newInput & (1 << GUI_INPUT_SELECT) || (state->cursorOverItem == 2 && state->cursor == GUI_CURSOR_CLICKED)) {
struct GUIMenuItem* item = GUIMenuItemListGetPointer(&menu->items, menu->index);
if (item->submenu) {
// Selected menus get shown inline
state->resultItem = item;
return GUI_MENU_ENTER;
} else if (item->validStates && GUIVariantIsString(item->data)) {
// Selected items with multiple (named) states get scrolled through
_itemNext(item, true);
} else {
// Otherwise tell caller item was accepted
state->resultItem = item;
return GUI_MENU_EXIT_ACCEPT;
}
}
if (state->cursorOverItem == 1 && (state->cursor == GUI_CURSOR_UP || state->cursor == GUI_CURSOR_NOT_PRESENT)) {
state->cursorOverItem = 2;
}
if (newInput & (1 << GUI_INPUT_BACK)) {
return GUI_MENU_EXIT_BACK;
}
// No action taken
return GUI_MENU_CONTINUE;
}
static void GUIMenuDraw(struct GUIParams* params, const struct GUIMenu* menu, const struct GUIMenuState* state) {
size_t lineHeight = GUIFontHeight(params->font);
params->drawStart();
if (menu->background) {
menu->background->draw(menu->background, GUIMenuItemListGetConstPointer(&menu->items, menu->index)->data.v.p);
}
if (params->guiPrepare) {
params->guiPrepare();
}
unsigned y = lineHeight;
GUIFontPrint(params->font, 0, y, GUI_ALIGN_LEFT, 0xFFFFFFFF, menu->title);
if (menu->subtitle) {
GUIFontPrint(params->font, 0, y * 2, GUI_ALIGN_LEFT, 0xFFFFFFFF, menu->subtitle);
}
y += 2 * lineHeight;
unsigned right;
GUIFontIconMetrics(params->font, GUI_ICON_SCROLLBAR_BUTTON, &right, 0);
size_t itemsPerScreen = (params->height - y) / lineHeight;
size_t i;
for (i = state->start; i < GUIMenuItemListSize(&menu->items); ++i) {
int color = 0xE0A0A0A0;
if (i == menu->index) {
color = 0xFFFFFFFF;
GUIFontDrawIcon(params->font, lineHeight * 0.8f, y, GUI_ALIGN_BOTTOM | GUI_ALIGN_RIGHT, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_POINTER);
}
const struct GUIMenuItem* item = GUIMenuItemListGetConstPointer(&menu->items, i);
GUIFontPrint(params->font, lineHeight, y, GUI_ALIGN_LEFT, color, item->title);
if (item->validStates && item->validStates[item->state]) {
GUIFontPrintf(params->font, params->width - right - 8, y, GUI_ALIGN_RIGHT, color, "%s ", item->validStates[item->state]);
}
y += lineHeight;
if (y + lineHeight > params->height) {
break;
}
}
if (itemsPerScreen < GUIMenuItemListSize(&menu->items)) {
size_t top = 2 * lineHeight;
size_t bottom = params->height - 8;
unsigned w;
GUIFontIconMetrics(params->font, GUI_ICON_SCROLLBAR_TRACK, &w, 0);
right = (right - w) / 2;
GUIFontDrawIconSize(params->font, params->width - right - 8, top, 0, bottom - top, 0xA0FFFFFF, GUI_ICON_SCROLLBAR_TRACK);
GUIFontDrawIcon(params->font, params->width - 8, top, GUI_ALIGN_HCENTER | GUI_ALIGN_BOTTOM, GUI_ORIENT_VMIRROR, 0xFFFFFFFF, GUI_ICON_SCROLLBAR_BUTTON);
GUIFontDrawIcon(params->font, params->width - 8, bottom, GUI_ALIGN_HCENTER | GUI_ALIGN_TOP, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_SCROLLBAR_BUTTON);
y = menu->index * (bottom - top - 16) / GUIMenuItemListSize(&menu->items);
GUIFontDrawIcon(params->font, params->width - 8, top + y, GUI_ALIGN_HCENTER | GUI_ALIGN_TOP, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_SCROLLBAR_THUMB);
}
GUIDrawBattery(params);
GUIDrawClock(params);
if (state->cursor != GUI_CURSOR_NOT_PRESENT) {
GUIFontDrawIcon(params->font, state->cx, state->cy, GUI_ALIGN_HCENTER | GUI_ALIGN_TOP, GUI_ORIENT_0, 0xFFFFFFFF, GUI_ICON_CURSOR);
}
if (params->guiFinish) {
params->guiFinish();
}
params->drawEnd();
}
enum GUIMenuExitReason GUIMenuRun(struct GUIParams* params, struct GUIMenu* menu, struct GUIMenuState* state) {
enum GUIMenuExitReason reason = GUIMenuPollInput(params, menu, state);
if (reason != GUI_MENU_CONTINUE) {
return reason;
}
GUIMenuDraw(params, menu, state);
return GUI_MENU_CONTINUE;
}
enum GUIMenuExitReason GUIShowMessageBox(struct GUIParams* params, int buttons, int frames, const char* format, ...) {