// dvxDialog.c — Modal dialogs for DV/X GUI #include "dvxDialog.h" #include "dvxWidget.h" #include "widgets/widgetInternal.h" #include // ============================================================ // 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; }