// dvxhelp.c -- DVX Help Viewer application // // A callback-only DXE app that displays .hlp help files with a TreeView // TOC sidebar and scrollable content area. Supports headings, text, // links, images, lists, horizontal rules, notes, and code blocks. // // The content area is rebuilt per-topic by reading records from the .hlp // file and creating private widget types (HelpText, HelpHeading, HelpLink) // registered via wgtRegisterClass at startup. // // Navigation history supports back/forward with a fixed-size circular stack. #include "hlpformat.h" #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxWidgetPlugin.h" #include "dvxWm.h" #include "dvxDraw.h" #include "dvxVideo.h" #include "dvxPlatform.h" #include "shellApp.h" #include "widgetBox.h" #include "widgetButton.h" #include "widgetSplitter.h" #include "widgetTreeView.h" #include "widgetScrollPane.h" #include "widgetToolbar.h" #include "widgetImage.h" #include #include #include #include #include #include "dvxMem.h" // ============================================================ // Constants // ============================================================ #define HELP_WIN_W 560 #define HELP_WIN_H 400 #define HELP_CASCADE_OFFSET 16 #define HELP_SPLITTER_POS 160 #define HELP_TOC_MIN_W 100 #define HELP_CONTENT_PAD 6 #define HELP_HEADING1_PAD 4 #define HELP_HEADING2_PAD 2 #define HELP_LINK_PAD_X 2 #define HELP_LINK_PAD_Y 1 #define HELP_NOTE_PAD 6 #define HELP_NOTE_PREFIX_W 4 #define HELP_CODE_PAD 4 #define HELP_LIST_INDENT 12 #define HELP_LIST_BULLET_W 8 #define MAX_HISTORY 64 #define MAX_SEARCH_BUF 128 #define CMD_BACK 100 #define CMD_FORWARD 101 #define CMD_INDEX 102 #define CMD_SEARCH 103 // ============================================================ // Module state // ============================================================ static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; static WindowT *sWin = NULL; static WidgetT *sTocTree = NULL; static WidgetT *sContentScroll = NULL; static WidgetT *sContentBox = NULL; static WidgetT *sBtnBack = NULL; static WidgetT *sBtnForward = NULL; // .hlp file data static FILE *sHlpFile = NULL; static HlpHeaderT sHeader; static HlpTopicDirT *sTopicDir = NULL; static char *sStringTable = NULL; static HlpTocEntryT *sTocEntries = NULL; static HlpIndexEntryT *sIndexEntries = NULL; // Navigation history static int32_t sHistory[MAX_HISTORY]; static int32_t sHistoryPos = -1; static int32_t sHistoryCount = 0; static int32_t sCurrentTopic = -1; // Custom widget type IDs static int32_t sHelpTextTypeId = -1; static int32_t sHelpHeadingTypeId = -1; static int32_t sHelpLinkTypeId = -1; static int32_t sHelpNoteTypeId = -1; static int32_t sHelpCodeTypeId = -1; static int32_t sHelpRuleTypeId = -1; static int32_t sHelpListItemTypeId = -1; // ============================================================ // Private widget data types // ============================================================ typedef struct { char *text; int32_t lineCount; } HelpTextDataT; typedef struct { char *text; int32_t level; } HelpHeadingDataT; typedef struct { char *displayText; char *targetTopicId; } HelpLinkDataT; typedef struct { char *text; int32_t lineCount; uint8_t noteType; } HelpNoteDataT; typedef struct { char *text; int32_t lineCount; } HelpCodeDataT; typedef struct { char *text; } HelpListItemDataT; // ============================================================ // Prototypes // ============================================================ int32_t appMain(DxeAppContextT *ctx); static void buildContentWidgets(void); static void closeHelpFile(void); static int32_t countLines(const char *text); static void displayRecord(const HlpRecordHdrT *hdr, const char *payload); static int32_t findTopicByIdStr(const char *topicId); static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpCodeDestroy(WidgetT *w); static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpHeadingDestroy(WidgetT *w); static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpLinkDestroy(WidgetT *w); static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void helpListItemCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpListItemDestroy(WidgetT *w); static void helpListItemPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpNoteDestroy(WidgetT *w); static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font); static void helpTextDestroy(WidgetT *w); static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void historyPush(int32_t topicIdx); static const char *hlpString(uint32_t offset); static int32_t maxLineWidth(const BitmapFontT *font, const char *text); static void navigateBack(void); static void navigateForward(void); static void navigateToTopic(int32_t topicIdx); static void onClose(WindowT *win); static void onIndex(WidgetT *w); static void onMenu(WindowT *win, int32_t menuId); static void onNavBack(WidgetT *w); static void onNavForward(WidgetT *w); static void onSearch(WidgetT *w); static void onTocChange(WidgetT *w); static bool openHelpFile(const char *path); static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg); static void populateToc(void); static void registerWidgetClasses(void); static void updateNavButtons(void); // ============================================================ // App descriptor // ============================================================ AppDescriptorT appDescriptor = { .name = "DVX Help", .hasMainLoop = false, .multiInstance = true, .stackSize = SHELL_STACK_DEFAULT, .priority = TS_PRIORITY_NORMAL }; // ============================================================ // Utility helpers // ============================================================ static int32_t countLines(const char *text) { int32_t count = 1; for (const char *p = text; *p; p++) { if (*p == '\n') { count++; } } return count; } static int32_t maxLineWidth(const BitmapFontT *font, const char *text) { int32_t maxW = 0; int32_t lineLen = 0; for (const char *p = text; ; p++) { if (*p == '\n' || *p == '\0') { int32_t w = lineLen * font->charWidth; if (w > maxW) { maxW = w; } lineLen = 0; if (*p == '\0') { break; } } else { lineLen++; } } return maxW; } static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg) { const char *line = text; for (int32_t i = 0; i < lineCount; i++) { const char *end = strchr(line, '\n'); int32_t len = end ? (int32_t)(end - line) : (int32_t)strlen(line); if (len > 0) { drawTextN(d, ops, font, x, y, line, len, fg, 0, false); } y += font->charHeight; line = end ? end + 1 : line + len; } } // ============================================================ // HelpText widget class // ============================================================ static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpTextDataT *td = (HelpTextDataT *)w->data; w->calcMinW = maxLineWidth(font, td->text) + HELP_CONTENT_PAD * 2; w->calcMinH = td->lineCount * font->charHeight; } static void helpTextPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpTextDataT *td = (HelpTextDataT *)w->data; paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, td->text, td->lineCount, colors->contentFg); } static void helpTextDestroy(WidgetT *w) { HelpTextDataT *td = (HelpTextDataT *)w->data; if (td) { dvxFree(td->text); dvxFree(td); w->data = NULL; } } // ============================================================ // HelpHeading widget class // ============================================================ static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; int32_t textW = textWidth(font, hd->text); int32_t textH = font->charHeight; w->calcMinW = textW + HELP_CONTENT_PAD * 2; if (hd->level == 1) { // Full bar with padding above and below w->calcMinH = textH + HELP_HEADING1_PAD * 2 + 2; } else if (hd->level == 2) { // Text with underline w->calcMinH = textH + HELP_HEADING2_PAD + 2; } else { // Plain bold text w->calcMinH = textH + 2; } } static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; int32_t textLen = (int32_t)strlen(hd->text); if (hd->level == 1) { // Filled bar with title bar colors rectFill(d, ops, w->x, w->y, w->w, w->h, colors->activeTitleBg); int32_t textY = w->y + HELP_HEADING1_PAD; drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->activeTitleFg, 0, false); } else if (hd->level == 2) { // Text with underline int32_t textY = w->y; drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false); int32_t lineY = textY + font->charHeight + 1; drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); } else { // Plain text drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, hd->text, textLen, colors->contentFg, 0, false); } } static void helpHeadingDestroy(WidgetT *w) { HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; if (hd) { dvxFree(hd->text); dvxFree(hd); w->data = NULL; } } // ============================================================ // HelpLink widget class // ============================================================ static void helpLinkCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpLinkDataT *ld = (HelpLinkDataT *)w->data; w->calcMinW = textWidth(font, ld->displayText) + HELP_CONTENT_PAD * 2 + HELP_LINK_PAD_X * 2; w->calcMinH = font->charHeight + HELP_LINK_PAD_Y * 2; } static void helpLinkPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpLinkDataT *ld = (HelpLinkDataT *)w->data; int32_t textLen = (int32_t)strlen(ld->displayText); int32_t x = w->x + HELP_CONTENT_PAD + HELP_LINK_PAD_X; int32_t y = w->y + HELP_LINK_PAD_Y; // Use active title color as link color uint32_t linkColor = colors->activeTitleBg; drawTextN(d, ops, font, x, y, ld->displayText, textLen, linkColor, 0, false); // Underline int32_t lineY = y + font->charHeight; int32_t lineW = textLen * font->charWidth; drawHLine(d, ops, x, lineY, lineW, linkColor); } static void helpLinkOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vx; (void)vy; HelpLinkDataT *ld = (HelpLinkDataT *)w->data; if (!ld || !ld->targetTopicId) { return; } int32_t topicIdx = findTopicByIdStr(ld->targetTopicId); if (topicIdx >= 0) { navigateToTopic(topicIdx); } } static void helpLinkDestroy(WidgetT *w) { HelpLinkDataT *ld = (HelpLinkDataT *)w->data; if (ld) { dvxFree(ld->displayText); dvxFree(ld->targetTopicId); dvxFree(ld); w->data = NULL; } } // ============================================================ // HelpNote widget class // ============================================================ static void helpNoteCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpNoteDataT *nd = (HelpNoteDataT *)w->data; w->calcMinW = maxLineWidth(font, nd->text) + HELP_CONTENT_PAD * 2 + HELP_NOTE_PAD * 2 + HELP_NOTE_PREFIX_W; w->calcMinH = nd->lineCount * font->charHeight + HELP_NOTE_PAD * 2; } static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpNoteDataT *nd = (HelpNoteDataT *)w->data; // Left accent bar color varies by note type; background is always windowFace uint32_t barColor; if (nd->noteType == HLP_NOTE_WARNING) { barColor = colors->activeTitleBg; } else if (nd->noteType == HLP_NOTE_TIP) { barColor = colors->menuHighlightBg; } else { barColor = colors->windowShadow; } int32_t boxX = w->x + HELP_CONTENT_PAD; int32_t boxW = w->w - HELP_CONTENT_PAD * 2; rectFill(d, ops, boxX, w->y, boxW, w->h, colors->windowFace); rectFill(d, ops, boxX, w->y, HELP_NOTE_PREFIX_W, w->h, barColor); int32_t textX = boxX + HELP_NOTE_PREFIX_W + HELP_NOTE_PAD; paintLines(d, ops, font, textX, w->y + HELP_NOTE_PAD, nd->text, nd->lineCount, colors->contentFg); } static void helpNoteDestroy(WidgetT *w) { HelpNoteDataT *nd = (HelpNoteDataT *)w->data; if (nd) { dvxFree(nd->text); dvxFree(nd); w->data = NULL; } } // ============================================================ // HelpCode widget class // ============================================================ static void helpCodeCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpCodeDataT *cd = (HelpCodeDataT *)w->data; w->calcMinW = maxLineWidth(font, cd->text) + HELP_CONTENT_PAD * 2 + HELP_CODE_PAD * 2; w->calcMinH = cd->lineCount * font->charHeight + HELP_CODE_PAD * 2; } static void helpCodePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpCodeDataT *cd = (HelpCodeDataT *)w->data; int32_t boxX = w->x + HELP_CONTENT_PAD; int32_t boxW = w->w - HELP_CONTENT_PAD * 2; // Sunken background rectFill(d, ops, boxX, w->y, boxW, w->h, colors->contentBg); drawHLine(d, ops, boxX, w->y, boxW, colors->windowShadow); drawHLine(d, ops, boxX, w->y + w->h - 1, boxW, colors->windowHighlight); paintLines(d, ops, font, boxX + HELP_CODE_PAD, w->y + HELP_CODE_PAD, cd->text, cd->lineCount, colors->contentFg); } static void helpCodeDestroy(WidgetT *w) { HelpCodeDataT *cd = (HelpCodeDataT *)w->data; if (cd) { dvxFree(cd->text); dvxFree(cd); w->data = NULL; } } // ============================================================ // HelpRule widget class // ============================================================ static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; w->calcMinW = 0; w->calcMinH = 5; } static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { (void)font; int32_t lineY = w->y + 2; drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow); drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY + 1, w->w - HELP_CONTENT_PAD * 2, colors->windowHighlight); } // ============================================================ // HelpListItem widget class // ============================================================ static void helpListItemCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpListItemDataT *ld = (HelpListItemDataT *)w->data; w->calcMinW = textWidth(font, ld->text) + HELP_CONTENT_PAD * 2 + HELP_LIST_INDENT; w->calcMinH = font->charHeight; } static void helpListItemPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpListItemDataT *ld = (HelpListItemDataT *)w->data; int32_t textLen = (int32_t)strlen(ld->text); int32_t bulletX = w->x + HELP_CONTENT_PAD + HELP_LIST_INDENT - HELP_LIST_BULLET_W; int32_t textX = w->x + HELP_CONTENT_PAD + HELP_LIST_INDENT; int32_t textY = w->y; // Bullet character drawTextN(d, ops, font, bulletX, textY, "\x07", 1, colors->contentFg, 0, false); if (textLen > 0) { drawTextN(d, ops, font, textX, textY, ld->text, textLen, colors->contentFg, 0, false); } } static void helpListItemDestroy(WidgetT *w) { HelpListItemDataT *ld = (HelpListItemDataT *)w->data; if (ld) { dvxFree(ld->text); dvxFree(ld); w->data = NULL; } } // ============================================================ // Widget class registration // ============================================================ static const WidgetClassT sClassHelpText = { .version = WGT_CLASS_VERSION, .flags = 0, .handlers = { [WGT_METHOD_PAINT] = (void *)helpTextPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpTextCalcMinSize, [WGT_METHOD_DESTROY] = (void *)helpTextDestroy, } }; static const WidgetClassT sClassHelpHeading = { .version = WGT_CLASS_VERSION, .flags = 0, .handlers = { [WGT_METHOD_PAINT] = (void *)helpHeadingPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpHeadingCalcMinSize, [WGT_METHOD_DESTROY] = (void *)helpHeadingDestroy, } }; static const WidgetClassT sClassHelpLink = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE, .handlers = { [WGT_METHOD_PAINT] = (void *)helpLinkPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpLinkCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)helpLinkOnMouse, [WGT_METHOD_DESTROY] = (void *)helpLinkDestroy, } }; static const WidgetClassT sClassHelpNote = { .version = WGT_CLASS_VERSION, .flags = 0, .handlers = { [WGT_METHOD_PAINT] = (void *)helpNotePaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpNoteCalcMinSize, [WGT_METHOD_DESTROY] = (void *)helpNoteDestroy, } }; static const WidgetClassT sClassHelpCode = { .version = WGT_CLASS_VERSION, .flags = 0, .handlers = { [WGT_METHOD_PAINT] = (void *)helpCodePaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpCodeCalcMinSize, [WGT_METHOD_DESTROY] = (void *)helpCodeDestroy, } }; static const WidgetClassT sClassHelpRule = { .version = WGT_CLASS_VERSION, .flags = 0, .handlers = { [WGT_METHOD_PAINT] = (void *)helpRulePaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpRuleCalcMinSize, } }; static const WidgetClassT sClassHelpListItem = { .version = WGT_CLASS_VERSION, .flags = 0, .handlers = { [WGT_METHOD_PAINT] = (void *)helpListItemPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpListItemCalcMinSize, [WGT_METHOD_DESTROY] = (void *)helpListItemDestroy, } }; static void registerWidgetClasses(void) { sHelpTextTypeId = wgtRegisterClass(&sClassHelpText); sHelpHeadingTypeId = wgtRegisterClass(&sClassHelpHeading); sHelpLinkTypeId = wgtRegisterClass(&sClassHelpLink); sHelpNoteTypeId = wgtRegisterClass(&sClassHelpNote); sHelpCodeTypeId = wgtRegisterClass(&sClassHelpCode); sHelpRuleTypeId = wgtRegisterClass(&sClassHelpRule); sHelpListItemTypeId = wgtRegisterClass(&sClassHelpListItem); } // ============================================================ // String table lookup // ============================================================ static const char *hlpString(uint32_t offset) { if (!sStringTable || offset >= sHeader.stringTableSize) { return ""; } return sStringTable + offset; } // ============================================================ // Topic lookup by ID string // ============================================================ static int32_t findTopicByIdStr(const char *topicId) { if (!topicId || !sTopicDir) { return -1; } // Linear search -- topic directory is typically small for (uint32_t i = 0; i < sHeader.topicCount; i++) { const char *id = hlpString(sTopicDir[i].topicIdStr); if (strcmp(id, topicId) == 0) { return (int32_t)i; } } return -1; } // ============================================================ // History management // ============================================================ static void historyPush(int32_t topicIdx) { // Truncate forward history when navigating to a new topic sHistoryPos++; if (sHistoryPos >= MAX_HISTORY) { // Shift history down to make room memmove(&sHistory[0], &sHistory[1], (MAX_HISTORY - 1) * sizeof(int32_t)); sHistoryPos = MAX_HISTORY - 1; } sHistory[sHistoryPos] = topicIdx; sHistoryCount = sHistoryPos + 1; } static void navigateBack(void) { if (sHistoryPos <= 0) { return; } sHistoryPos--; int32_t topicIdx = sHistory[sHistoryPos]; sCurrentTopic = topicIdx; buildContentWidgets(); updateNavButtons(); } static void navigateForward(void) { if (sHistoryPos >= sHistoryCount - 1) { return; } sHistoryPos++; int32_t topicIdx = sHistory[sHistoryPos]; sCurrentTopic = topicIdx; buildContentWidgets(); updateNavButtons(); } static void navigateToTopic(int32_t topicIdx) { if (topicIdx < 0 || (uint32_t)topicIdx >= sHeader.topicCount) { return; } if (topicIdx == sCurrentTopic) { return; } historyPush(topicIdx); sCurrentTopic = topicIdx; buildContentWidgets(); updateNavButtons(); // Update window title const char *title = hlpString(sTopicDir[topicIdx].titleStr); char winTitle[300]; snprintf(winTitle, sizeof(winTitle), "%s - DVX Help", title); dvxSetTitle(sAc, sWin, winTitle); } static void updateNavButtons(void) { if (sBtnBack) { wgtSetEnabled(sBtnBack, sHistoryPos > 0); } if (sBtnForward) { wgtSetEnabled(sBtnForward, sHistoryPos < sHistoryCount - 1); } } // ============================================================ // Content display // ============================================================ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) { if (!sContentBox) { return; } switch (hdr->type) { case HLP_REC_TEXT: { WidgetT *w = widgetAlloc(sContentBox, sHelpTextTypeId); if (w) { HelpTextDataT *td = (HelpTextDataT *)dvxCalloc(1, sizeof(HelpTextDataT)); td->text = dvxStrdup(payload); td->lineCount = countLines(td->text); w->data = td; } break; } case HLP_REC_HEADING1: case HLP_REC_HEADING2: case HLP_REC_HEADING3: { WidgetT *w = widgetAlloc(sContentBox, sHelpHeadingTypeId); if (w) { HelpHeadingDataT *hd = (HelpHeadingDataT *)dvxCalloc(1, sizeof(HelpHeadingDataT)); hd->text = dvxStrdup(payload); hd->level = hdr->type - HLP_REC_HEADING1 + 1; w->data = hd; } break; } case HLP_REC_LINK: { // Payload: display text \0 target topic ID \0 const char *displayText = payload; const char *targetTopicId = payload + strlen(payload) + 1; WidgetT *w = widgetAlloc(sContentBox, sHelpLinkTypeId); if (w) { HelpLinkDataT *ld = (HelpLinkDataT *)dvxCalloc(1, sizeof(HelpLinkDataT)); ld->displayText = dvxStrdup(displayText); ld->targetTopicId = dvxStrdup(targetTopicId); w->data = ld; } break; } case HLP_REC_IMAGE: { if (hdr->length >= sizeof(HlpImageRefT)) { const HlpImageRefT *imgRef = (const HlpImageRefT *)payload; uint32_t absOffset = sHeader.imagePoolOffset + imgRef->imageOffset; // Read BMP data from file uint8_t *bmpData = (uint8_t *)dvxMalloc(imgRef->imageSize); if (bmpData) { fseek(sHlpFile, absOffset, SEEK_SET); size_t bytesRead = fread(bmpData, 1, imgRef->imageSize, sHlpFile); if (bytesRead == imgRef->imageSize) { int32_t imgW = 0; int32_t imgH = 0; int32_t imgP = 0; uint8_t *pixels = dvxLoadImageFromMemory(sAc, bmpData, (int32_t)imgRef->imageSize, &imgW, &imgH, &imgP); if (pixels) { wgtImage(sContentBox, pixels, imgW, imgH, imgP); // wgtImage takes ownership of the pixel data } } dvxFree(bmpData); } } break; } case HLP_REC_LIST_ITEM: { WidgetT *w = widgetAlloc(sContentBox, sHelpListItemTypeId); if (w) { HelpListItemDataT *ld = (HelpListItemDataT *)dvxCalloc(1, sizeof(HelpListItemDataT)); ld->text = dvxStrdup(payload); w->data = ld; } break; } case HLP_REC_HRULE: { widgetAlloc(sContentBox, sHelpRuleTypeId); break; } case HLP_REC_NOTE: { WidgetT *w = widgetAlloc(sContentBox, sHelpNoteTypeId); if (w) { HelpNoteDataT *nd = (HelpNoteDataT *)dvxCalloc(1, sizeof(HelpNoteDataT)); nd->text = dvxStrdup(payload); nd->lineCount = countLines(nd->text); nd->noteType = hdr->flags; w->data = nd; } break; } case HLP_REC_CODE: { WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId); if (w) { HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT)); cd->text = dvxStrdup(payload); cd->lineCount = countLines(cd->text); w->data = cd; } break; } case HLP_REC_TABLE: { // Render tables as preformatted text WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId); if (w) { HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT)); cd->text = dvxStrdup(payload); cd->lineCount = countLines(cd->text); w->data = cd; } break; } } } static void buildContentWidgets(void) { if (!sContentBox || sCurrentTopic < 0) { return; } // Destroy old content widgetDestroyChildren(sContentBox); // Read topic content from file HlpTopicDirT *topic = &sTopicDir[sCurrentTopic]; fseek(sHlpFile, topic->contentOffset, SEEK_SET); uint32_t bytesRead = 0; while (bytesRead < topic->contentSize) { HlpRecordHdrT recHdr; if (fread(&recHdr, sizeof(recHdr), 1, sHlpFile) != 1) { break; } bytesRead += sizeof(recHdr); if (recHdr.type == HLP_REC_END) { break; } if (recHdr.length == 0) { displayRecord(&recHdr, ""); continue; } // Read payload char *payload = (char *)dvxMalloc(recHdr.length + 1); if (!payload) { break; } if (fread(payload, 1, recHdr.length, sHlpFile) != recHdr.length) { dvxFree(payload); break; } payload[recHdr.length] = '\0'; bytesRead += recHdr.length; displayRecord(&recHdr, payload); dvxFree(payload); } // Scroll to top by scrolling the first child into view if (sContentScroll && sContentBox->firstChild) { wgtScrollPaneScrollToChild(sContentScroll, sContentBox->firstChild); } wgtInvalidate(sContentBox); } // ============================================================ // TOC population // ============================================================ static void populateToc(void) { if (!sTocTree || !sTocEntries) { return; } // Track parent nodes at each depth level #define MAX_TOC_DEPTH 16 WidgetT *parents[MAX_TOC_DEPTH]; for (int32_t i = 0; i < MAX_TOC_DEPTH; i++) { parents[i] = sTocTree; } for (uint32_t i = 0; i < sHeader.tocCount; i++) { HlpTocEntryT *entry = &sTocEntries[i]; const char *title = hlpString(entry->titleStr); int32_t depth = entry->depth; if (depth >= MAX_TOC_DEPTH) { depth = MAX_TOC_DEPTH - 1; } WidgetT *parent = parents[depth]; WidgetT *item = wgtTreeItem(parent, title); if (item) { // Store topic index in userData for selection callback item->userData = (void *)(intptr_t)entry->topicIdx; // Update parent for next deeper level if (depth + 1 < MAX_TOC_DEPTH) { parents[depth + 1] = item; } // Expand by default if flagged if (entry->flags & HLP_TOC_EXPANDED) { wgtTreeItemSetExpanded(item, true); } } } } // ============================================================ // File I/O // ============================================================ static bool openHelpFile(const char *path) { sHlpFile = fopen(path, "rb"); if (!sHlpFile) { return false; } // Read header from end of file fseek(sHlpFile, -(long)sizeof(HlpHeaderT), SEEK_END); if (fread(&sHeader, sizeof(HlpHeaderT), 1, sHlpFile) != 1) { fclose(sHlpFile); sHlpFile = NULL; return false; } // Validate magic if (sHeader.magic != HLP_MAGIC) { fclose(sHlpFile); sHlpFile = NULL; return false; } // Validate version if (sHeader.version != HLP_VERSION) { fclose(sHlpFile); sHlpFile = NULL; return false; } // Read topic directory sTopicDir = (HlpTopicDirT *)dvxMalloc(sHeader.topicCount * sizeof(HlpTopicDirT)); if (!sTopicDir) { closeHelpFile(); return false; } fseek(sHlpFile, sHeader.topicDirOffset, SEEK_SET); if (fread(sTopicDir, sizeof(HlpTopicDirT), sHeader.topicCount, sHlpFile) != sHeader.topicCount) { closeHelpFile(); return false; } // Read string table sStringTable = (char *)dvxMalloc(sHeader.stringTableSize); if (!sStringTable) { closeHelpFile(); return false; } fseek(sHlpFile, sHeader.stringTableOffset, SEEK_SET); if (fread(sStringTable, 1, sHeader.stringTableSize, sHlpFile) != sHeader.stringTableSize) { closeHelpFile(); return false; } // Read TOC entries if (sHeader.tocCount > 0) { sTocEntries = (HlpTocEntryT *)dvxMalloc(sHeader.tocCount * sizeof(HlpTocEntryT)); if (!sTocEntries) { closeHelpFile(); return false; } fseek(sHlpFile, sHeader.tocOffset, SEEK_SET); if (fread(sTocEntries, sizeof(HlpTocEntryT), sHeader.tocCount, sHlpFile) != sHeader.tocCount) { closeHelpFile(); return false; } } // Read keyword index if (sHeader.indexCount > 0) { sIndexEntries = (HlpIndexEntryT *)dvxMalloc(sHeader.indexCount * sizeof(HlpIndexEntryT)); if (!sIndexEntries) { closeHelpFile(); return false; } fseek(sHlpFile, sHeader.indexOffset, SEEK_SET); if (fread(sIndexEntries, sizeof(HlpIndexEntryT), sHeader.indexCount, sHlpFile) != sHeader.indexCount) { closeHelpFile(); return false; } } return true; } static void closeHelpFile(void) { if (sHlpFile) { fclose(sHlpFile); sHlpFile = NULL; } dvxFree(sTopicDir); sTopicDir = NULL; dvxFree(sStringTable); sStringTable = NULL; dvxFree(sTocEntries); sTocEntries = NULL; dvxFree(sIndexEntries); sIndexEntries = NULL; } // ============================================================ // Callbacks // ============================================================ static void onClose(WindowT *win) { closeHelpFile(); dvxDestroyWindow(sAc, win); sWin = NULL; sTocTree = NULL; sContentScroll = NULL; sContentBox = NULL; sBtnBack = NULL; sBtnForward = NULL; } static void onIndex(WidgetT *w) { (void)w; if (!sIndexEntries || sHeader.indexCount == 0) { dvxMessageBox(sAc, "DVX Help", "No index available.", MB_OK | MB_ICONINFO); return; } // Build list of keyword strings const char **items = (const char **)dvxMalloc(sHeader.indexCount * sizeof(const char *)); if (!items) { return; } for (uint32_t i = 0; i < sHeader.indexCount; i++) { items[i] = hlpString(sIndexEntries[i].keywordStr); } int32_t selected = -1; bool ok = dvxChoiceDialog(sAc, "Index", "Select a topic:", items, (int32_t)sHeader.indexCount, 0, &selected); if (ok && selected >= 0 && (uint32_t)selected < sHeader.indexCount) { int32_t topicIdx = (int32_t)sIndexEntries[selected].topicIdx; navigateToTopic(topicIdx); } dvxFree(items); } static void onMenu(WindowT *win, int32_t menuId) { (void)win; switch (menuId) { case CMD_BACK: navigateBack(); break; case CMD_FORWARD: navigateForward(); break; case CMD_INDEX: onIndex(NULL); break; case CMD_SEARCH: onSearch(NULL); break; } } static void onNavBack(WidgetT *w) { (void)w; navigateBack(); } static void onNavForward(WidgetT *w) { (void)w; navigateForward(); } static void onSearch(WidgetT *w) { (void)w; if (!sIndexEntries || sHeader.indexCount == 0) { dvxMessageBox(sAc, "DVX Help", "No search index available.", MB_OK | MB_ICONINFO); return; } char searchBuf[MAX_SEARCH_BUF] = ""; if (!dvxInputBox(sAc, "Search Help", "Search for:", NULL, searchBuf, sizeof(searchBuf))) { return; } if (searchBuf[0] == '\0') { return; } // Linear scan of keyword index for case-insensitive substring match int32_t firstMatch = -1; for (uint32_t i = 0; i < sHeader.indexCount; i++) { const char *keyword = hlpString(sIndexEntries[i].keywordStr); // Case-insensitive substring search const char *kp = keyword; const char *sp = searchBuf; bool found = false; while (*kp && !found) { const char *k = kp; const char *s = sp; bool match = true; while (*s && *k) { char kc = *k; char sc = *s; if (kc >= 'A' && kc <= 'Z') { kc += 32; } if (sc >= 'A' && sc <= 'Z') { sc += 32; } if (kc != sc) { match = false; break; } k++; s++; } if (match && *s == '\0') { found = true; } kp++; } if (found) { firstMatch = (int32_t)sIndexEntries[i].topicIdx; break; } } if (firstMatch >= 0) { navigateToTopic(firstMatch); } else { dvxMessageBox(sAc, "Search Help", "No matching topics found.", MB_OK | MB_ICONINFO); } } static void onTocChange(WidgetT *w) { WidgetT *selected = wgtTreeViewGetSelected(w); if (!selected) { return; } int32_t topicIdx = (int32_t)(intptr_t)selected->userData; if (topicIdx == 0xFFFF) { return; } navigateToTopic(topicIdx); } // ============================================================ // Entry point // ============================================================ int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; // Register private widget classes registerWidgetClasses(); // Determine help file path: use args if provided, else default char hlpPath[DVX_MAX_PATH]; if (ctx->args[0]) { // args may be "path/to/file.hlp" or "path/to/file.hlp topicId" snprintf(hlpPath, sizeof(hlpPath), "%s", ctx->args); // Truncate at first space to separate path from topic ID char *space = strchr(hlpPath, ' '); if (space) { *space = '\0'; } } else { snprintf(hlpPath, sizeof(hlpPath), "%s%c%s", ctx->appDir, DVX_PATH_SEP, "dvxhelp.hlp"); } if (!openHelpFile(hlpPath)) { dvxSetBusy(sAc, false); dvxMessageBox(sAc, "DVX Help", "Could not open help file.", MB_OK | MB_ICONERROR); return -1; } // Create window int32_t screenW = sAc->display.width; int32_t screenH = sAc->display.height; int32_t winW = HELP_WIN_W; int32_t winH = HELP_WIN_H; int32_t winX = (screenW - winW) / 2 + HELP_CASCADE_OFFSET; int32_t winY = (screenH - winH) / 3 + HELP_CASCADE_OFFSET; sWin = dvxCreateWindow(sAc, "DVX Help", winX, winY, winW, winH, true); if (!sWin) { closeHelpFile(); return -1; } sWin->onClose = onClose; sWin->onMenu = onMenu; // Menu bar MenuBarT *menuBar = wmAddMenuBar(sWin); MenuT *navMenu = wmAddMenu(menuBar, "&Navigate"); wmAddMenuItem(navMenu, "&Back", CMD_BACK); wmAddMenuItem(navMenu, "&Forward", CMD_FORWARD); wmAddMenuSeparator(navMenu); wmAddMenuItem(navMenu, "&Index...", CMD_INDEX); wmAddMenuItem(navMenu, "&Search...", CMD_SEARCH); // Widget tree WidgetT *root = wgtInitWindow(sAc, sWin); // Toolbar WidgetT *toolbar = wgtToolbar(root); sBtnBack = wgtButton(toolbar, "< Back"); sBtnBack->onClick = onNavBack; wgtSetEnabled(sBtnBack, false); sBtnForward = wgtButton(toolbar, "Fwd >"); sBtnForward->onClick = onNavForward; wgtSetEnabled(sBtnForward, false); WidgetT *btnIndex = wgtButton(toolbar, "Index"); btnIndex->onClick = onIndex; WidgetT *btnSearch = wgtButton(toolbar, "Search"); btnSearch->onClick = onSearch; // Splitter with TOC tree and content scroll pane WidgetT *splitter = wgtSplitter(root, true); splitter->weight = 100; sTocTree = wgtTreeView(splitter); sTocTree->weight = 0; sTocTree->minW = wgtPixels(HELP_TOC_MIN_W); sTocTree->onChange = onTocChange; sContentScroll = wgtScrollPane(splitter); sContentScroll->weight = 100; sContentBox = wgtVBox(sContentScroll); sContentBox->spacing = wgtPixels(2); sContentBox->padding = wgtPixels(HELP_CONTENT_PAD); wgtSplitterSetPos(splitter, HELP_SPLITTER_POS); // Populate TOC tree populateToc(); // Navigate to requested topic (from args) or default int32_t startTopic = -1; if (ctx->args[0]) { // Check for topic ID after the file path const char *space = strchr(ctx->args, ' '); if (space && space[1]) { startTopic = findTopicByIdStr(space + 1); } } if (startTopic < 0) { startTopic = findTopicByIdStr(hlpString(sHeader.defaultTopicStr)); } if (startTopic < 0 && sHeader.topicCount > 0) { startTopic = 0; } if (startTopic >= 0) { navigateToTopic(startTopic); } return 0; }