486 lines
14 KiB
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;
|
|
}
|