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 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:44:01 -05:00
parent 52d1e7414d
commit 774c8ad0f8
17 changed files with 328 additions and 27 deletions
+1 -1
View File
@@ -5,8 +5,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
uiframe.c
uiconfirm.c uiconfirm.c
uimodal.c
) )
add_subdirectory(game) add_subdirectory(game)
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "uibackpack.h" #include "uibackpack.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
#include "rpg/item/backpack.h" #include "rpg/item/backpack.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "uibattlemenu.h" #include "uibattlemenu.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "uigamemenu.h" #include "uigamemenu.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
#include "ui/frame/uiconfirm.h" #include "ui/frame/uiconfirm.h"
#include "ui/frame/backpack/uibackpack.h" #include "ui/frame/backpack/uibackpack.h"
#include "ui/rpg/textbox/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "uimainmenu.h" #include "uimainmenu.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
#include "scene/scene.h" #include "scene/scene.h"
#include "engine/engine.h" #include "engine/engine.h"
#include "rpg/battle/testbattle/testbattle.h" #include "rpg/battle/testbattle/testbattle.h"
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#include "uiconfirm.h" #include "uiconfirm.h"
#include "uiframe.h" #include "ui/widget/uiframe.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
+171
View File
@@ -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();
}
+116
View File
@@ -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);
+1 -1
View File
@@ -14,7 +14,7 @@
#include "display/color.h" #include "display/color.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
void uiTextboxInit( void uiTextboxInit(
uitextbox_t *box, uitextbox_t *box,
+1 -1
View File
@@ -12,7 +12,7 @@
#include "display/color.h" #include "display/color.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
uitextboxmain_t UI_TEXTBOX_MAIN; uitextboxmain_t UI_TEXTBOX_MAIN;
static uifocusitem_t *focusItem = NULL; static uifocusitem_t *focusItem = NULL;
+1 -1
View File
@@ -12,7 +12,7 @@
#include "rpg/rpgcamera.h" #include "rpg/rpgcamera.h"
#include "display/text/text.h" #include "display/text/text.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
void uiTextboxMiniInit(uitextboxmini_t *mini) { void uiTextboxMiniInit(uitextboxmini_t *mini) {
assertNotNull(mini, "Mini textbox cannot be NULL"); assertNotNull(mini, "Mini textbox cannot be NULL");
+1 -1
View File
@@ -7,7 +7,7 @@
#include "uielement.h" #include "uielement.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
#include "engine/engine.h" #include "engine/engine.h"
bool_t uiElementIsNull(const uielement_t *element) { bool_t uiElementIsNull(const uielement_t *element) {
+10 -4
View File
@@ -6,7 +6,7 @@
*/ */
#include "uielementlist.h" #include "uielementlist.h"
#include "ui/frame/uiframe.h" #include "ui/widget/uiframe.h"
#include "ui/debug/uifps.h" #include "ui/debug/uifps.h"
#include "ui/overlay/uifullbox.h" #include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h" #include "ui/overlay/uiloading.h"
@@ -20,6 +20,7 @@
#include "ui/frame/battle/uibattlehud.h" #include "ui/frame/battle/uibattlehud.h"
#include "ui/frame/backpack/uibackpack.h" #include "ui/frame/backpack/uibackpack.h"
#include "ui/frame/uiconfirm.h" #include "ui/frame/uiconfirm.h"
#include "ui/frame/uimodal.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"
@@ -89,6 +90,11 @@ uielement_t UI_ELEMENTS[] = {
.draw = uiConfirmDraw, .draw = uiConfirmDraw,
.dispose = uiConfirmDispose .dispose = uiConfirmDispose
}, },
{
.init = uiModalInit,
.draw = uiModalDraw,
.dispose = uiModalDispose
},
{ {
.init = uiTextboxMainInit, .init = uiTextboxMainInit,
@@ -133,9 +139,9 @@ uielement_t UI_ELEMENTS[] = {
// =========================================================================== // ===========================================================================
// Debug components, disregards everything else. // Debug components, disregards everything else.
// =========================================================================== // ===========================================================================
{ .draw = uiConsoleDraw }, { .init = uiConsoleInit, .draw = uiConsoleDraw },
{ .draw = uiFPSDraw }, { .init = uiFPSInit, .draw = uiFPSDraw },
{ .draw = uiPlayerPosDraw }, { .init = uiPlayerPosInit, .draw = uiPlayerPosDraw },
// =========================================================================== // ===========================================================================
// Null terminator // Null terminator
+1
View File
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
uibutton.c uibutton.c
uicheckbox.c uicheckbox.c
uiframe.c
uilabel.c uilabel.c
uitab.c uitab.c
uislider.c uislider.c
+11 -9
View File
@@ -8,14 +8,15 @@
#include "uibutton.h" #include "uibutton.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/text/text.h"
#include "display/color.h" #include "display/color.h"
void uiButtonInit(uibutton_t *button, const char_t *label) { void uiButtonInit(uibutton_t *button, const char_t *label) {
assertNotNull(button, "Button cannot be NULL"); assertNotNull(button, "Button cannot be NULL");
assertNotNull(label, "Label cannot be NULL"); assertNotNull(label, "Label cannot be NULL");
memoryZero(button, sizeof(uibutton_t)); 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) { bool_t uiButtonIsHighlighted(const uibutton_t *button) {
@@ -29,15 +30,16 @@ void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) {
} }
errorret_t uiButtonDraw( errorret_t uiButtonDraw(
const uibutton_t *button, uibutton_t *button,
const float_t x, const float_t x,
const float_t y const float_t y
) { ) {
assertNotNull(button, "Button cannot be NULL"); assertNotNull(button, "Button cannot be NULL");
errorChain(textDraw(
x, y, button->label, uiLabelSetX(&button->label, x);
button->highlighted ? COLOR_RED : COLOR_WHITE, uiLabelSetY(&button->label, y);
&FONT_DEFAULT
)); return uiLabelRender(
errorOk(); &button->label, button->highlighted ? COLOR_RED : COLOR_WHITE
);
} }
+8 -3
View File
@@ -7,9 +7,13 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "ui/widget/uilabel.h"
#define UI_BUTTON_SPRITES_MAX 32
typedef struct { typedef struct {
const char_t *label; uilabel_t label;
spritebatchsprite_t sprites[UI_BUTTON_SPRITES_MAX];
bool_t highlighted; bool_t highlighted;
} uibutton_t; } uibutton_t;
@@ -17,7 +21,8 @@ typedef struct {
* Initializes a button. * Initializes a button.
* *
* @param button The button to initialize. * @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); 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. * @return Any error that occurs.
*/ */
errorret_t uiButtonDraw( errorret_t uiButtonDraw(
const uibutton_t *button, uibutton_t *button,
const float_t x, const float_t x,
const float_t y const float_t y
); );
+1 -1
View File
@@ -98,7 +98,7 @@ errorret_t uiMenuDraw(
uint8_t row = 0; uint8_t row = 0;
for(uint8_t i = 0; i < menu->itemCount; i++) { 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(item->type == UI_MENU_WIDGET_TYPE_LABEL) {
if(col > 0) { row++; col = 0; } if(col > 0) { row++; col = 0; }