// 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 "dvxDlg.h" #include "dvxWgt.h" #include "dvxWgtP.h" #include "dvxWm.h" #include "dvxDraw.h" #include "dvxVideo.h" #include "dvxPlat.h" #include "shellApp.h" #include "box/box.h" #include "button/button.h" #include "splitter/splitter.h" #include "treeView/treeView.h" #include "scrollPane/scrlPane.h" #include "toolbar/toolbar.h" #include "image/image.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_PARA_SPACING 12 #define HELP_HEADING1_TOP 24 #define HELP_HEADING1_PAD 6 #define HELP_HEADING2_TOP 16 #define HELP_HEADING2_PAD 4 #define HELP_HEADING3_TOP 8 #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 MAX_HISTORY 64 #define MAX_SEARCH_BUF 128 #define CMD_BACK 100 #define CMD_FORWARD 101 #define CMD_INDEX 102 #define CMD_SEARCH 103 #define CMD_EXIT 104 // ============================================================ // 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; // Viewport wrap width — the width text should wrap at, derived from the // ScrollPane's inner area. Updated when the window is first laid out and // on resize. Wrapping widgets use this instead of w->parent->w so that // wide tables/code don't inflate the wrap width. static int32_t sWrapWidth = 0; // 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; // original flowing text (no newlines) char *wrapped; // cached wrapped text (with \n), NULL = needs wrap int32_t lineCount; // line count of wrapped text int32_t wrapWidth; // pixel width text was wrapped at (-1 = dirty) } HelpTextDataT; typedef struct { char *text; int32_t level; } HelpHeadingDataT; typedef struct { char *displayText; char *targetTopicId; } HelpLinkDataT; typedef struct { char *text; char *wrapped; int32_t lineCount; int32_t wrapWidth; uint8_t noteType; } HelpNoteDataT; typedef struct { char *text; int32_t lineCount; } HelpCodeDataT; typedef struct { char *text; char *wrapped; int32_t lineCount; int32_t wrapWidth; } 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 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 // ============================================================ // Word-wrap flowing text to fit within colWidth characters. // Returns a dvxMalloc'd string with \n at wrap points. // Caller must dvxFree the result. static char *wrapText(const char *text, int32_t colWidth) { if (!text || !text[0] || colWidth < 10) { return dvxStrdup(text ? text : ""); } int32_t textLen = (int32_t)strlen(text); char *result = (char *)dvxMalloc(textLen + textLen / colWidth + 16); if (!result) { return dvxStrdup(text); } int32_t outPos = 0; int32_t col = 0; const char *p = text; while (*p) { // Skip leading whitespace at start of line if (col == 0) { while (*p == ' ') { p++; } } if (!*p) { break; } // Find end of word const char *wordStart = p; while (*p && *p != ' ' && *p != '\n') { p++; } int32_t wordLen = (int32_t)(p - wordStart); // Check if word fits on current line if (col > 0 && col + 1 + wordLen > colWidth) { result[outPos++] = '\n'; col = 0; } else if (col > 0) { result[outPos++] = ' '; col++; } memcpy(result + outPos, wordStart, wordLen); outPos += wordLen; col += wordLen; // Skip spaces between words while (*p == ' ') { p++; } // Explicit newline in source = paragraph break if (*p == '\n') { result[outPos++] = '\n'; col = 0; p++; } } result[outPos] = '\0'; return result; } // Ensure a HelpText-style widget's wrapped text is up to date for the given pixel width. // Returns the wrapped text and updates lineCount. Uses cached result if width unchanged. // Wrap text at the given pixel width. Caches the result — only re-wraps // when pixelW changes. Returns the wrapped string. static const char *doWrap(char **wrappedPtr, int32_t *wrapWidthPtr, int32_t *lineCountPtr, const char *text, int32_t pixelW, int32_t charW, int32_t padPixels) { int32_t cols = (pixelW - padPixels) / charW; if (cols < 10) { cols = 10; } // Return cached result if width hasn't changed if (*wrappedPtr && *wrapWidthPtr == pixelW) { return *wrappedPtr; } dvxFree(*wrappedPtr); *wrappedPtr = wrapText(text, cols); *wrapWidthPtr = pixelW; *lineCountPtr = countLines(*wrappedPtr); return *wrappedPtr; } 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 int32_t countLines(const char *text) { int32_t count = 1; for (const char *p = text; *p; p++) { if (*p == '\n') { count++; } } return count; } 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 // ============================================================ // Update sWrapWidth from the ScrollPane's current size. static void updateWrapWidth(void) { // 38 = ScrollPane border (4) + VBox padding (12) + scrollbar (14) + 1 char buffer (8) if (sContentScroll && sContentScroll->w > 38) { sWrapWidth = sContentScroll->w - 38; } } static void helpTextCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpTextDataT *td = (HelpTextDataT *)w->data; updateWrapWidth(); if (sWrapWidth > 0) { doWrap(&td->wrapped, &td->wrapWidth, &td->lineCount, td->text, sWrapWidth, font->charWidth, 0); } w->calcMinW = 0; 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; const char *text = td->wrapped ? td->wrapped : td->text; paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, text, td->lineCount, colors->contentFg); } static void helpTextDestroy(WidgetT *w) { HelpTextDataT *td = (HelpTextDataT *)w->data; if (td) { dvxFree(td->wrapped); dvxFree(td->text); dvxFree(td); w->data = NULL; } } // ============================================================ // HelpHeading widget class // ============================================================ static bool isFirstChild(WidgetT *w) { return w->parent && w->parent->firstChild == w; } static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) { HelpHeadingDataT *hd = (HelpHeadingDataT *)w->data; int32_t textH = font->charHeight; bool first = isFirstChild(w); w->calcMinW = 0; if (hd->level == 1) { int32_t top = first ? 0 : HELP_HEADING1_TOP; w->calcMinH = top + textH + HELP_HEADING1_PAD * 2; } else if (hd->level == 2) { int32_t top = first ? 0 : HELP_HEADING2_TOP; w->calcMinH = top + textH + HELP_HEADING2_PAD + 2; } else { int32_t top = first ? 0 : HELP_HEADING3_TOP; w->calcMinH = top + textH; } } 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); bool first = isFirstChild(w); if (hd->level == 1) { int32_t top = first ? 0 : HELP_HEADING1_TOP; int32_t barY = w->y + top; int32_t barH = w->h - top; rectFill(d, ops, w->x, barY, w->w, barH, colors->activeTitleBg); int32_t textY = barY + 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) { int32_t top = first ? 0 : HELP_HEADING2_TOP; int32_t textY = w->y + top; 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 { int32_t top = first ? 0 : HELP_HEADING3_TOP; int32_t textY = w->y + top; drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, 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) { w->calcMinW = 0; 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; updateWrapWidth(); if (sWrapWidth > 0) { int32_t notePad = HELP_NOTE_PAD * 2 + HELP_NOTE_PREFIX_W; doWrap(&nd->wrapped, &nd->wrapWidth, &nd->lineCount, nd->text, sWrapWidth, font->charWidth, notePad); } w->calcMinW = 0; 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; 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; const char *text = nd->wrapped ? nd->wrapped : nd->text; paintLines(d, ops, font, textX, w->y + HELP_NOTE_PAD, text, nd->lineCount, colors->contentFg); } static void helpNoteDestroy(WidgetT *w) { HelpNoteDataT *nd = (HelpNoteDataT *)w->data; if (nd) { dvxFree(nd->wrapped); 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; updateWrapWidth(); if (sWrapWidth > 0) { doWrap(&ld->wrapped, &ld->wrapWidth, &ld->lineCount, ld->text, sWrapWidth, font->charWidth, 0); } w->calcMinW = 0; w->calcMinH = ld->lineCount * font->charHeight; } static void helpListItemPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { HelpListItemDataT *ld = (HelpListItemDataT *)w->data; const char *text = ld->wrapped ? ld->wrapped : ld->text; paintLines(d, ops, font, w->x + HELP_CONTENT_PAD, w->y, text, ld->lineCount, colors->contentFg); } static void helpListItemDestroy(WidgetT *w) { HelpListItemDataT *ld = (HelpListItemDataT *)w->data; if (ld) { dvxFree(ld->wrapped); 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(); } // userData stores topicIdx + 1 so that topic 0 doesn't collide with NULL #define TOPIC_TO_USERDATA(idx) ((void *)(intptr_t)((idx) + 1)) #define USERDATA_TO_TOPIC(ud) ((int32_t)(intptr_t)(ud) - 1) static WidgetT *findTreeItemByTopic(WidgetT *node, int32_t topicIdx) { for (WidgetT *child = node->firstChild; child; child = child->nextSibling) { if (child->userData && USERDATA_TO_TOPIC(child->userData) == topicIdx) { return child; } WidgetT *found = findTreeItemByTopic(child, topicIdx); if (found) { return found; } } return NULL; } static void navigateToTopic(int32_t topicIdx) { if (topicIdx < 0 || (uint32_t)topicIdx >= sHeader.topicCount) { return; } if (topicIdx == sCurrentTopic) { dvxLog("[WRAP] navigateToTopic: SKIPPED (same topic %d)", (int)topicIdx); return; } dvxLog("[WRAP] navigateToTopic: %d -> %d", (int)sCurrentTopic, (int)topicIdx); 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); // Sync TOC tree selection if (sTocTree) { WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx); dvxLog("[WRAP] treeSync: topicIdx=%d item=%p", (int)topicIdx, (void *)item); if (item) { // Suppress onChange to avoid re-entering navigateToTopic void (*savedOnChange)(WidgetT *) = sTocTree->onChange; sTocTree->onChange = NULL; wgtTreeViewSetSelected(sTocTree, item); sTocTree->onChange = savedOnChange; } } } 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->wrapWidth = -1; 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: target topic ID \0 display text \0 const char *targetTopicId = payload; const char *displayText = 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; // Save file position — buildContentWidgets reads records // sequentially and we must not disturb its position. long savedPos = ftell(sHlpFile); 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; bool hasAlpha = false; uint32_t keyColor = 0; uint8_t *pixels = dvxLoadImageAlpha(sAc, bmpData, (int32_t)imgRef->imageSize, &imgW, &imgH, &imgP, &hasAlpha, &keyColor); if (pixels) { WidgetT *parent = sContentBox; if (hdr->flags == HLP_IMG_CENTER || hdr->flags == HLP_IMG_RIGHT) { WidgetT *row = wgtHBox(sContentBox); if (row) { row->align = (hdr->flags == HLP_IMG_CENTER) ? AlignCenterE : AlignEndE; parent = row; } } WidgetT *imgWgt = wgtImage(parent, pixels, imgW, imgH, imgP); if (imgWgt && hasAlpha) { wgtImageSetTransparent(imgWgt, true, keyColor); } } } dvxFree(bmpData); } // Restore file position for sequential record reading fseek(sHlpFile, savedPos, SEEK_SET); } break; } case HLP_REC_LIST_ITEM: { WidgetT *w = widgetAlloc(sContentBox, sHelpListItemTypeId); if (w) { HelpListItemDataT *ld = (HelpListItemDataT *)dvxCalloc(1, sizeof(HelpListItemDataT)); // Prepend bullet + space to the text int32_t pLen = (int32_t)strlen(payload); ld->text = (char *)dvxMalloc(pLen + 3); if (ld->text) { ld->text[0] = '\x07'; ld->text[1] = ' '; memcpy(ld->text + 2, payload, pLen + 1); } ld->wrapWidth = -1; 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->wrapWidth = -1; 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; } } } // Re-wrap all text content widgets at the current content box width. 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); } // Compute viewport wrap width from the ScrollPane. // ScrollPane border = 2*2 = 4, vertical scrollbar = 14, VBox padding = 2*6 = 12. // Total overhead = 30. This is approximate but stable. if (sContentScroll && sContentScroll->w > 30) { updateWrapWidth(); } // Reset scroll to top if (sContentScroll) { wgtScrollPaneScrollToTop(sContentScroll); } if (sContentScroll) { wgtInvalidate(sContentScroll); } } // ============================================================ // 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 (+1 so topic 0 != NULL) item->userData = TOPIC_TO_USERDATA(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; case CMD_EXIT: if (sWin) { onClose(sWin); } 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 || !selected->userData) { return; } int32_t topicIdx = USERDATA_TO_TOPIC(selected->userData); if (topicIdx < 0 || topicIdx >= (int32_t)sHeader.topicCount) { 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); wmAddMenuSeparator(navMenu); wmAddMenuItem(navMenu, "E&xit", CMD_EXIT); // 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(HELP_PARA_SPACING); 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; }