From 774c8ad0f89511720a47196530c12d59570e8eb3 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 20 Aug 2026 12:44:01 -0500 Subject: [PATCH] Add uimodal dialog; uibutton reuses uilabel uibutton now embeds a uilabel instead of drawing its text directly, caching glyph sprites since a button's label never changes after init. uimenu.c's item pointer in uiMenuDraw drops its unnecessary const so uiButtonDraw can update the button's cache. Also fixes remaining #include "ui/frame/uiframe.h" references left over from uiframe's move to ui/widget/. uimodal is a new generalized dialog (title + message + up to UI_MODAL_OPTIONS_MAX option buttons) built the same way uiconfirm is, but driven by a caller-supplied option list instead of a fixed confirm/cancel pair. Co-Authored-By: Claude Sonnet 5 --- src/dusk/ui/frame/CMakeLists.txt | 2 +- src/dusk/ui/frame/backpack/uibackpack.c | 2 +- src/dusk/ui/frame/battle/uibattlemenu.c | 2 +- src/dusk/ui/frame/game/uigamemenu.c | 2 +- src/dusk/ui/frame/mainmenu/uimainmenu.c | 2 +- src/dusk/ui/frame/uiconfirm.c | 2 +- src/dusk/ui/frame/uimodal.c | 171 ++++++++++++++++++++++++ src/dusk/ui/frame/uimodal.h | 116 ++++++++++++++++ src/dusk/ui/rpg/textbox/uitextbox.c | 2 +- src/dusk/ui/rpg/textbox/uitextboxmain.c | 2 +- src/dusk/ui/rpg/textbox/uitextboxmini.c | 2 +- src/dusk/ui/uielement.c | 2 +- src/dusk/ui/uielementlist.c | 14 +- src/dusk/ui/widget/CMakeLists.txt | 1 + src/dusk/ui/widget/uibutton.c | 20 +-- src/dusk/ui/widget/uibutton.h | 11 +- src/dusk/ui/widget/uimenu.c | 2 +- 17 files changed, 328 insertions(+), 27 deletions(-) create mode 100644 src/dusk/ui/frame/uimodal.c create mode 100644 src/dusk/ui/frame/uimodal.h diff --git a/src/dusk/ui/frame/CMakeLists.txt b/src/dusk/ui/frame/CMakeLists.txt index 39047650..0f189335 100644 --- a/src/dusk/ui/frame/CMakeLists.txt +++ b/src/dusk/ui/frame/CMakeLists.txt @@ -5,8 +5,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - uiframe.c uiconfirm.c + uimodal.c ) add_subdirectory(game) diff --git a/src/dusk/ui/frame/backpack/uibackpack.c b/src/dusk/ui/frame/backpack/uibackpack.c index ab06074f..2f054125 100644 --- a/src/dusk/ui/frame/backpack/uibackpack.c +++ b/src/dusk/ui/frame/backpack/uibackpack.c @@ -6,7 +6,7 @@ */ #include "uibackpack.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" #include "rpg/item/backpack.h" #include "util/memory.h" #include "util/string.h" diff --git a/src/dusk/ui/frame/battle/uibattlemenu.c b/src/dusk/ui/frame/battle/uibattlemenu.c index 21e348a5..a0265b14 100644 --- a/src/dusk/ui/frame/battle/uibattlemenu.c +++ b/src/dusk/ui/frame/battle/uibattlemenu.c @@ -6,7 +6,7 @@ */ #include "uibattlemenu.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index b9b5f873..e92d48a7 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -6,7 +6,7 @@ */ #include "uigamemenu.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" #include "ui/frame/uiconfirm.h" #include "ui/frame/backpack/uibackpack.h" #include "ui/rpg/textbox/uitextboxmain.h" diff --git a/src/dusk/ui/frame/mainmenu/uimainmenu.c b/src/dusk/ui/frame/mainmenu/uimainmenu.c index 810a0f8d..6f96a2ba 100644 --- a/src/dusk/ui/frame/mainmenu/uimainmenu.c +++ b/src/dusk/ui/frame/mainmenu/uimainmenu.c @@ -6,7 +6,7 @@ */ #include "uimainmenu.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" #include "scene/scene.h" #include "engine/engine.h" #include "rpg/battle/testbattle/testbattle.h" diff --git a/src/dusk/ui/frame/uiconfirm.c b/src/dusk/ui/frame/uiconfirm.c index 75e33931..76fe8fbd 100644 --- a/src/dusk/ui/frame/uiconfirm.c +++ b/src/dusk/ui/frame/uiconfirm.c @@ -6,7 +6,7 @@ */ #include "uiconfirm.h" -#include "uiframe.h" +#include "ui/widget/uiframe.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" diff --git a/src/dusk/ui/frame/uimodal.c b/src/dusk/ui/frame/uimodal.c new file mode 100644 index 00000000..e907a9b3 --- /dev/null +++ b/src/dusk/ui/frame/uimodal.c @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uimodal.h" +#include "ui/widget/uiframe.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "util/math.h" +#include "display/screen/screen.h" +#include "display/text/text.h" +#include "display/color.h" +#include "display/spritebatch/spritebatch.h" +#include "display/texture/texture.h" +#include "display/shader/shaderunlit.h" + +#define UI_MODAL_BACKDROP_COLOR color4b(0, 0, 0, 160) + +uimodal_t UI_MODAL; + +void uiModalSelected( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + UI_MODAL.result = index; + uiModalClose(); +} + +void uiModalClosed(const uimenu_t *menu) { + if(UI_MODAL.callback != NULL) { + UI_MODAL.callback(UI_MODAL.result, UI_MODAL.user); + } +} + +errorret_t uiModalInit(void) { + memoryZero(&UI_MODAL, sizeof(uimodal_t)); + + uiLabelInit( + &UI_MODAL.titleLabel, UI_MODAL.titleText, + UI_MODAL.titleSprites, UI_MODAL_TITLE_SPRITES_MAX + ); + uiLabelInit( + &UI_MODAL.messageLabel, UI_MODAL.messageText, + UI_MODAL.messageSprites, UI_MODAL_MESSAGE_SPRITES_MAX + ); + + errorOk(); +} + +errorret_t uiModalDraw(void) { + if(!uiMenuIsActive(&UI_MODAL.menu)) errorOk(); + + spritebatchsprite_t backdropSprite = { + .min = { 0.0f, 0.0f, 0.0f }, + .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + shadermaterial_t backdropMaterial = { + .unlit = { + .color = UI_MODAL_BACKDROP_COLOR, + .texture = &TEXTURE_WHITE + } + }; + errorChain( + spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial) + ); + errorChain(spriteBatchFlush()); + + float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + float_t contentWidth = mathMax( + mathMax( + (float_t)UI_MODAL.titleLabel.width, (float_t)UI_MODAL.messageLabel.width + ), + UI_MODAL_MIN_WIDTH - (UI_FRAME_START_X * 2) + ); + float_t width = contentWidth + (UI_FRAME_START_X * 2); + float_t height = (UI_FRAME_START_Y * 2) + + (float_t)UI_MODAL.titleLabel.height + UI_FRAME_PADDING_Y + + (float_t)UI_MODAL.messageLabel.height + UI_FRAME_PADDING_Y + + rowHeight; + + float_t x = (float_t)SCREEN.scanX + + ((float_t)SCREEN.scanWidth - width) * 0.5f; + float_t y = (float_t)SCREEN.scanY + + ((float_t)SCREEN.scanHeight - height) * 0.5f; + + errorChain(uiFrameDraw(x, y, width, height)); + + float_t contentX = x + UI_FRAME_START_X; + float_t contentY = y + UI_FRAME_START_Y; + + uiLabelSetX(&UI_MODAL.titleLabel, contentX); + uiLabelSetY(&UI_MODAL.titleLabel, contentY); + errorChain(uiLabelRender(&UI_MODAL.titleLabel, COLOR_WHITE)); + + float_t messageY = contentY + (float_t)UI_MODAL.titleLabel.height + + UI_FRAME_PADDING_Y; + uiLabelSetX(&UI_MODAL.messageLabel, contentX); + uiLabelSetY(&UI_MODAL.messageLabel, messageY); + errorChain(uiLabelRender(&UI_MODAL.messageLabel, COLOR_WHITE)); + + float_t buttonsY = messageY + (float_t)UI_MODAL.messageLabel.height + + UI_FRAME_PADDING_Y; + errorChain( + uiMenuDraw(&UI_MODAL.menu, contentX, buttonsY, contentWidth, rowHeight) + ); + + errorChain(spriteBatchFlush()); + errorOk(); +} + +bool_t uiModalIsOpen(void) { + return uiMenuIsActive(&UI_MODAL.menu); +} + +uint8_t uiModalGetResult(void) { + return UI_MODAL.result; +} + +void uiModalOpen( + const char_t *title, + const char_t *message, + const char_t **options, + const uint8_t optionCount, + uimodalcallback_t callback, + void *user +) { + assertNotNull(title, "Title cannot be NULL"); + assertNotNull(message, "Message cannot be NULL"); + assertNotNull(options, "Options cannot be NULL"); + assertTrue(optionCount > 0, "Must have at least one option"); + assertTrue( + optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal" + ); + + stringCopy(UI_MODAL.titleText, title, UI_MODAL_TITLE_TEXT_MAX - 1); + UI_MODAL.titleLabel.dirty = true; + uiLabelRebuffer(&UI_MODAL.titleLabel); + + stringCopy(UI_MODAL.messageText, message, UI_MODAL_MESSAGE_TEXT_MAX - 1); + UI_MODAL.messageLabel.dirty = true; + uiLabelRebuffer(&UI_MODAL.messageLabel); + + UI_MODAL.callback = callback; + UI_MODAL.user = user; + UI_MODAL.result = UI_MODAL_RESULT_NONE; + + MENU_BEGIN( + &UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL + ); + for(uint8_t i = 0; i < optionCount; i++) { + MENU_BUTTON(options[i]); + } + MENU_END(UI_MODAL.options, menuIndex); + + uiMenuOpen(&UI_MODAL.menu); +} + +void uiModalClose(void) { + uiMenuClose(&UI_MODAL.menu); +} + +errorret_t uiModalDispose(void) { + errorOk(); +} diff --git a/src/dusk/ui/frame/uimodal.h b/src/dusk/ui/frame/uimodal.h new file mode 100644 index 00000000..41d77155 --- /dev/null +++ b/src/dusk/ui/frame/uimodal.h @@ -0,0 +1,116 @@ +/** + * 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" + +#define UI_MODAL_TITLE_TEXT_MAX 64 +#define UI_MODAL_TITLE_SPRITES_MAX UI_MODAL_TITLE_TEXT_MAX +#define UI_MODAL_MESSAGE_TEXT_MAX 256 +#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX +#define UI_MODAL_OPTIONS_MAX 4 +#define UI_MODAL_MIN_WIDTH 160.0f +#define UI_MODAL_RESULT_NONE 0xFF + +/** + * Callback invoked once a modal is dismissed. + * + * @param optionIndex Index into the options array passed to uiModalOpen + * that was selected, or UI_MODAL_RESULT_NONE if backed out of without + * selecting an option. + * @param user Arbitrary pointer passed to uiModalOpen. + */ +typedef void (*uimodalcallback_t)(const uint8_t optionIndex, void *user); + +typedef struct { + uilabel_t titleLabel; + char_t titleText[UI_MODAL_TITLE_TEXT_MAX]; + spritebatchsprite_t titleSprites[UI_MODAL_TITLE_SPRITES_MAX]; + + uilabel_t messageLabel; + char_t messageText[UI_MODAL_MESSAGE_TEXT_MAX]; + spritebatchsprite_t messageSprites[UI_MODAL_MESSAGE_SPRITES_MAX]; + + uimenu_t menu; + uimenuitem_t options[UI_MODAL_OPTIONS_MAX]; + + uimodalcallback_t callback; + void *user; + uint8_t result; +} uimodal_t; + +extern uimodal_t UI_MODAL; + +/** + * Initializes the modal dialog. + * + * @return Any error that occurs. + */ +errorret_t uiModalInit(void); + +/** + * Draws the modal dialog: a semi-transparent black backdrop covering the + * whole screen, then its own centered frame with the title, message, and + * option buttons. No-op when not open. + * + * @return Any error that occurs. + */ +errorret_t uiModalDraw(void); + +/** + * Returns true when the modal dialog is currently open. + * + * @returns True if open. + */ +bool_t uiModalIsOpen(void); + +/** + * Returns the result of the most recently dismissed modal. + * + * @returns The selected option index, or UI_MODAL_RESULT_NONE. + */ +uint8_t uiModalGetResult(void); + +/** + * Opens the modal dialog with the given title, message, and options. + * callback is invoked exactly once with the result, whether the dialog + * was dismissed by selecting an option or by pressing cancel/back. + * + * @param title Display title; copied internally, safe to be transient. + * @param message Display message; copied internally, safe to be + * transient. + * @param options Array of option label strings; NOT copied internally, + * the pointers are stored directly by the underlying buttons, so they + * must remain valid for as long as the modal is open (e.g. string + * literals or locale-owned strings). + * @param optionCount Number of options, from 1 to UI_MODAL_OPTIONS_MAX. + * @param callback Called with the result once the dialog closes. May be + * NULL. + * @param user Arbitrary pointer passed through to callback. + */ +void uiModalOpen( + const char_t *title, + const char_t *message, + const char_t **options, + const uint8_t optionCount, + uimodalcallback_t callback, + void *user +); + +/** + * Closes the modal dialog. No-op when already closed. + */ +void uiModalClose(void); + +/** + * Disposes of the modal dialog. + * + * @return Any error that occurs. + */ +errorret_t uiModalDispose(void); diff --git a/src/dusk/ui/rpg/textbox/uitextbox.c b/src/dusk/ui/rpg/textbox/uitextbox.c index 47bdbdf6..20b078e5 100644 --- a/src/dusk/ui/rpg/textbox/uitextbox.c +++ b/src/dusk/ui/rpg/textbox/uitextbox.c @@ -14,7 +14,7 @@ #include "display/color.h" #include "display/spritebatch/spritebatch.h" #include "display/shader/shaderunlit.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" void uiTextboxInit( uitextbox_t *box, diff --git a/src/dusk/ui/rpg/textbox/uitextboxmain.c b/src/dusk/ui/rpg/textbox/uitextboxmain.c index e65892ea..09918b1f 100644 --- a/src/dusk/ui/rpg/textbox/uitextboxmain.c +++ b/src/dusk/ui/rpg/textbox/uitextboxmain.c @@ -12,7 +12,7 @@ #include "display/color.h" #include "display/spritebatch/spritebatch.h" #include "display/shader/shaderunlit.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" uitextboxmain_t UI_TEXTBOX_MAIN; static uifocusitem_t *focusItem = NULL; diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.c b/src/dusk/ui/rpg/textbox/uitextboxmini.c index 251eaa3b..8ae38f34 100644 --- a/src/dusk/ui/rpg/textbox/uitextboxmini.c +++ b/src/dusk/ui/rpg/textbox/uitextboxmini.c @@ -12,7 +12,7 @@ #include "rpg/rpgcamera.h" #include "display/text/text.h" #include "display/screen/screen.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" void uiTextboxMiniInit(uitextboxmini_t *mini) { assertNotNull(mini, "Mini textbox cannot be NULL"); diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 064aba75..af4c8cf3 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -7,7 +7,7 @@ #include "uielement.h" #include "assert/assert.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" #include "engine/engine.h" bool_t uiElementIsNull(const uielement_t *element) { diff --git a/src/dusk/ui/uielementlist.c b/src/dusk/ui/uielementlist.c index 07e867e1..ac28237d 100644 --- a/src/dusk/ui/uielementlist.c +++ b/src/dusk/ui/uielementlist.c @@ -6,7 +6,7 @@ */ #include "uielementlist.h" -#include "ui/frame/uiframe.h" +#include "ui/widget/uiframe.h" #include "ui/debug/uifps.h" #include "ui/overlay/uifullbox.h" #include "ui/overlay/uiloading.h" @@ -20,6 +20,7 @@ #include "ui/frame/battle/uibattlehud.h" #include "ui/frame/backpack/uibackpack.h" #include "ui/frame/uiconfirm.h" +#include "ui/frame/uimodal.h" #include "ui/rpg/textbox/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxminilist.h" #include "ui/rpg/uiemoji.h" @@ -89,6 +90,11 @@ uielement_t UI_ELEMENTS[] = { .draw = uiConfirmDraw, .dispose = uiConfirmDispose }, + { + .init = uiModalInit, + .draw = uiModalDraw, + .dispose = uiModalDispose + }, { .init = uiTextboxMainInit, @@ -133,9 +139,9 @@ uielement_t UI_ELEMENTS[] = { // =========================================================================== // Debug components, disregards everything else. // =========================================================================== - { .draw = uiConsoleDraw }, - { .draw = uiFPSDraw }, - { .draw = uiPlayerPosDraw }, + { .init = uiConsoleInit, .draw = uiConsoleDraw }, + { .init = uiFPSInit, .draw = uiFPSDraw }, + { .init = uiPlayerPosInit, .draw = uiPlayerPosDraw }, // =========================================================================== // Null terminator diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt index a71f8012..936ca640 100644 --- a/src/dusk/ui/widget/CMakeLists.txt +++ b/src/dusk/ui/widget/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uibutton.c uicheckbox.c + uiframe.c uilabel.c uitab.c uislider.c diff --git a/src/dusk/ui/widget/uibutton.c b/src/dusk/ui/widget/uibutton.c index 732c122d..5d0f8a0f 100644 --- a/src/dusk/ui/widget/uibutton.c +++ b/src/dusk/ui/widget/uibutton.c @@ -8,14 +8,15 @@ #include "uibutton.h" #include "assert/assert.h" #include "util/memory.h" -#include "display/text/text.h" #include "display/color.h" void uiButtonInit(uibutton_t *button, const char_t *label) { assertNotNull(button, "Button cannot be NULL"); assertNotNull(label, "Label cannot be NULL"); + memoryZero(button, sizeof(uibutton_t)); - button->label = label; + uiLabelInit(&button->label, label, button->sprites, UI_BUTTON_SPRITES_MAX); + button->label.dirty = true; } bool_t uiButtonIsHighlighted(const uibutton_t *button) { @@ -29,15 +30,16 @@ void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) { } errorret_t uiButtonDraw( - const uibutton_t *button, + uibutton_t *button, const float_t x, const float_t y ) { assertNotNull(button, "Button cannot be NULL"); - errorChain(textDraw( - x, y, button->label, - button->highlighted ? COLOR_RED : COLOR_WHITE, - &FONT_DEFAULT - )); - errorOk(); + + uiLabelSetX(&button->label, x); + uiLabelSetY(&button->label, y); + + return uiLabelRender( + &button->label, button->highlighted ? COLOR_RED : COLOR_WHITE + ); } diff --git a/src/dusk/ui/widget/uibutton.h b/src/dusk/ui/widget/uibutton.h index 0085f564..956938cf 100644 --- a/src/dusk/ui/widget/uibutton.h +++ b/src/dusk/ui/widget/uibutton.h @@ -7,9 +7,13 @@ #pragma once #include "error/error.h" +#include "ui/widget/uilabel.h" + +#define UI_BUTTON_SPRITES_MAX 32 typedef struct { - const char_t *label; + uilabel_t label; + spritebatchsprite_t sprites[UI_BUTTON_SPRITES_MAX]; bool_t highlighted; } uibutton_t; @@ -17,7 +21,8 @@ typedef struct { * Initializes a button. * * @param button The button to initialize. - * @param label Display label. + * @param label Display label; never written to, safe to be a caller- + * owned or immutable string. */ void uiButtonInit(uibutton_t *button, const char_t *label); @@ -46,7 +51,7 @@ void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted); * @return Any error that occurs. */ errorret_t uiButtonDraw( - const uibutton_t *button, + uibutton_t *button, const float_t x, const float_t y ); diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c index 5eb10597..56ec65d3 100644 --- a/src/dusk/ui/widget/uimenu.c +++ b/src/dusk/ui/widget/uimenu.c @@ -98,7 +98,7 @@ errorret_t uiMenuDraw( uint8_t row = 0; for(uint8_t i = 0; i < menu->itemCount; i++) { - const uimenuitem_t *item = &menu->items[i]; + uimenuitem_t *item = &menu->items[i]; if(item->type == UI_MENU_WIDGET_TYPE_LABEL) { if(col > 0) { row++; col = 0; }