diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c index 73b548b4..1b6103e8 100644 --- a/src/dusk/ui/focus/uifocus.c +++ b/src/dusk/ui/focus/uifocus.c @@ -30,7 +30,8 @@ uifocusitem_t * uiFocusPush( const uint8_t rows, uifocusitemcallback_t selected, uifocusitemcallback_t changed, - uifocusitemcallback_t closed + uifocusitemcallback_t closed, + void *user ) { assertTrue( UI_FOCUS.count < UI_FOCUS_STACK_MAX, @@ -46,6 +47,7 @@ uifocusitem_t * uiFocusPush( item->selected = selected; item->changed = changed; item->closed = closed; + item->user = user; UI_FOCUS.count++; if(item->changed != NULL) item->changed(item); return item; diff --git a/src/dusk/ui/focus/uifocus.h b/src/dusk/ui/focus/uifocus.h index 1c69edef..9369e097 100644 --- a/src/dusk/ui/focus/uifocus.h +++ b/src/dusk/ui/focus/uifocus.h @@ -71,6 +71,7 @@ void uiFocusInit(void); * @param selected Called when the user selects the focused cell. * @param changed Called when the focused cell position changes. * @param closed Called when this focus item is popped. + * @param user Arbitrary pointer stored on the item before changed fires. * @returns Pointer to the newly pushed focus item. */ uifocusitem_t * uiFocusPush( @@ -78,7 +79,8 @@ uifocusitem_t * uiFocusPush( const uint8_t rows, uifocusitemcallback_t selected, uifocusitemcallback_t changed, - uifocusitemcallback_t closed + uifocusitemcallback_t closed, + void *user ); /** diff --git a/src/dusk/ui/focus/uifocusitem.h b/src/dusk/ui/focus/uifocusitem.h index ebe44167..431835ac 100644 --- a/src/dusk/ui/focus/uifocusitem.h +++ b/src/dusk/ui/focus/uifocusitem.h @@ -29,4 +29,5 @@ struct uifocusitem_s { uifocusitemcallback_t selected; uifocusitemcallback_t changed; uifocusitemcallback_t closed; + void *user; }; diff --git a/src/dusk/ui/frame/uisettings.c b/src/dusk/ui/frame/uisettings.c index 46058a0c..551ada08 100644 --- a/src/dusk/ui/frame/uisettings.c +++ b/src/dusk/ui/frame/uisettings.c @@ -7,63 +7,67 @@ #include "uisettings.h" #include "ui/frame/uiframe.h" -#include "ui/widget/uicheckbox.h" #include "util/memory.h" #include "display/spritebatch/spritebatch.h" #include "display/text/text.h" uisettings_t UI_SETTINGS; -bool_t uiSettingsSelected(const uifocusitem_t *item) { - if(item->y == 3) uiCheckboxToggle(&UI_SETTINGS.checkboxTest); - return true; +void uiSettingsSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + if(item->type == UIMENU_WIDGET_TYPE_CHECKBOX) { + uiCheckboxToggle(&UI_SETTINGS.items[index].checkbox); + } } -bool_t uiSettingsChanged(const uifocusitem_t *item) { - uiCheckboxSetHighlighted(&UI_SETTINGS.checkboxTest, item->y == 3); - return true; -} - -bool_t uiSettingsClosed(const uifocusitem_t *item) { - UI_SETTINGS.item = NULL; - return true; +void uiSettingsClosed(const uimenu_t *menu) { + // nothing extra needed; menu clears focusItem itself } errorret_t uiSettingsInit(void) { memoryZero(&UI_SETTINGS, sizeof(uisettings_t)); - uiCheckboxInit(&UI_SETTINGS.checkboxTest, "Enable thing?"); + + UI_SETTINGS.items[0] = (uimenuitem_t){ + .type = UIMENU_WIDGET_TYPE_LABEL, + .label = "Settings" + }; + UI_SETTINGS.items[1] = (uimenuitem_t){ + .type = UIMENU_WIDGET_TYPE_LABEL, + .label = "Settings two" + }; + UI_SETTINGS.items[2] = (uimenuitem_t){ + .type = UIMENU_WIDGET_TYPE_LABEL, + .label = "Settings three" + }; + UI_SETTINGS.items[3].type = UIMENU_WIDGET_TYPE_CHECKBOX; + uiCheckboxInit(&UI_SETTINGS.items[3].checkbox, "Enable thing?"); + + uiMenuInit(&UI_SETTINGS.menu, uiSettingsSelected, uiSettingsClosed, NULL); + uiMenuSetItems( + &UI_SETTINGS.menu, + UI_SETTINGS.items, + UI_SETTINGS_ITEM_COUNT, + 2 + ); + errorOk(); } errorret_t uiSettingsDraw(void) { - if(UI_SETTINGS.item == NULL) errorOk(); + if(!uiMenuIsActive(&UI_SETTINGS.menu)) errorOk(); - const char_t *texts[] = { - "Settings", - "Settings two", - "Settings three" - }; - const uint8_t textCount = sizeof(texts) / sizeof(texts[0]); - const uint8_t totalRows = textCount + 1; + uint8_t rows = UI_SETTINGS_ITEM_COUNT; - errorChain(uiFrameDraw( - 0.0f, 0.0f, 100.0f, - FONT_DEFAULT.tileset->tileHeight * totalRows + (UIFRAME_BORDER_HEIGHT * 2) - )); - - for(uint8_t i = 0; i < textCount; i++) { - errorChain(textDraw( - UIFRAME_BORDER_WIDTH, - UIFRAME_BORDER_HEIGHT + (i * FONT_DEFAULT.tileset->tileHeight), - texts[i], - UI_SETTINGS.item->y == i ? COLOR_RED : COLOR_WHITE, NULL - )); - } - - errorChain(uiCheckboxDraw( - &UI_SETTINGS.checkboxTest, + errorChain(uiFrameDraw(0.0f, 0.0f, 300.0f, 300)); + errorChain(uiMenuDraw( + &UI_SETTINGS.menu, UIFRAME_BORDER_WIDTH, - UIFRAME_BORDER_HEIGHT + (textCount * FONT_DEFAULT.tileset->tileHeight) + UIFRAME_BORDER_HEIGHT, + 300.0f - (UIFRAME_BORDER_WIDTH * 2), + 300.0f - (UIFRAME_BORDER_HEIGHT * 2) )); errorChain(spriteBatchFlush()); @@ -71,22 +75,15 @@ errorret_t uiSettingsDraw(void) { } bool_t uiSettingsIsOpen(void) { - return UI_SETTINGS.item != NULL; + return uiMenuIsActive(&UI_SETTINGS.menu); } void uiSettingsOpen() { - if(UI_SETTINGS.item != NULL) return; - - UI_SETTINGS.item = uiFocusPush( - 1, 4, uiSettingsSelected, uiSettingsChanged, uiSettingsClosed - ); + uiMenuOpen(&UI_SETTINGS.menu); } void uiSettingsClose() { - if(UI_SETTINGS.item == NULL) return; - - uiFocusPopItem(UI_SETTINGS.item); - UI_SETTINGS.item = NULL; + uiMenuClose(&UI_SETTINGS.menu); } errorret_t uiSettingsDispose(void) { diff --git a/src/dusk/ui/frame/uisettings.h b/src/dusk/ui/frame/uisettings.h index 7b2ef2f9..386dcbfd 100644 --- a/src/dusk/ui/frame/uisettings.h +++ b/src/dusk/ui/frame/uisettings.h @@ -7,12 +7,13 @@ #pragma once #include "error/error.h" -#include "ui/focus/uifocus.h" -#include "ui/widget/uicheckbox.h" +#include "ui/widget/uimenu.h" + +#define UI_SETTINGS_ITEM_COUNT 4 typedef struct { - uifocusitem_t *item; - uicheckbox_t checkboxTest; + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_ITEM_COUNT]; } uisettings_t; extern uisettings_t UI_SETTINGS; diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index 70f9d666..5c3c5f88 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -6,4 +6,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uicheckbox.c + uimenu.c ) diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c new file mode 100644 index 00000000..5e7b8a5a --- /dev/null +++ b/src/dusk/ui/widget/uimenu.c @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uimenu.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "display/text/text.h" +#include "display/color.h" + +void uiMenuInit( + uimenu_t *menu, + uimenuselectedcallback_t selected, + uimenuclosedcallback_t closed, + uimenuchangedcallback_t changed +) { + assertNotNull(menu, "Menu cannot be NULL"); + memoryZero(menu, sizeof(uimenu_t)); + menu->selected = selected; + menu->closed = closed; + menu->changed = changed; +} + +void uiMenuSetItems( + uimenu_t *menu, + const uimenuitem_t *items, + const uint8_t itemCount, + const uint8_t columns +) { + assertNotNull(menu, "Menu cannot be NULL"); + assertNotNull(items, "Items cannot be NULL"); + assertTrue(itemCount > 0, "Item count must be > 0"); + assertTrue(columns > 0, "Columns must be > 0"); + menu->items = (uimenuitem_t *)items; + menu->itemCount = itemCount; + menu->columns = columns; +} + +void uiMenuSetPosition(uimenu_t *menu, const uint8_t x, const uint8_t y) { + assertNotNull(menu, "Menu cannot be NULL"); + if(menu->focusItem == NULL) return; + uiFocusSetPosition(menu->focusItem, x, y); +} + +void uiMenuOpen(uimenu_t *menu) { + assertNotNull(menu, "Menu cannot be NULL"); + assertNotNull(menu->items, "Menu items cannot be NULL"); + assertTrue(menu->itemCount > 0, "Menu item count must be > 0"); + assertTrue(menu->columns > 0, "Menu columns must be > 0"); + if(menu->focusItem != NULL) return; + + uint8_t rows = (menu->itemCount + menu->columns - 1) / menu->columns; + menu->focusItem = uiFocusPush( + menu->columns, rows, + uiMenuFocusSelected, + uiMenuFocusChanged, + uiMenuFocusClosed, + menu + ); +} + +void uiMenuClose(uimenu_t *menu) { + assertNotNull(menu, "Menu cannot be NULL"); + if(menu->focusItem == NULL) return; + uiFocusPopItem(menu->focusItem); + menu->focusItem = NULL; +} + +bool_t uiMenuIsActive(const uimenu_t *menu) { + assertNotNull(menu, "Menu cannot be NULL"); + return menu->focusItem != NULL; +} + +errorret_t uiMenuDraw( + const uimenu_t *menu, + const float_t x, + const float_t y, + const float_t width, + const float_t height +) { + assertNotNull(menu, "Menu cannot be NULL"); + if(menu->itemCount == 0) errorOk(); + + float_t colStep = width / (float_t)menu->columns; + float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + + uint8_t focusIndex = menu->focusItem != NULL ? + menu->focusItem->y * menu->columns + menu->focusItem->x : + 0xFF; + + for(uint8_t i = 0; i < menu->itemCount; i++) { + const uimenuitem_t *item = &menu->items[i]; + uint8_t col = i % menu->columns; + uint8_t row = i / menu->columns; + float_t ix = x + (float_t)col * colStep; + float_t iy = y + (float_t)row * rowHeight; + bool_t highlighted = i == focusIndex; + + switch(item->type) { + case UIMENU_WIDGET_TYPE_LABEL: + errorChain(textDraw( + ix, iy, item->label, + highlighted ? COLOR_RED : COLOR_WHITE, + &FONT_DEFAULT + )); + break; + + case UIMENU_WIDGET_TYPE_CHECKBOX: + errorChain(uiCheckboxDraw(&item->checkbox, ix, iy)); + break; + + default: + break; + } + } + + errorOk(); +} + +bool_t uiMenuFocusSelected(const uifocusitem_t *focusItem) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + uimenu_t *menu = (uimenu_t *)focusItem->user; + if(menu->selected == NULL) return true; + + uint8_t index = focusItem->y * menu->columns + focusItem->x; + if(index >= menu->itemCount) return true; + + menu->selected(menu, index, &menu->items[index]); + return true; +} + +bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + uimenu_t *menu = (uimenu_t *)focusItem->user; + + uint8_t index = focusItem->y * menu->columns + focusItem->x; + + for(uint8_t i = 0; i < menu->itemCount; i++) { + uimenuitem_t *item = &menu->items[i]; + if(item->type != UIMENU_WIDGET_TYPE_CHECKBOX) continue; + uiCheckboxSetHighlighted(&item->checkbox, i == index); + } + + if(menu->changed == NULL) return true; + if(index >= menu->itemCount) return true; + + menu->changed(menu, index, &menu->items[index]); + return true; +} + +bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem) { + assertNotNull(focusItem, "Focus item cannot be NULL"); + assertNotNull(focusItem->user, "Focus item user cannot be NULL"); + uimenu_t *menu = (uimenu_t *)focusItem->user; + menu->focusItem = NULL; + if(menu->closed != NULL) menu->closed(menu); + return true; +} diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h new file mode 100644 index 00000000..1acdf57d --- /dev/null +++ b/src/dusk/ui/widget/uimenu.h @@ -0,0 +1,160 @@ +/** + * 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/focus/uifocus.h" +#include "ui/widget/uicheckbox.h" + +#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX + +typedef struct uimenu_s uimenu_t; + +typedef enum { + UIMENU_WIDGET_TYPE_NONE, + UIMENU_WIDGET_TYPE_LABEL, + UIMENU_WIDGET_TYPE_CHECKBOX, +} uimenuwidgettype_t; + +typedef struct { + uimenuwidgettype_t type; + union { + char_t label[UI_MENU_LABEL_MAX]; + uicheckbox_t checkbox; + }; +} uimenuitem_t; + +typedef void (*uimenuselectedcallback_t)( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +); + +typedef void (*uimenuchangedcallback_t)( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +); + +typedef void (*uimenuclosedcallback_t)(const uimenu_t *menu); + +typedef struct uimenu_s { + uimenuitem_t *items; + uint8_t itemCount; + uint8_t columns; + uifocusitem_t *focusItem; + + uimenuselectedcallback_t selected; + uimenuclosedcallback_t closed; + uimenuchangedcallback_t changed; +} uimenu_t; + +/** + * Initializes a menu, clearing all items and focus state. + * + * @param menu The menu to initialize. + * @param items The list of items to display in the menu. + * @param itemCount The number of items in the list. + * @param columns The number of columns to display the items in. + * @param selected The callback to invoke when an item is selected. + * @param closed The callback to invoke when the menu is closed. + * @param changed The callback to invoke when the menu changes. + */ +void uiMenuInit( + uimenu_t *menu, + uimenuselectedcallback_t selected, + uimenuclosedcallback_t closed, + uimenuchangedcallback_t changed +); + +/** + * Sets the items to display in the menu. + * + * @param menu The menu to update. + * @param items The list of items to display in the menu. + * @param itemCount The number of items in the list. + * @param columns The number of columns to display the items in. + */ +void uiMenuSetItems( + uimenu_t *menu, + const uimenuitem_t *items, + const uint8_t itemCount, + const uint8_t columns +); + +/** + * Sets the position of the menu on the screen. + * + * @param menu The menu to position. + * @param x The x-coordinate to position the menu at. + * @param y The y-coordinate to position the menu at. + */ +void uiMenuSetPosition(uimenu_t *menu, const uint8_t x, const uint8_t y); + +/** + * Pushes a menu onto the UI focus stack, making it the active menu. + * + * @param menu The menu to push. + */ +void uiMenuOpen(uimenu_t *menu); + +/** + * Pops a menu from the UI focus stack, removing it from the active menu. + * + * @param menu The menu to pop. + */ +void uiMenuClose(uimenu_t *menu); + +/** + * Returns whether the menu is currently active (on the UI focus stack). + * + * @param menu The menu to query. + * @returns True if the menu is active. + */ +bool_t uiMenuIsActive(const uimenu_t *menu); + +/** + * Draws the menu at the specified position and size. + * + * @param menu The menu to draw. + * @param x The x-coordinate to draw the menu at. + * @param y The y-coordinate to draw the menu at. + * @param width The width of the menu. + * @param height The height of the menu. + * @returns An error code indicating success or failure. + */ +errorret_t uiMenuDraw( + const uimenu_t *menu, + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +/** + * Internal focus callback — forwards selection to the menu's selected handler. + * + * @param focusItem The active focus item; user field must point to uimenu_t. + * @returns True. + */ +bool_t uiMenuFocusSelected(const uifocusitem_t *focusItem); + +/** + * Internal focus callback — updates checkbox highlights and fires changed. + * + * @param focusItem The active focus item; user field must point to uimenu_t. + * @returns True. + */ +bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem); + +/** + * Internal focus callback — clears focusItem and fires the closed handler. + * + * @param focusItem The active focus item; user field must point to uimenu_t. + * @returns True. + */ +bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem); \ No newline at end of file