DVX_GUI/apps/dvxhelp/dvxhelp.c

1406 lines
40 KiB
C

// 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 <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}