DVX_GUI/apps/dvxhelp/dvxhelp.c

1640 lines
47 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 "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 <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_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;
}