From 344ab4794dd3308bfb8a0b8499f80271bfe605fd Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 29 Mar 2026 22:06:01 -0500 Subject: [PATCH] More IDE changes and a few core changes to help support easier app development. --- .gitignore | 1 + apps/cpanel/cpanel.c | 16 +-- apps/dvxbasic/compiler/lexer.c | 4 + apps/dvxbasic/dvxbasic.res | 7 + apps/dvxbasic/ide/ideMain.c | 163 ++++++++++++++++++++-- apps/dvxbasic/ide/ideToolbox.c | 49 ++----- apps/dvxbasic/tb_code.bmp | 3 + apps/dvxbasic/tb_design.bmp | 3 + apps/dvxbasic/tb_open.bmp | 3 + apps/dvxbasic/tb_run.bmp | 3 + apps/dvxbasic/tb_save.bmp | 3 + apps/dvxbasic/tb_stop.bmp | 3 + apps/dvxdemo/dvxdemo.c | 4 +- apps/imgview/imgview.c | 2 +- apps/notepad/notepad.c | 6 +- apps/progman/progman.c | 44 +----- core/dvxApp.c | 86 +++++++++++- core/dvxApp.h | 25 +++- core/dvxResource.h | 8 +- core/dvxTypes.h | 11 ++ core/dvxWm.c | 120 +++++++++++++++- core/dvxWm.h | 9 ++ core/platform/dvxPlatformDos.c | 2 +- core/widgetClass.c | 2 +- loader/loaderMain.c | 8 +- shell/shellApp.c | 5 +- shell/shellApp.h | 9 +- tools/Makefile | 5 +- tools/mktbicon.c | 243 +++++++++++++++++++++++++++++++++ 29 files changed, 731 insertions(+), 116 deletions(-) create mode 100644 apps/dvxbasic/tb_code.bmp create mode 100644 apps/dvxbasic/tb_design.bmp create mode 100644 apps/dvxbasic/tb_open.bmp create mode 100644 apps/dvxbasic/tb_run.bmp create mode 100644 apps/dvxbasic/tb_save.bmp create mode 100644 apps/dvxbasic/tb_stop.bmp create mode 100644 tools/mktbicon.c diff --git a/.gitignore b/.gitignore index defbee0..6ffc101 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ lib/ .gitattributes~ *.SWP .claude/ +capture/ diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index 1dd1f4c..c033087 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -77,7 +77,7 @@ AppDescriptorT appDescriptor = { typedef struct { char name[64]; - char path[280]; + char path[DVX_MAX_PATH]; } FileEntryT; // ============================================================ @@ -119,7 +119,7 @@ static WidgetT *sColorSwatch = NULL; static WidgetT *sWallpaperLbl = NULL; static WidgetT *sWpaperList = NULL; static WidgetT *sWpModeDrop = NULL; -static char sWallpaperPath[280]; +static char sWallpaperPath[DVX_MAX_PATH]; static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array static const char **sWpaperLabels = NULL; // stb_ds dynamic array @@ -575,7 +575,7 @@ static void onBrowseTheme(WidgetT *w) { { "Theme Files (*.thm)", "*.thm" }, { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Load Theme", FD_OPEN, THEME_DIR, filters, 2, path, sizeof(path))) { dvxLoadTheme(sAc, path); @@ -591,7 +591,7 @@ static void onSaveTheme(WidgetT *w) { { "Theme Files (*.thm)", "*.thm" }, { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Save Theme", FD_SAVE, THEME_DIR, filters, 2, path, sizeof(path))) { dvxSaveTheme(sAc, path); @@ -637,7 +637,7 @@ static void onChooseWallpaper(WidgetT *w) { { "Images (*.bmp;*.jpg;*.png)", "*.bmp;*.jpg;*.png" }, { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, "CONFIG/WPAPER", filters, 2, path, sizeof(path))) { if (dvxSetWallpaper(sAc, path)) { @@ -867,7 +867,7 @@ static void onClose(WindowT *win) { // saveSnapshot / restoreSnapshot // ============================================================ -static char sSavedWallpaperPath[260]; +static char sSavedWallpaperPath[DVX_MAX_PATH]; static void saveSnapshot(void) { memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb)); @@ -948,7 +948,7 @@ static void scanThemes(void) { memcpy(entry.name, ent->d_name, nameLen); entry.name[nameLen] = '\0'; - snprintf(entry.path, sizeof(entry.path), "%s/%s", THEME_DIR, ent->d_name); + snprintf(entry.path, sizeof(entry.path), "%s/%.240s", THEME_DIR, ent->d_name); arrput(sThemeEntries, entry); } @@ -992,7 +992,7 @@ static void scanWallpapers(void) { FileEntryT entry = {0}; snprintf(entry.name, sizeof(entry.name), "%.63s", ent->d_name); - snprintf(entry.path, sizeof(entry.path), "%s/%s", WPAPER_DIR, ent->d_name); + snprintf(entry.path, sizeof(entry.path), "%s/%.240s", WPAPER_DIR, ent->d_name); arrput(sWpaperEntries, entry); } diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index 3ebdf16..1712b5d 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -330,6 +330,10 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) { lex->token.type = TOK_HASH; break; + case '?': + lex->token.type = TOK_PRINT; + break; + case '=': lex->token.type = TOK_EQ; break; diff --git a/apps/dvxbasic/dvxbasic.res b/apps/dvxbasic/dvxbasic.res index 3bdd26f..18b92da 100644 --- a/apps/dvxbasic/dvxbasic.res +++ b/apps/dvxbasic/dvxbasic.res @@ -3,3 +3,10 @@ icon32 icon icon32.bmp name text "DVX BASIC" author text "DVX Project" description text "BASIC language IDE and runtime" +# Toolbar icons (16x16) +tb_open icon tb_open.bmp +tb_save icon tb_save.bmp +tb_run icon tb_run.bmp +tb_stop icon tb_stop.bmp +tb_code icon tb_code.bmp +tb_design icon tb_design.bmp diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 2ec7e75..6475d30 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -11,11 +11,13 @@ #include "dvxCursor.h" #include "dvxPlatform.h" #include "dvxDialog.h" +#include "dvxPrefs.h" #include "dvxWidget.h" #include "dvxWidgetPlugin.h" #include "dvxWm.h" #include "shellApp.h" #include "widgetBox.h" +#include "widgetImageButton.h" #include "widgetLabel.h" #include "widgetTextInput.h" #include "widgetDropdown.h" @@ -66,6 +68,12 @@ #define CMD_WIN_TOOLBOX 112 #define CMD_WIN_PROPS 113 #define CMD_DELETE 114 +#define CMD_CUT 115 +#define CMD_COPY 116 +#define CMD_PASTE 117 +#define CMD_SELECT_ALL 118 +#define CMD_VIEW_TOOLBAR 119 +#define CMD_VIEW_STATUS 120 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -83,6 +91,8 @@ static void loadFilePath(const char *path); static void saveFile(void); static void onTbSave(WidgetT *w); static void onClose(WindowT *win); +static void onContentFocus(WindowT *win); +static WindowT *getLastFocusWin(void); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static void evaluateImmediate(const char *expr); @@ -128,6 +138,8 @@ static WidgetT *sOutput = NULL; static WidgetT *sImmediate = NULL; static WidgetT *sObjDropdown = NULL; static WidgetT *sEvtDropdown = NULL; +static WidgetT *sToolbar = NULL; +static WidgetT *sStatusBar = NULL; static WidgetT *sStatus = NULL; static BasVmT *sVm = NULL; // VM instance (non-NULL while running) static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5) @@ -135,11 +147,12 @@ static DsgnStateT sDesigner; static WindowT *sFormWin = NULL; // Form designer window (separate) static WindowT *sToolboxWin = NULL; static WindowT *sPropsWin = NULL; +static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window static char sSourceBuf[IDE_MAX_SOURCE]; static char sOutputBuf[IDE_MAX_OUTPUT]; static int32_t sOutputLen = 0; -static char sFilePath[260]; +static char sFilePath[DVX_MAX_PATH]; // Procedure table for Object/Event dropdowns typedef struct { @@ -175,6 +188,27 @@ int32_t appMain(DxeAppContextT *ctx) { basStringSystemInit(); buildWindow(); + // Load persisted settings + char prefsPath[DVX_MAX_PATH]; + shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath)); + prefsLoad(prefsPath); + + if (sToolbar && sWin && sWin->menuBar) { + bool showTb = prefsGetBool("view", "toolbar", true); + sToolbar->visible = showTb; + wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb); + } + + if (sStatusBar && sWin && sWin->menuBar) { + bool showSb = prefsGetBool("view", "statusbar", true); + sStatusBar->visible = showSb; + wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb); + } + + if (sWin) { + dvxFitWindowH(sAc, sWin); + } + sFilePath[0] = '\0'; sSourceBuf[0] = '\0'; sOutputBuf[0] = '\0'; @@ -187,6 +221,25 @@ int32_t appMain(DxeAppContextT *ctx) { return 0; } +// ============================================================ +// loadTbIcon -- load a toolbar icon from the app's resources +// ============================================================ + +static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText) { + int32_t iconW = 0; + int32_t iconH = 0; + int32_t iconPitch = 0; + uint8_t *data = dvxResLoadIcon(sAc, sCtx->appPath, resName, &iconW, &iconH, &iconPitch); + + if (data) { + return wgtImageButton(parent, data, iconW, iconH, iconPitch); + } + + // Fallback to text button if icon not found + return wgtButton(parent, fallbackText); +} + + // ============================================================ // buildWindow // ============================================================ @@ -211,6 +264,12 @@ static void buildWindow(void) { wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); MenuT *editMenu = wmAddMenu(menuBar, "&Edit"); + wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT); + wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY); + wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE); + wmAddMenuSeparator(editMenu); + wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL); + wmAddMenuSeparator(editMenu); wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE); MenuT *runMenu = wmAddMenu(menuBar, "&Run"); @@ -223,6 +282,9 @@ static void buildWindow(void) { MenuT *viewMenu = wmAddMenu(menuBar, "&View"); wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN); + wmAddMenuSeparator(viewMenu); + wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true); + wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true); MenuT *winMenu = wmAddMenu(menuBar, "&Window"); wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE); @@ -243,27 +305,35 @@ static void buildWindow(void) { sWin->accelTable = accel; WidgetT *tbRoot = wgtInitWindow(sAc, sWin); - WidgetT *tb = wgtToolbar(tbRoot); + sToolbar = wgtToolbar(tbRoot); + WidgetT *tb = sToolbar; - WidgetT *tbOpen = wgtButton(tb, "Open"); + WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open"); tbOpen->onClick = onTbOpen; + wgtSetTooltip(tbOpen, "Open (Ctrl+O)"); - WidgetT *tbSave = wgtButton(tb, "Save"); + WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save"); tbSave->onClick = onTbSave; + wgtSetTooltip(tbSave, "Save (Ctrl+S)"); - WidgetT *tbRun = wgtButton(tb, "Run"); + WidgetT *tbRun = loadTbIcon(tb, "tb_run", "Run"); tbRun->onClick = onTbRun; + wgtSetTooltip(tbRun, "Run (F5)"); - WidgetT *tbStop = wgtButton(tb, "Stop"); + WidgetT *tbStop = loadTbIcon(tb, "tb_stop", "Stop"); tbStop->onClick = onTbStop; + wgtSetTooltip(tbStop, "Stop (Esc)"); - WidgetT *tbCode = wgtButton(tb, "Code"); + WidgetT *tbCode = loadTbIcon(tb, "tb_code", "Code"); tbCode->onClick = onTbCode; + wgtSetTooltip(tbCode, "Code View (F7)"); - WidgetT *tbDesign = wgtButton(tb, "Design"); + WidgetT *tbDesign = loadTbIcon(tb, "tb_design", "Design"); tbDesign->onClick = onTbDesign; + wgtSetTooltip(tbDesign, "Design View (Shift+F7)"); - WidgetT *statusBar = wgtStatusBar(tbRoot); + sStatusBar = wgtStatusBar(tbRoot); + WidgetT *statusBar = sStatusBar; sStatus = wgtLabel(statusBar, ""); sStatus->weight = 100; @@ -792,7 +862,7 @@ static void loadFile(void) { { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { loadFilePath(path); @@ -833,7 +903,7 @@ static void saveFile(void) { // Save the .frm if the designer has form data if (sDesigner.form && sDesigner.form->dirty) { - char frmPath[260]; + char frmPath[DVX_MAX_PATH]; snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); char *dot = strrchr(frmPath, '.'); @@ -875,7 +945,7 @@ static void loadFrmFiles(BasFormRtT *rt) { } // Build .frm path from .bas path - char frmPath[260]; + char frmPath[DVX_MAX_PATH]; snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); // Find the extension and replace with .frm @@ -928,6 +998,26 @@ static void loadFrmFiles(BasFormRtT *rt) { +// ============================================================ +// onContentFocus -- track last focused content window for clipboard +// ============================================================ + +static void onContentFocus(WindowT *win) { + sLastFocusWin = win; +} + +static WindowT *getLastFocusWin(void) { + if (sLastFocusWin == sCodeWin || + sLastFocusWin == sOutWin || + sLastFocusWin == sImmWin) { + return sLastFocusWin; + } + + sLastFocusWin = NULL; + return NULL; +} + + // ============================================================ // onClose // ============================================================ @@ -954,6 +1044,7 @@ static void onClose(WindowT *win) { sCodeWin = NULL; sOutWin = NULL; sImmWin = NULL; + sLastFocusWin = NULL; sEditor = NULL; sOutput = NULL; sImmediate = NULL; @@ -1045,6 +1136,23 @@ static void onMenu(WindowT *win, int32_t menuId) { } break; + case CMD_CUT: + case CMD_COPY: + case CMD_PASTE: + case CMD_SELECT_ALL: { + // Send the corresponding Ctrl+key to the last focused content window + static const int32_t keys[] = { 24, 3, 22, 1 }; // Ctrl+X, C, V, A + int32_t key = keys[menuId - CMD_CUT]; + + WindowT *target = getLastFocusWin(); + + if (target && target->onKey) { + target->onKey(target, key, ACCEL_CTRL); + } + + break; + } + case CMD_DELETE: if (sFormWin && sDesigner.selectedIdx >= 0) { int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls); @@ -1059,6 +1167,26 @@ static void onMenu(WindowT *win, int32_t menuId) { } break; + case CMD_VIEW_TOOLBAR: + if (sToolbar && sWin->menuBar) { + bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR); + sToolbar->visible = show; + dvxFitWindowH(sAc, sWin); + prefsSetBool("view", "toolbar", show); + prefsSave(); + } + break; + + case CMD_VIEW_STATUS: + if (sStatusBar && sWin->menuBar) { + bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS); + sStatusBar->visible = show; + dvxFitWindowH(sAc, sWin); + prefsSetBool("view", "statusbar", show); + prefsSave(); + } + break; + case CMD_EXIT: if (sWin) { onClose(sWin); @@ -1446,7 +1574,7 @@ static void switchToDesign(void) { // Load .frm if we don't have a form yet if (!sDesigner.form) { if (sFilePath[0]) { - char frmPath[260]; + char frmPath[DVX_MAX_PATH]; snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); char *dot = strrchr(frmPath, '.'); @@ -1575,7 +1703,9 @@ static void showCodeWindow(void) { if (sCodeWin) { sCodeWin->onMenu = onMenu; + sCodeWin->onFocus = onContentFocus; sCodeWin->accelTable = sWin ? sWin->accelTable : NULL; + sLastFocusWin = sCodeWin; WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin); @@ -1622,6 +1752,9 @@ static void showOutputWindow(void) { sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); if (sOutWin) { + sOutWin->onFocus = onContentFocus; + sLastFocusWin = sOutWin; + WidgetT *outRoot = wgtInitWindow(sAc, sOutWin); sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT); sOutput->weight = 100; @@ -1630,7 +1763,6 @@ static void showOutputWindow(void) { if (sOutputLen > 0) { wgtSetText(sOutput, sOutputBuf); } - } } @@ -1650,6 +1782,9 @@ static void showImmediateWindow(void) { sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true); if (sImmWin) { + sImmWin->onFocus = onContentFocus; + sLastFocusWin = sImmWin; + WidgetT *immRoot = wgtInitWindow(sAc, sImmWin); if (immRoot) { diff --git a/apps/dvxbasic/ide/ideToolbox.c b/apps/dvxbasic/ide/ideToolbox.c index e1e6d56..699eb39 100644 --- a/apps/dvxbasic/ide/ideToolbox.c +++ b/apps/dvxbasic/ide/ideToolbox.c @@ -110,47 +110,28 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds) { snprintf(entry.typeName, DSGN_MAX_NAME, "%s", iface->basName); // Load icon data and tooltip from the .wgt file resources - uint8_t *iconData = NULL; - int32_t iconW = 0; - int32_t iconH = 0; + uint8_t *iconData = NULL; + int32_t iconW = 0; + int32_t iconH = 0; int32_t iconPitch = 0; const char *wgtPath = wgtIfaceGetPath(wgtName); int32_t pathIdx = wgtIfaceGetPathIndex(wgtName); if (wgtPath) { - DvxResHandleT *res = dvxResOpen(wgtPath); + // Build suffixed resource names: "icon16", "icon16-2", etc. + char iconResName[32]; + char nameResName[32]; - if (res) { - // Build suffixed resource names: "icon16", "icon16-2", "icon16-3", etc. - char iconResName[32]; - char nameResName[32]; - - if (pathIdx <= 1) { - snprintf(iconResName, sizeof(iconResName), "icon16"); - snprintf(nameResName, sizeof(nameResName), "name"); - } else { - snprintf(iconResName, sizeof(iconResName), "icon16-%d", (int)pathIdx); - snprintf(nameResName, sizeof(nameResName), "name-%d", (int)pathIdx); - } - - uint32_t iconSize = 0; - void *iconBuf = dvxResRead(res, iconResName, &iconSize); - - if (iconBuf) { - iconData = dvxLoadImageFromMemory(ctx, (const uint8_t *)iconBuf, (int32_t)iconSize, &iconW, &iconH, &iconPitch); - free(iconBuf); - } - - uint32_t nameSize = 0; - void *nameBuf = dvxResRead(res, nameResName, &nameSize); - - if (nameBuf) { - snprintf(entry.tooltip, sizeof(entry.tooltip), "%s", (const char *)nameBuf); - free(nameBuf); - } - - dvxResClose(res); + if (pathIdx <= 1) { + snprintf(iconResName, sizeof(iconResName), "icon16"); + snprintf(nameResName, sizeof(nameResName), "name"); + } else { + snprintf(iconResName, sizeof(iconResName), "icon16-%d", (int)pathIdx); + snprintf(nameResName, sizeof(nameResName), "name-%d", (int)pathIdx); } + + iconData = dvxResLoadIcon(ctx, wgtPath, iconResName, &iconW, &iconH, &iconPitch); + dvxResLoadText(wgtPath, nameResName, entry.tooltip, sizeof(entry.tooltip)); } if (!entry.tooltip[0]) { diff --git a/apps/dvxbasic/tb_code.bmp b/apps/dvxbasic/tb_code.bmp new file mode 100644 index 0000000..e4a3389 --- /dev/null +++ b/apps/dvxbasic/tb_code.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef17dfa3bfe51fb02c6a20c17df7cd3f1cd1713960f15a7848a78a36e97285d2 +size 822 diff --git a/apps/dvxbasic/tb_design.bmp b/apps/dvxbasic/tb_design.bmp new file mode 100644 index 0000000..664913e --- /dev/null +++ b/apps/dvxbasic/tb_design.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd0c5220bce76d8d9e5d3ec45c1d1e3d39b83b36087521383e9469f034f156f2 +size 822 diff --git a/apps/dvxbasic/tb_open.bmp b/apps/dvxbasic/tb_open.bmp new file mode 100644 index 0000000..019eb08 --- /dev/null +++ b/apps/dvxbasic/tb_open.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17248ab322b712f9aaaf9a126b410f8db4145d95e3e24a1d6190a65a3d2c592d +size 822 diff --git a/apps/dvxbasic/tb_run.bmp b/apps/dvxbasic/tb_run.bmp new file mode 100644 index 0000000..d053a9d --- /dev/null +++ b/apps/dvxbasic/tb_run.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce4ef5c26f20333de66cca76b27a688e3fc5c29c0cb8a95de11c84d488d098c2 +size 822 diff --git a/apps/dvxbasic/tb_save.bmp b/apps/dvxbasic/tb_save.bmp new file mode 100644 index 0000000..585cb15 --- /dev/null +++ b/apps/dvxbasic/tb_save.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb9baee905444a92bc65505f503fff7f54d8e460da524873fa0d027454a69000 +size 822 diff --git a/apps/dvxbasic/tb_stop.bmp b/apps/dvxbasic/tb_stop.bmp new file mode 100644 index 0000000..dd5a720 --- /dev/null +++ b/apps/dvxbasic/tb_stop.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:509ee540684cea0f840a3fdd92e28c506767476f560d6bda4af49937449df0f4 +size 822 diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index 7be9850..e985632 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -163,7 +163,7 @@ static const FileFilterT sFileFilters[] = { static void onMenuCb(WindowT *win, int32_t menuId) { switch (menuId) { case CMD_FILE_OPEN: { - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Open File", FD_OPEN, NULL, sFileFilters, 5, path, sizeof(path))) { char msg[300]; @@ -175,7 +175,7 @@ static void onMenuCb(WindowT *win, int32_t menuId) { } case CMD_FILE_SAVE: { - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Save As", FD_SAVE, NULL, sFileFilters, 5, path, sizeof(path))) { char msg[300]; diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index 3943621..6d10233 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -331,7 +331,7 @@ static void openFile(void) { { "Images (*.bmp;*.jpg;*.png;*.gif)", "*.bmp;*.jpg;*.png;*.gif" }, { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Open Image", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { loadAndDisplay(path); diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index abb65d7..7faefd0 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -55,7 +55,7 @@ static DxeAppContextT *sCtx = NULL; static WindowT *sWin = NULL; static WidgetT *sTextArea = NULL; -static char sFilePath[260] = ""; +static char sFilePath[DVX_MAX_PATH] = ""; // Hash of text content at last save/open, used for cheap dirty detection static uint32_t sCleanHash = 0; @@ -171,7 +171,7 @@ static void doOpen(void) { { "Text Files (*.txt)", "*.txt" }, { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (!dvxFileDialog(sCtx->shellCtx, "Open", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { return; @@ -257,7 +257,7 @@ static void doSaveAs(void) { { "Text Files (*.txt)", "*.txt" }, { "All Files (*.*)", "*.*" } }; - char path[260]; + char path[DVX_MAX_PATH]; if (!dvxFileDialog(sCtx->shellCtx, "Save As", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { return; diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 0c3e9cc..aabdb26 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -445,45 +445,15 @@ static void scanAppsDirRecurse(const char *dirPath) { } // Override from embedded resources if available - DvxResHandleT *res = dvxResOpen(fullPath); + entry->iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &entry->iconW, &entry->iconH, &entry->iconPitch); - if (res) { - uint32_t iconSize = 0; - void *iconBuf = dvxResRead(res, "icon32", &iconSize); - - if (iconBuf) { - entry->iconData = dvxLoadImageFromMemory(sAc, (const uint8_t *)iconBuf, (int32_t)iconSize, &entry->iconW, &entry->iconH, &entry->iconPitch); - - if (!entry->iconData) { - dvxLog("Progman: failed to decode icon for %s (%d bytes)", ent->d_name, (int)iconSize); - } - - free(iconBuf); - } else { - dvxLog("Progman: no icon32 resource in %s", ent->d_name); - } - - uint32_t nameSize = 0; - void *nameBuf = dvxResRead(res, "name", &nameSize); - - if (nameBuf) { - snprintf(entry->name, SHELL_APP_NAME_MAX, "%s", (const char *)nameBuf); - free(nameBuf); - } - - uint32_t descSize = 0; - void *descBuf = dvxResRead(res, "description", &descSize); - - if (descBuf) { - snprintf(entry->tooltip, sizeof(entry->tooltip), "%s", (const char *)descBuf); - free(descBuf); - } - - dvxResClose(res); - } else { - dvxLog("Progman: no resources in %s", ent->d_name); + if (!entry->iconData) { + dvxLog("Progman: no icon32 resource in %s", ent->d_name); } + dvxResLoadText(fullPath, "name", entry->name, SHELL_APP_NAME_MAX); + dvxResLoadText(fullPath, "description", entry->tooltip, sizeof(entry->tooltip)); + dvxLog("Progman: found %s (%s) icon=%s", entry->name, ent->d_name, entry->iconData ? "yes" : "no"); sAppCount++; } @@ -563,7 +533,7 @@ int32_t appMain(DxeAppContextT *ctx) { sAc = ctx->shellCtx; // Load saved preferences - char prefsPath[260]; + char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath)); prefsLoad(prefsPath); sMinOnRun = prefsGetBool("options", "minimizeOnRun", false); diff --git a/core/dvxApp.c b/core/dvxApp.c index 83136f8..696bee4 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -38,6 +38,7 @@ #include "dvxCursor.h" #include "dvxPlatform.h" +#include "dvxResource.h" #include "thirdparty/stb_ds_wrap.h" #include @@ -1942,7 +1943,7 @@ static void interactiveScreenshot(AppContextT *ctx) { { "PNG Images (*.png)", "*.png" }, { "BMP Images (*.bmp)", "*.bmp" } }; - char path[260]; + char path[DVX_MAX_PATH]; int32_t scrW = ctx->display.width; int32_t scrH = ctx->display.height; @@ -1977,7 +1978,7 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) { { "PNG Images (*.png)", "*.png" }, { "BMP Images (*.bmp)", "*.bmp" } }; - char path[260]; + char path[DVX_MAX_PATH]; int32_t capW = win->contentW; int32_t capH = win->contentH; @@ -3701,6 +3702,87 @@ const char *dvxClipboardGet(int32_t *outLen) { } +// ============================================================ +// dvxResLoadIcon +// ============================================================ + +uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resName, int32_t *outW, int32_t *outH, int32_t *outPitch) { + if (!ctx || !dxePath || !resName) { + return NULL; + } + + DvxResHandleT *res = dvxResOpen(dxePath); + + if (!res) { + return NULL; + } + + uint32_t size = 0; + void *buf = dvxResRead(res, resName, &size); + dvxResClose(res); + + if (!buf) { + return NULL; + } + + uint8_t *pixels = dvxLoadImageFromMemory(ctx, (const uint8_t *)buf, (int32_t)size, outW, outH, outPitch); + free(buf); + return pixels; +} + + +// ============================================================ +// dvxResLoadText +// ============================================================ + +bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize) { + if (!dxePath || !resName || !buf || bufSize <= 0) { + return false; + } + + DvxResHandleT *res = dvxResOpen(dxePath); + + if (!res) { + return false; + } + + uint32_t size = 0; + void *data = dvxResRead(res, resName, &size); + dvxResClose(res); + + if (!data) { + return false; + } + + int32_t copyLen = (int32_t)size < bufSize - 1 ? (int32_t)size : bufSize - 1; + memcpy(buf, data, copyLen); + buf[copyLen] = '\0'; + free(data); + return true; +} + + +// ============================================================ +// dvxResLoadData +// ============================================================ + +void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize) { + if (!dxePath || !resName) { + return NULL; + } + + DvxResHandleT *res = dvxResOpen(dxePath); + + if (!res) { + return NULL; + } + + void *data = dvxResRead(res, resName, outSize); + dvxResClose(res); + return data; +} + + // ============================================================ // dvxColorLabel // ============================================================ diff --git a/core/dvxApp.h b/core/dvxApp.h index 85445c1..ec5f3d8 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -112,7 +112,7 @@ typedef struct AppContextT { // kept so the image can be reloaded after a video mode or mode change. uint8_t *wallpaperBuf; // pixel data (screen width * height * bpp/8) int32_t wallpaperPitch; // bytes per row - char wallpaperPath[260]; // source image path (empty = none) + char wallpaperPath[DVX_MAX_PATH]; // source image path (empty = none) WallpaperModeE wallpaperMode; // stretch, tile, or center } AppContextT; @@ -315,4 +315,27 @@ void dvxClipboardCopy(const char *text, int32_t len); // NULL if the clipboard is empty. const char *dvxClipboardGet(int32_t *outLen); +// ============================================================ +// Resource loading helpers +// ============================================================ +// +// Convenience functions for loading typed resources from DXE files. +// These combine dvxResOpen/Read/Close with type-specific processing +// so callers don't need to manage the resource handle or temp buffers. + +// Load an icon/image resource and decode it to native pixel format. +// Returns the decoded pixel data (caller must free with dvxFreeImage), +// or NULL if not found. Sets outW/outH/outPitch on success. +uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resName, int32_t *outW, int32_t *outH, int32_t *outPitch); + +// Load a text resource into a caller-provided buffer. +// Returns true on success. The text is null-terminated and truncated +// to fit bufSize. Returns false if the resource is not found. +bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize); + +// Load a raw binary resource. Returns a malloc'd buffer that the +// caller must free, or NULL if not found. Sets *outSize to the +// data size in bytes. +void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize); + #endif // DVX_APP_H diff --git a/core/dvxResource.h b/core/dvxResource.h index 3ab7735..c4a5723 100644 --- a/core/dvxResource.h +++ b/core/dvxResource.h @@ -18,6 +18,12 @@ #include +// Fallback for the native dvxres tool, which includes this header +// without dvxTypes.h. The canonical definition is in dvxTypes.h. +#ifndef DVX_MAX_PATH +#define DVX_MAX_PATH 260 +#endif + // Resource type IDs #define DVX_RES_ICON 1 // image data (BMP icon: 16x16, 32x32, etc.) #define DVX_RES_TEXT 2 // null-terminated string (author, copyright, etc.) @@ -48,7 +54,7 @@ typedef struct { // Runtime resource handle (opaque to callers) typedef struct { - char path[260]; + char path[DVX_MAX_PATH]; DvxResDirEntryT *entries; uint32_t entryCount; } DvxResHandleT; diff --git a/core/dvxTypes.h b/core/dvxTypes.h index f12cfe1..fc10dcc 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -11,6 +11,15 @@ #include #include +// ============================================================ +// Path limits +// ============================================================ +// +// Maximum file path length. 260 matches DOS MAX_PATH. Future +// platforms (Linux, Windows) should increase this. + +#define DVX_MAX_PATH 260 + // ============================================================ // Pixel format descriptor // ============================================================ @@ -557,6 +566,8 @@ typedef struct WindowT { void (*onMenu)(struct WindowT *win, int32_t menuId); void (*onScroll)(struct WindowT *win, ScrollbarOrientE orient, int32_t value); int32_t (*onCursorQuery)(struct WindowT *win, int32_t x, int32_t y); // return CURSOR_* or 0 for default + void (*onFocus)(struct WindowT *win); // window gained focus + void (*onBlur)(struct WindowT *win); // window lost focus } WindowT; // ============================================================ diff --git a/core/dvxWm.c b/core/dvxWm.c index 8da6899..aa85162 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -1031,6 +1031,105 @@ void wmAddMenuSeparator(MenuT *menu) { } +// ============================================================ +// wmMenuFindItem -- find menu item by command ID across all menus +// ============================================================ + +static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) { + if (!bar) { + return NULL; + } + + for (int32_t m = 0; m < bar->menuCount; m++) { + MenuT *menu = &bar->menus[m]; + + for (int32_t i = 0; i < menu->itemCount; i++) { + if (menu->items[i].id == id) { + if (outMenu) { + *outMenu = menu; + } + + return &menu->items[i]; + } + + // Search submenus recursively + if (menu->items[i].subMenu) { + for (int32_t j = 0; j < menu->items[i].subMenu->itemCount; j++) { + if (menu->items[i].subMenu->items[j].id == id) { + if (outMenu) { + *outMenu = menu->items[i].subMenu; + } + + return &menu->items[i].subMenu->items[j]; + } + } + } + } + } + + return NULL; +} + + +// ============================================================ +// wmMenuItemIsChecked +// ============================================================ + +bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id) { + MenuItemT *item = wmMenuFindItem(bar, id, NULL); + return item ? item->checked : false; +} + + +// ============================================================ +// wmMenuItemSetChecked +// ============================================================ + +void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked) { + MenuT *menu = NULL; + MenuItemT *item = wmMenuFindItem(bar, id, &menu); + + if (!item) { + return; + } + + if (item->type == MenuItemRadioE && checked && menu) { + // Find the radio group and uncheck all others + int32_t idx = (int32_t)(item - menu->items); + int32_t groupStart = idx; + + while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) { + groupStart--; + } + + int32_t groupEnd = idx; + + while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) { + groupEnd++; + } + + for (int32_t i = groupStart; i <= groupEnd; i++) { + menu->items[i].checked = (i == idx); + } + } else { + item->checked = checked; + } +} + + +// ============================================================ +// wmMenuItemSetEnabled +// ============================================================ + +void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled) { + MenuItemT *item = wmMenuFindItem(bar, id, NULL); + + if (item) { + item->enabled = enabled; + } +} + + // ============================================================ // wmAddSubMenu // ============================================================ @@ -1782,6 +1881,14 @@ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) { } } + if (stack->focusedIdx >= 0 && stack->focusedIdx < stack->count) { + WindowT *oldWin = stack->windows[stack->focusedIdx]; + + if (oldWin->onBlur) { + oldWin->onBlur(oldWin); + } + } + stack->focusedIdx = -1; } @@ -2618,8 +2725,10 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) { } // Unfocus old window + WindowT *oldWin = NULL; + if (stack->focusedIdx >= 0 && stack->focusedIdx < stack->count) { - WindowT *oldWin = stack->windows[stack->focusedIdx]; + oldWin = stack->windows[stack->focusedIdx]; oldWin->focused = false; // Dirty the old title bar @@ -2635,6 +2744,15 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) { // Dirty the new title bar dirtyListAdd(dl, newWin->x, newWin->y, newWin->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT); + + // Fire callbacks (after state is updated) + if (oldWin && oldWin != newWin && oldWin->onBlur) { + oldWin->onBlur(oldWin); + } + + if (newWin != oldWin && newWin->onFocus) { + newWin->onFocus(newWin); + } } diff --git a/core/dvxWm.h b/core/dvxWm.h index 8f2b00a..d12fddc 100644 --- a/core/dvxWm.h +++ b/core/dvxWm.h @@ -79,6 +79,15 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked // Insert a horizontal separator line. Separators are not interactive. void wmAddMenuSeparator(MenuT *menu); +// Query or set the checked state of a menu item by command ID. +// Searches all menus in the menu bar. For radio items, setting +// checked=true also unchecks other radio items in the same group. +bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id); +void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked); + +// Enable or disable a menu item by command ID. +void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled); + // Create a cascading submenu attached to the parent menu. Returns the // child MenuT to populate, or NULL on allocation failure. // The child MenuT is heap-allocated and freed when the parent window diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index c15fc7c..cb6ab83 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -1306,7 +1306,7 @@ void dvxMemResetApp(int32_t appId) { // is silently ignored. int32_t platformMkdirRecursive(const char *path) { - char buf[260]; + char buf[DVX_MAX_PATH]; strncpy(buf, path, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; diff --git a/core/widgetClass.c b/core/widgetClass.c index cfbdffe..54f2f9d 100644 --- a/core/widgetClass.c +++ b/core/widgetClass.c @@ -32,7 +32,7 @@ static ApiMapEntryT *sApiMap = NULL; // stb_ds string hashmap: key = widget name, value = iface + path typedef struct { const WgtIfaceT *iface; - char path[260]; + char path[DVX_MAX_PATH]; int32_t pathIndex; // 1-based: 1 = first widget from this file, 2 = second, etc. } IfaceEntryT; diff --git a/loader/loaderMain.c b/loader/loaderMain.c index a8c2aea..8a25841 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -66,7 +66,7 @@ void dvxLog(const char *fmt, ...) { // ============================================================ typedef struct { - char path[260]; + char path[DVX_MAX_PATH]; char baseName[16]; char **deps; bool loaded; @@ -134,7 +134,7 @@ static void extractBaseName(const char *path, const char *ext, char *out, int32_ static void readDeps(ModuleT *mod) { // Build dep file path: replace extension with .dep - char depPath[260]; + char depPath[DVX_MAX_PATH]; strncpy(depPath, mod->path, sizeof(depPath) - 1); depPath[sizeof(depPath) - 1] = '\0'; @@ -200,7 +200,7 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { continue; } - char path[260]; + char path[DVX_MAX_PATH]; snprintf(path, sizeof(path), "%s/%s", dirPath, name); // Check for matching extension @@ -443,7 +443,7 @@ static void *findSymbol(void **handles, const char *symbol) { int main(int argc, char *argv[]) { // Change to the directory containing the executable so relative // paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly. - char exeDir[260]; + char exeDir[DVX_MAX_PATH]; strncpy(exeDir, argv[0], sizeof(exeDir) - 1); exeDir[sizeof(exeDir) - 1] = '\0'; diff --git a/shell/shellApp.c b/shell/shellApp.c index 26068b7..042c4b7 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -312,7 +312,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { // allows multiple instances. We read the descriptor from the existing // slot directly -- no need to dlopen again. const char *loadPath = path; - char tempPath[260] = {0}; + char tempPath[DVX_MAX_PATH] = {0}; ShellAppT *existing = findLoadedPath(path); if (existing) { @@ -428,6 +428,9 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { app->dxeCtx->shellCtx = ctx; app->dxeCtx->appId = id; + // Store the full app path so the app can load its own resources. + snprintf(app->dxeCtx->appPath, sizeof(app->dxeCtx->appPath), "%s", path); + // Derive app directory from path (everything up to the last separator). // This lets apps load resources relative to their own location rather // than the shell's working directory. diff --git a/shell/shellApp.h b/shell/shellApp.h index 3171ba8..133a55e 100644 --- a/shell/shellApp.h +++ b/shell/shellApp.h @@ -62,8 +62,9 @@ typedef struct { typedef struct { AppContextT *shellCtx; // the shell's GUI context int32_t appId; // this app's ID - char appDir[260]; // directory containing the .app file - char configDir[260]; // writable config directory (CONFIG//) + char appPath[DVX_MAX_PATH]; // full path to the .app file + char appDir[DVX_MAX_PATH]; // directory containing the .app file + char configDir[DVX_MAX_PATH]; // writable config directory (CONFIG//) } DxeAppContextT; // ============================================================ @@ -85,8 +86,8 @@ typedef enum { typedef struct { int32_t appId; // unique ID = slot index (1-based; 0 = shell) char name[SHELL_APP_NAME_MAX]; - char path[260]; - char tempPath[260]; // temp copy path for multi-instance (empty = not a copy) + char path[DVX_MAX_PATH]; + char tempPath[DVX_MAX_PATH]; // temp copy path for multi-instance (empty = not a copy) void *dxeHandle; // dlopen() handle AppStateE state; bool hasMainLoop; diff --git a/tools/Makefile b/tools/Makefile index bbc2e96..81710a5 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -10,11 +10,14 @@ BINDIR = ../bin .PHONY: all clean -all: $(BINDIR)/dvxres +all: $(BINDIR)/dvxres $(BINDIR)/mktbicon $(BINDIR)/dvxres: dvxres.c ../core/dvxResource.c ../core/dvxResource.h | $(BINDIR) $(CC) $(CFLAGS) -o $@ dvxres.c ../core/dvxResource.c +$(BINDIR)/mktbicon: mktbicon.c | $(BINDIR) + $(CC) $(CFLAGS) -o $@ mktbicon.c + $(BINDIR): mkdir -p $(BINDIR) diff --git a/tools/mktbicon.c b/tools/mktbicon.c new file mode 100644 index 0000000..8d7cde7 --- /dev/null +++ b/tools/mktbicon.c @@ -0,0 +1,243 @@ +// mktbicon.c -- Generate 16x16 BMP toolbar icons for DVX BASIC IDE +// +// Usage: mktbicon +// Types: open, save, run, stop, code, design + +#include +#include +#include + +#define W 16 +#define H 16 + +static uint8_t pixels[H][W][3]; // BGR + +static void clear(uint8_t r, uint8_t g, uint8_t b) { + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + pixels[y][x][0] = b; + pixels[y][x][1] = g; + pixels[y][x][2] = r; + } + } +} + +static void px(int x, int y, uint8_t r, uint8_t g, uint8_t b) { + if (x >= 0 && x < W && y >= 0 && y < H) { + pixels[y][x][0] = b; + pixels[y][x][1] = g; + pixels[y][x][2] = r; + } +} + +static void rect(int x0, int y0, int w, int h, uint8_t r, uint8_t g, uint8_t b) { + for (int y = y0; y < y0 + h && y < H; y++) { + for (int x = x0; x < x0 + w && x < W; x++) { + px(x, y, r, g, b); + } + } +} + +static void hline(int x0, int y, int w, uint8_t r, uint8_t g, uint8_t b) { + for (int x = x0; x < x0 + w; x++) { + px(x, y, r, g, b); + } +} + +static void vline(int x, int y0, int h, uint8_t r, uint8_t g, uint8_t b) { + for (int y = y0; y < y0 + h; y++) { + px(x, y, r, g, b); + } +} + +static void outline(int x0, int y0, int w, int h, uint8_t r, uint8_t g, uint8_t b) { + hline(x0, y0, w, r, g, b); + hline(x0, y0 + h - 1, w, r, g, b); + vline(x0, y0, h, r, g, b); + vline(x0 + w - 1, y0, h, r, g, b); +} + + +// Open: folder icon (yellow folder with tab) +static void iconOpen(void) { + clear(192, 192, 192); + // Folder body + rect(1, 5, 14, 9, 220, 180, 50); + outline(1, 5, 14, 9, 160, 120, 20); + // Folder tab + rect(2, 3, 5, 3, 220, 180, 50); + hline(2, 3, 5, 160, 120, 20); + vline(2, 3, 2, 160, 120, 20); + vline(6, 3, 2, 160, 120, 20); + // Front flap (lighter) + rect(2, 7, 12, 6, 240, 210, 80); + hline(2, 7, 12, 160, 120, 20); +} + + +// Save: floppy disk icon (blue disk) +static void iconSave(void) { + clear(192, 192, 192); + // Disk body + rect(2, 1, 12, 14, 60, 60, 160); + outline(2, 1, 12, 14, 30, 30, 100); + // Metal slider (silver top) + rect(5, 1, 6, 5, 200, 200, 200); + outline(5, 1, 6, 5, 120, 120, 120); + // Slot in metal + rect(7, 2, 2, 3, 60, 60, 160); + // Label (white bottom) + rect(4, 9, 8, 5, 240, 240, 240); + outline(4, 9, 8, 5, 120, 120, 120); + // Lines on label + hline(5, 11, 6, 160, 160, 160); + hline(5, 12, 6, 160, 160, 160); +} + + +// Run: green play triangle +static void iconRun(void) { + clear(192, 192, 192); + // Play triangle pointing right + for (int y = 0; y < 12; y++) { + int w = (y < 6) ? y + 1 : 12 - y; + hline(4, 2 + y, w, 0, 160, 0); + } + + // Darker outline on top and bottom edges + for (int y = 0; y < 12; y++) { + int w = (y < 6) ? y + 1 : 12 - y; + px(4, 2 + y, 0, 100, 0); + px(4 + w - 1, 2 + y, 0, 100, 0); + } +} + + +// Stop: red square +static void iconStop(void) { + clear(192, 192, 192); + rect(3, 3, 10, 10, 200, 40, 40); + outline(3, 3, 10, 10, 140, 20, 20); +} + + +// Code: text/code icon (page with angle brackets) +static void iconCode(void) { + clear(192, 192, 192); + // Page + rect(3, 1, 10, 14, 255, 255, 255); + outline(3, 1, 10, 14, 80, 80, 80); + // Dog-ear fold + rect(10, 1, 3, 3, 192, 192, 192); + hline(10, 3, 3, 80, 80, 80); + vline(10, 1, 3, 80, 80, 80); + px(10, 1, 80, 80, 80); + px(11, 2, 80, 80, 80); + px(12, 3, 80, 80, 80); + // "<" bracket + px(5, 6, 0, 0, 180); + px(4, 7, 0, 0, 180); + px(5, 8, 0, 0, 180); + // ">" bracket + px(9, 6, 0, 0, 180); + px(10, 7, 0, 0, 180); + px(9, 8, 0, 0, 180); + // "/" between + px(8, 5, 0, 0, 180); + px(7, 7, 0, 0, 180); + px(6, 9, 0, 0, 180); + // Text lines + hline(5, 11, 6, 160, 160, 160); + hline(5, 13, 4, 160, 160, 160); +} + + +// Design: form/window with grid dots +static void iconDesign(void) { + clear(192, 192, 192); + // Window frame + rect(1, 1, 14, 14, 255, 255, 255); + outline(1, 1, 14, 14, 0, 0, 128); + // Title bar + rect(2, 2, 12, 3, 0, 0, 128); + // Grid dots + for (int y = 7; y < 14; y += 2) { + for (int x = 3; x < 14; x += 2) { + px(x, y, 160, 160, 160); + } + } + + // Small widget rectangle in the form + rect(4, 8, 6, 3, 192, 192, 192); + outline(4, 8, 6, 3, 80, 80, 80); +} + + +static void writeBmp(const char *path) { + int32_t rowPad = (4 - (W * 3) % 4) % 4; + int32_t rowSize = W * 3 + rowPad; + int32_t dataSize = rowSize * H; + int32_t fileSize = 54 + dataSize; + + uint8_t header[54]; + memset(header, 0, sizeof(header)); + + header[0] = 'B'; + header[1] = 'M'; + *(int32_t *)&header[2] = fileSize; + *(int32_t *)&header[10] = 54; + *(int32_t *)&header[14] = 40; + *(int32_t *)&header[18] = W; + *(int32_t *)&header[22] = H; + *(int16_t *)&header[26] = 1; + *(int16_t *)&header[28] = 24; + *(int32_t *)&header[34] = dataSize; + + FILE *f = fopen(path, "wb"); + + if (!f) { + fprintf(stderr, "Cannot write: %s\n", path); + return; + } + + fwrite(header, 1, 54, f); + + uint8_t pad[3] = {0}; + + for (int y = H - 1; y >= 0; y--) { + fwrite(pixels[y], 1, W * 3, f); + + if (rowPad > 0) { + fwrite(pad, 1, rowPad, f); + } + } + + fclose(f); +} + + +int main(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, "Usage: mktbicon \n"); + fprintf(stderr, "Types: open, save, run, stop, code, design\n"); + return 1; + } + + const char *path = argv[1]; + const char *type = argv[2]; + + if (strcmp(type, "open") == 0) { iconOpen(); } + else if (strcmp(type, "save") == 0) { iconSave(); } + else if (strcmp(type, "run") == 0) { iconRun(); } + else if (strcmp(type, "stop") == 0) { iconStop(); } + else if (strcmp(type, "code") == 0) { iconCode(); } + else if (strcmp(type, "design") == 0) { iconDesign(); } + else { + fprintf(stderr, "Unknown icon type: %s\n", type); + return 1; + } + + writeBmp(path); + printf("Generated %s (%s)\n", path, type); + return 0; +}