DVX_GUI/dvx/dvxDialog.c

486 lines
14 KiB
C

// dvxDialog.c — Modal dialogs for DV/X GUI
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "widgets/widgetInternal.h"
#include <string.h>
// ============================================================
// Constants
// ============================================================
#define MSG_MAX_WIDTH 320
#define MSG_PADDING 8
#define ICON_AREA_WIDTH 40
#define BUTTON_WIDTH 80
#define BUTTON_HEIGHT 24
#define BUTTON_GAP 8
// ============================================================
// Prototypes
// ============================================================
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color);
static void onButtonClick(WidgetT *w);
static void onMsgBoxClose(WindowT *win);
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW);
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg);
// ============================================================
// Message box state (one active at a time)
// ============================================================
typedef struct {
AppContextT *ctx;
int32_t result;
bool done;
const char *message;
int32_t iconType;
int32_t textX;
int32_t textY;
int32_t textMaxW;
int32_t msgAreaH;
} MsgBoxStateT;
static MsgBoxStateT sMsgBox;
// ============================================================
// drawIconGlyph — draw a simple icon shape
// ============================================================
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color) {
if (iconType == MB_ICONINFO) {
// Circle outline with 'i'
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
rectFill(d, ops, x + 11, y + 6, 2, 2, color);
rectFill(d, ops, x + 11, y + 10, 2, 8, color);
} else if (iconType == MB_ICONWARNING) {
// Triangle outline with '!'
for (int32_t row = 0; row < 24; row++) {
int32_t halfW = row / 2;
int32_t lx = 12 - halfW;
int32_t rx = 12 + halfW;
rectFill(d, ops, x + lx, y + row, 1, 1, color);
rectFill(d, ops, x + rx, y + row, 1, 1, color);
if (row == 23) {
drawHLine(d, ops, x + lx, y + row, rx - lx + 1, color);
}
}
rectFill(d, ops, x + 11, y + 8, 2, 9, color);
rectFill(d, ops, x + 11, y + 19, 2, 2, color);
} else if (iconType == MB_ICONERROR) {
// Circle outline with X
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
for (int32_t i = 0; i < 12; i++) {
rectFill(d, ops, x + 6 + i, y + 6 + i, 2, 2, color);
rectFill(d, ops, x + 18 - i, y + 6 + i, 2, 2, color);
}
} else if (iconType == MB_ICONQUESTION) {
// Circle outline with '?'
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
rectFill(d, ops, x + 9, y + 6, 6, 2, color);
rectFill(d, ops, x + 13, y + 8, 2, 4, color);
rectFill(d, ops, x + 11, y + 12, 2, 3, color);
rectFill(d, ops, x + 11, y + 17, 2, 2, color);
}
}
// ============================================================
// dvxMessageBox
// ============================================================
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
int32_t btnFlags = flags & 0x000F;
int32_t iconFlags = flags & 0x00F0;
// Determine button labels and IDs
const char *btnLabels[3];
int32_t btnIds[3];
int32_t btnCount = 0;
switch (btnFlags) {
case MB_OK:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnCount = 1;
break;
case MB_OKCANCEL:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnLabels[1] = "&Cancel";
btnIds[1] = ID_CANCEL;
btnCount = 2;
break;
case MB_YESNO:
btnLabels[0] = "&Yes";
btnIds[0] = ID_YES;
btnLabels[1] = "&No";
btnIds[1] = ID_NO;
btnCount = 2;
break;
case MB_YESNOCANCEL:
btnLabels[0] = "&Yes";
btnIds[0] = ID_YES;
btnLabels[1] = "&No";
btnIds[1] = ID_NO;
btnLabels[2] = "&Cancel";
btnIds[2] = ID_CANCEL;
btnCount = 3;
break;
case MB_RETRYCANCEL:
btnLabels[0] = "&Retry";
btnIds[0] = ID_RETRY;
btnLabels[1] = "&Cancel";
btnIds[1] = ID_CANCEL;
btnCount = 2;
break;
default:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnCount = 1;
break;
}
// Calculate message text dimensions
bool hasIcon = (iconFlags != 0);
int32_t textMaxW = MSG_MAX_WIDTH - MSG_PADDING * 2 - (hasIcon ? ICON_AREA_WIDTH : 0);
int32_t textH = wordWrapHeight(&ctx->font, message, textMaxW);
// Calculate content area sizes
int32_t msgAreaH = textH + MSG_PADDING * 2;
int32_t iconAreaH = hasIcon ? (24 + MSG_PADDING * 2) : 0;
if (msgAreaH < iconAreaH) {
msgAreaH = iconAreaH;
}
int32_t buttonsW = btnCount * BUTTON_WIDTH + (btnCount - 1) * BUTTON_GAP;
int32_t contentW = MSG_MAX_WIDTH;
if (buttonsW + MSG_PADDING * 2 > contentW) {
contentW = buttonsW + MSG_PADDING * 2;
}
int32_t contentH = msgAreaH + BUTTON_HEIGHT + MSG_PADDING * 3;
// Create the dialog window (non-resizable)
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE;
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title, winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) {
return ID_CANCEL;
}
win->modal = true;
// Set up state
sMsgBox.ctx = ctx;
sMsgBox.result = ID_CANCEL;
sMsgBox.done = false;
sMsgBox.message = message;
sMsgBox.iconType = iconFlags;
sMsgBox.textX = MSG_PADDING + (hasIcon ? ICON_AREA_WIDTH : 0);
sMsgBox.textY = MSG_PADDING;
sMsgBox.textMaxW = textMaxW;
sMsgBox.msgAreaH = msgAreaH;
// Create button widgets using wgtInitWindow for proper root setup
// (sets onPaint, onMouse, onKey, onResize, userData on root)
WidgetT *root = wgtInitWindow(ctx, win);
// Override onPaint with our custom handler, set window-level state
win->userData = &sMsgBox;
win->onPaint = onMsgBoxPaint;
win->onClose = onMsgBoxClose;
win->maxW = win->w;
win->maxH = win->h;
if (root) {
// Spacer for message area (text/icon drawn by onPaint)
WidgetT *msgSpacer = wgtSpacer(root);
if (msgSpacer) {
msgSpacer->minH = wgtPixels(msgAreaH);
}
// Button row centered
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignCenterE;
for (int32_t i = 0; i < btnCount; i++) {
WidgetT *btn = wgtButton(btnRow, btnLabels[i]);
if (btn) {
btn->minW = wgtPixels(BUTTON_WIDTH);
btn->minH = wgtPixels(BUTTON_HEIGHT);
btn->userData = (void *)(intptr_t)btnIds[i];
btn->onClick = onButtonClick;
}
}
}
// Bottom padding
WidgetT *bottomSpacer = wgtSpacer(root);
if (bottomSpacer) {
bottomSpacer->minH = wgtPixels(MSG_PADDING);
}
}
// Initial paint (window is already correctly sized, don't call dvxFitWindow)
RectT fullRect = { 0, 0, win->contentW, win->contentH };
win->onPaint(win, &fullRect);
// Set as modal
ctx->modalWindow = win;
// Nested event loop
while (!sMsgBox.done && ctx->running) {
dvxUpdate(ctx);
}
// Clean up
ctx->modalWindow = NULL;
dvxDestroyWindow(ctx, win);
return sMsgBox.result;
}
// ============================================================
// onButtonClick
// ============================================================
static void onButtonClick(WidgetT *w) {
sMsgBox.result = (int32_t)(intptr_t)w->userData;
sMsgBox.done = true;
}
// ============================================================
// onMsgBoxClose
// ============================================================
static void onMsgBoxClose(WindowT *win) {
(void)win;
sMsgBox.result = ID_CANCEL;
sMsgBox.done = true;
}
// ============================================================
// onMsgBoxPaint — custom paint: background + text/icon + widgets
// ============================================================
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
(void)dirtyArea;
MsgBoxStateT *state = (MsgBoxStateT *)win->userData;
AppContextT *ctx = state->ctx;
// Set up display context pointing at content buffer
DisplayT cd = ctx->display;
cd.lfb = win->contentBuf;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Fill background with window face color (not content bg — dialog style)
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.windowFace);
// Draw word-wrapped message text
wordWrapDraw(&cd, &ctx->blitOps, &ctx->font, state->textX, state->textY, state->message, state->textMaxW, ctx->colors.contentFg, ctx->colors.windowFace);
// Draw icon
if (state->iconType != 0) {
drawIconGlyph(&cd, &ctx->blitOps, MSG_PADDING, MSG_PADDING, state->iconType, ctx->colors.contentFg);
}
// Draw separator line above buttons
drawHLine(&cd, &ctx->blitOps, 0, state->msgAreaH, win->contentW, ctx->colors.windowShadow);
drawHLine(&cd, &ctx->blitOps, 0, state->msgAreaH + 1, win->contentW, ctx->colors.windowHighlight);
// Paint widget tree (buttons) on top
if (win->widgetRoot) {
WidgetT *root = win->widgetRoot;
// Layout widgets
widgetCalcMinSizeTree(root, &ctx->font);
root->x = 0;
root->y = 0;
root->w = win->contentW;
root->h = win->contentH;
widgetLayoutChildren(root, &ctx->font);
// Paint widgets
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
}
}
// ============================================================
// wordWrapDraw — draw word-wrapped text
// ============================================================
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t curY = y;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
// Skip leading spaces
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
// Handle explicit newlines
if (*text == '\n') {
curY += lineH;
text++;
continue;
}
// Find how many characters fit on this line
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
// If we didn't reach end and didn't hit newline, wrap at word boundary
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
drawTextN(d, ops, font, x, curY, text, lineLen, fg, bg, true);
curY += lineH;
text += lineLen;
}
}
// ============================================================
// wordWrapHeight — compute height of word-wrapped text
// ============================================================
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t lines = 0;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
if (*text == '\n') {
lines++;
text++;
continue;
}
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
lines++;
text += lineLen;
}
if (lines == 0) {
lines = 1;
}
return lines * lineH;
}