Unicode removed from source. Lots of bugs fixed. Close to Alpha 2!

This commit is contained in:
Scott Duensing 2026-03-20 19:10:53 -05:00
parent 70459616cc
commit 5a1332d024
103 changed files with 1682 additions and 1096 deletions

View file

@ -1,4 +1,4 @@
# DVX GUI Top-level Makefile # DVX GUI -- Top-level Makefile
# #
# Builds the full DVX stack: library, task switcher, shell, and apps. # Builds the full DVX stack: library, task switcher, shell, and apps.

View file

@ -1,4 +1,4 @@
# DVX DOS Visual eXecutive # DVX -- DOS Visual eXecutive
A Windows 3.x-style desktop shell for DOS, built with DJGPP/DPMI. Combines a A Windows 3.x-style desktop shell for DOS, built with DJGPP/DPMI. Combines a
windowed GUI compositor, cooperative task switcher, and DXE3 dynamic loading to windowed GUI compositor, cooperative task switcher, and DXE3 dynamic loading to
@ -103,20 +103,20 @@ mcopy -o -i <floppy.img> bin/dvx.exe ::DVX.EXE
mcopy -s -o -i <floppy.img> bin/apps ::APPS mcopy -s -o -i <floppy.img> bin/apps ::APPS
``` ```
The `apps/` directory structure must be preserved on the target Program Manager The `apps/` directory structure must be preserved on the target -- Program Manager
recursively scans `apps/` for `.app` files at startup. recursively scans `apps/` for `.app` files at startup.
## Documentation ## Documentation
Each component directory has its own README with detailed API reference: Each component directory has its own README with detailed API reference:
- [`dvx/README.md`](dvx/README.md) GUI library architecture and API - [`dvx/README.md`](dvx/README.md) -- GUI library architecture and API
- [`tasks/README.md`](tasks/README.md) Task switcher API - [`tasks/README.md`](tasks/README.md) -- Task switcher API
- [`dvxshell/README.md`](dvxshell/README.md) Shell internals and DXE app contract - [`dvxshell/README.md`](dvxshell/README.md) -- Shell internals and DXE app contract
- [`apps/README.md`](apps/README.md) Writing DXE applications - [`apps/README.md`](apps/README.md) -- Writing DXE applications
- [`rs232/README.md`](rs232/README.md) Serial port driver - [`rs232/README.md`](rs232/README.md) -- Serial port driver
- [`packet/README.md`](packet/README.md) Packet transport protocol - [`packet/README.md`](packet/README.md) -- Packet transport protocol
- [`security/README.md`](security/README.md) Cryptographic primitives - [`security/README.md`](security/README.md) -- Cryptographic primitives
- [`seclink/README.md`](seclink/README.md) Secure serial link - [`seclink/README.md`](seclink/README.md) -- Secure serial link
- [`proxy/README.md`](proxy/README.md) Linux SecLink proxy - [`proxy/README.md`](proxy/README.md) -- Linux SecLink proxy
- [`termdemo/README.md`](termdemo/README.md) Encrypted terminal demo - [`termdemo/README.md`](termdemo/README.md) -- Encrypted terminal demo

View file

@ -1,4 +1,4 @@
# DVX Shell Applications Makefile builds DXE3 modules # DVX Shell Applications Makefile -- builds DXE3 modules
DJGPP_PREFIX = $(HOME)/djgpp/djgpp DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib

View file

@ -1,4 +1,4 @@
// clock.c Clock DXE application (main-loop with tsYield) // clock.c -- Clock DXE application (main-loop with tsYield)
// //
// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback- // This is a main-loop DXE app (hasMainLoop = true), in contrast to callback-
// only apps like notepad or progman. The difference is fundamental: // only apps like notepad or progman. The difference is fundamental:
@ -8,14 +8,14 @@
// to cooperatively yield to the shell and other tasks. // to cooperatively yield to the shell and other tasks.
// //
// A main-loop app is needed when the app has ongoing work that can't be // A main-loop app is needed when the app has ongoing work that can't be
// expressed purely as event callbacks here, polling the system clock // expressed purely as event callbacks -- here, polling the system clock
// every second and repainting. The shell allocates a dedicated task stack // every second and repainting. The shell allocates a dedicated task stack
// and schedules this task alongside the main event loop. // and schedules this task alongside the main event loop.
// //
// The onPaint/onClose callbacks still execute in task 0 during dvxUpdate(), // The onPaint/onClose callbacks still execute in task 0 during dvxUpdate(),
// not in this task. The main loop only handles the clock-tick polling; the // not in this task. The main loop only handles the clock-tick polling; the
// actual rendering and window management happen through the normal callback // actual rendering and window management happen through the normal callback
// path. tsYield() is what makes this cooperative without it, this task // path. tsYield() is what makes this cooperative -- without it, this task
// would starve the shell and all other apps. // would starve the shell and all other apps.
#include "dvxApp.h" #include "dvxApp.h"
@ -87,7 +87,7 @@ static void onClose(WindowT *win) {
// onPaint demonstrates the raw paint callback approach (no widget tree). // onPaint demonstrates the raw paint callback approach (no widget tree).
// Instead of using wgtInitWindow + widgets, this app renders directly into // Instead of using wgtInitWindow + widgets, this app renders directly into
// the window's content buffer. This is the lower-level alternative to the // the window's content buffer. This is the lower-level alternative to the
// widget system useful for custom rendering like this centered clock display. // widget system -- useful for custom rendering like this centered clock display.
// //
// We create a temporary DisplayT struct pointing at the window's content buffer // We create a temporary DisplayT struct pointing at the window's content buffer
// so the drawing primitives (rectFill, drawText) can operate on the window // so the drawing primitives (rectFill, drawText) can operate on the window
@ -197,13 +197,13 @@ int32_t appMain(DxeAppContextT *ctx) {
sWin->onClose = onClose; sWin->onClose = onClose;
sWin->onPaint = onPaint; sWin->onPaint = onPaint;
// Initial paint dvxInvalidateWindow calls onPaint automatically // Initial paint -- dvxInvalidateWindow calls onPaint automatically
dvxInvalidateWindow(ac, sWin); dvxInvalidateWindow(ac, sWin);
// Main loop: check if the second has changed, repaint if so, then yield. // Main loop: check if the second has changed, repaint if so, then yield.
// tsYield() transfers control back to the shell's task scheduler. // tsYield() transfers control back to the shell's task scheduler.
// On a 486, time() resolution is 1 second, so we yield many times per // On a 486, time() resolution is 1 second, so we yield many times per
// second between actual updates this keeps CPU usage near zero. // second between actual updates -- this keeps CPU usage near zero.
// dvxInvalidateWindow marks the window dirty and calls onPaint to // dvxInvalidateWindow marks the window dirty and calls onPaint to
// update the content buffer before the compositor flushes it. // update the content buffer before the compositor flushes it.
while (!sState.quit) { while (!sState.quit) {

View file

@ -1,10 +1,10 @@
// ctrlpanel.c DVX Control Panel // ctrlpanel.c -- DVX Control Panel
// //
// System configuration app with four tabs: // System configuration app with four tabs:
// Mouse scroll direction, double-click speed, acceleration // Mouse -- scroll direction, double-click speed, acceleration
// Colors all 18 system colors, theme load/save // Colors -- all 18 system colors, theme load/save
// Desktop wallpaper image (stretch mode) // Desktop -- wallpaper image (stretch mode)
// Video resolution and color depth // Video -- resolution and color depth
// //
// Changes preview live. OK saves to DVX.INI, Cancel reverts to the // Changes preview live. OK saves to DVX.INI, Cancel reverts to the
// state captured when the control panel was opened. // state captured when the control panel was opened.
@ -72,7 +72,7 @@ static int32_t sSavedAccel;
static int32_t sSavedVideoW; static int32_t sSavedVideoW;
static int32_t sSavedVideoH; static int32_t sSavedVideoH;
static int32_t sSavedVideoBpp; static int32_t sSavedVideoBpp;
static bool sSavedHadWallpaper; static WallpaperModeE sSavedWpMode;
// Mouse tab widgets // Mouse tab widgets
static WidgetT *sWheelDrop = NULL; static WidgetT *sWheelDrop = NULL;
@ -94,6 +94,7 @@ static WidgetT *sColorSwatch = NULL;
// Desktop tab widgets // Desktop tab widgets
static WidgetT *sWallpaperLbl = NULL; static WidgetT *sWallpaperLbl = NULL;
static WidgetT *sWpaperList = NULL; static WidgetT *sWpaperList = NULL;
static WidgetT *sWpModeDrop = NULL;
static char sWallpaperPath[260]; static char sWallpaperPath[260];
static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array
static const char **sWpaperLabels = NULL; // stb_ds dynamic array static const char **sWpaperLabels = NULL; // stb_ds dynamic array
@ -125,6 +126,7 @@ static void onApplyTheme(WidgetT *w);
static void onBrowseTheme(WidgetT *w); static void onBrowseTheme(WidgetT *w);
static void onResetColors(WidgetT *w); static void onResetColors(WidgetT *w);
static void onApplyWallpaper(WidgetT *w); static void onApplyWallpaper(WidgetT *w);
static void onWallpaperMode(WidgetT *w);
static void onCancel(WidgetT *w); static void onCancel(WidgetT *w);
static void onChooseWallpaper(WidgetT *w); static void onChooseWallpaper(WidgetT *w);
static void onClearWallpaper(WidgetT *w); static void onClearWallpaper(WidgetT *w);
@ -167,7 +169,7 @@ static void buildColorsTab(WidgetT *page) {
static const char *colorNames[ColorCountE]; static const char *colorNames[ColorCountE];
for (int32_t i = 0; i < ColorCountE; i++) { for (int32_t i = 0; i < ColorCountE; i++) {
colorNames[i] = dvxColorName((ColorIdE)i); colorNames[i] = dvxColorLabel((ColorIdE)i);
} }
sColorList = wgtListBox(leftVbox); sColorList = wgtListBox(leftVbox);
@ -203,28 +205,37 @@ static void buildColorsTab(WidgetT *page) {
resetBtn->onClick = onResetColors; resetBtn->onClick = onResetColors;
resetBtn->prefW = wgtPixels(60); resetBtn->prefW = wgtPixels(60);
// Right side: RGB sliders // Right side: RGB sliders in a non-weighted inner box so they stay
// at natural size. The outer box absorbs extra vertical space.
WidgetT *rightVbox = wgtVBox(hbox); WidgetT *rightVbox = wgtVBox(hbox);
rightVbox->weight = 50; rightVbox->weight = 50;
rightVbox->spacing = wgtPixels(4);
wgtLabel(rightVbox, "Red:"); WidgetT *sliderBox = wgtVBox(rightVbox);
sRedSldr = wgtSlider(rightVbox, 0, 255); sliderBox->spacing = wgtPixels(4);
wgtLabel(sliderBox, "Red:");
sRedSldr = wgtSlider(sliderBox, 0, 255);
sRedSldr->onChange = onColorSlider; sRedSldr->onChange = onColorSlider;
sRedLbl = wgtLabel(rightVbox, "0"); sRedLbl = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sRedLbl, AlignEndE);
wgtLabel(rightVbox, "Green:"); wgtLabel(sliderBox, "Green:");
sGreenSldr = wgtSlider(rightVbox, 0, 255); sGreenSldr = wgtSlider(sliderBox, 0, 255);
sGreenSldr->onChange = onColorSlider; sGreenSldr->onChange = onColorSlider;
sGreenLbl = wgtLabel(rightVbox, "0"); sGreenLbl = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sGreenLbl, AlignEndE);
wgtLabel(rightVbox, "Blue:"); wgtLabel(sliderBox, "Blue:");
sBlueSldr = wgtSlider(rightVbox, 0, 255); sBlueSldr = wgtSlider(sliderBox, 0, 255);
sBlueSldr->onChange = onColorSlider; sBlueSldr->onChange = onColorSlider;
sBlueLbl = wgtLabel(rightVbox, "0"); sBlueLbl = wgtLabel(sliderBox, "0");
wgtLabelSetAlign(sBlueLbl, AlignEndE);
wgtLabel(rightVbox, "Preview:"); wgtLabel(sliderBox, "Preview:");
sColorSwatch = wgtCanvas(rightVbox, 64, 24); sColorSwatch = wgtCanvas(sliderBox, 64, 24);
// Absorb remaining vertical space below the sliders
wgtSpacer(rightVbox)->weight = 100;
updateColorSliders(); updateColorSliders();
} }
@ -259,6 +270,15 @@ static void buildDesktopTab(WidgetT *page) {
WidgetT *clearBtn = wgtButton(btnRow, "Clear"); WidgetT *clearBtn = wgtButton(btnRow, "Clear");
clearBtn->onClick = onClearWallpaper; clearBtn->onClick = onClearWallpaper;
clearBtn->prefW = wgtPixels(90); clearBtn->prefW = wgtPixels(90);
wgtSpacer(btnRow)->weight = 100;
wgtLabel(btnRow, "Mode:");
static const char *modeItems[] = {"Stretch", "Tile", "Center"};
sWpModeDrop = wgtDropdown(btnRow);
sWpModeDrop->onChange = onWallpaperMode;
wgtDropdownSetItems(sWpModeDrop, modeItems, 3);
wgtDropdownSetSelected(sWpModeDrop, (int32_t)sAc->wallpaperMode);
} }
@ -420,7 +440,7 @@ static const char *mapAccelValue(int32_t idx) {
// ============================================================ // ============================================================
// Callbacks Mouse tab // Callbacks -- Mouse tab
// ============================================================ // ============================================================
static void onWheelChange(WidgetT *w) { static void onWheelChange(WidgetT *w) {
@ -472,7 +492,7 @@ static void onAccelChange(WidgetT *w) {
// ============================================================ // ============================================================
// Callbacks Colors tab // Callbacks -- Colors tab
// ============================================================ // ============================================================
static void onColorSelect(WidgetT *w) { static void onColorSelect(WidgetT *w) {
@ -565,7 +585,7 @@ static void onResetColors(WidgetT *w) {
// ============================================================ // ============================================================
// Callbacks Desktop tab // Callbacks -- Desktop tab
// ============================================================ // ============================================================
static void onApplyWallpaper(WidgetT *w) { static void onApplyWallpaper(WidgetT *w) {
@ -615,8 +635,17 @@ static void onClearWallpaper(WidgetT *w) {
} }
static void onWallpaperMode(WidgetT *w) {
int32_t sel = wgtDropdownGetSelected(w);
if (sel >= 0 && sel <= 2) {
dvxSetWallpaperMode(sAc, (WallpaperModeE)sel);
}
}
// ============================================================ // ============================================================
// Callbacks — Video tab // Callbacks -- Video tab
// ============================================================ // ============================================================
static int32_t sVideoConfirmResult = -1; static int32_t sVideoConfirmResult = -1;
@ -743,7 +772,7 @@ static void onVideoApply(WidgetT *w) {
// ============================================================ // ============================================================
// Callbacks OK / Cancel / Close // Callbacks -- OK / Cancel / Close
// ============================================================ // ============================================================
static void onOk(WidgetT *w) { static void onOk(WidgetT *w) {
@ -774,6 +803,16 @@ static void onOk(WidgetT *w) {
prefsRemove("desktop", "wallpaper"); prefsRemove("desktop", "wallpaper");
} }
const char *modeStr = "stretch";
if (sAc->wallpaperMode == WallpaperTileE) {
modeStr = "tile";
} else if (sAc->wallpaperMode == WallpaperCenterE) {
modeStr = "center";
}
prefsSetString("desktop", "mode", modeStr);
// Save video settings // Save video settings
prefsSetInt("video", "width", sAc->display.width); prefsSetInt("video", "width", sAc->display.width);
prefsSetInt("video", "height", sAc->display.height); prefsSetInt("video", "height", sAc->display.height);
@ -804,6 +843,8 @@ static void onClose(WindowT *win) {
// saveSnapshot / restoreSnapshot // saveSnapshot / restoreSnapshot
// ============================================================ // ============================================================
static char sSavedWallpaperPath[260];
static void saveSnapshot(void) { static void saveSnapshot(void) {
memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb)); memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb));
sSavedWheelDir = sAc->wheelDirection; sSavedWheelDir = sAc->wheelDirection;
@ -812,16 +853,11 @@ static void saveSnapshot(void) {
sSavedVideoW = sAc->display.width; sSavedVideoW = sAc->display.width;
sSavedVideoH = sAc->display.height; sSavedVideoH = sAc->display.height;
sSavedVideoBpp = sAc->display.format.bitsPerPixel; sSavedVideoBpp = sAc->display.format.bitsPerPixel;
sSavedHadWallpaper = (sAc->wallpaperBuf != NULL);
const char *wp = prefsGetString("desktop", "wallpaper", NULL); // Save the active wallpaper path and mode from the context
snprintf(sSavedWallpaperPath, sizeof(sSavedWallpaperPath), "%s", sAc->wallpaperPath);
if (wp) { snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sAc->wallpaperPath);
strncpy(sWallpaperPath, wp, sizeof(sWallpaperPath) - 1); sSavedWpMode = sAc->wallpaperMode;
sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0';
} else {
sWallpaperPath[0] = '\0';
}
} }
@ -842,9 +878,11 @@ static void restoreSnapshot(void) {
dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp); dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp);
} }
// Restore wallpaper // Restore wallpaper mode and image
if (sSavedHadWallpaper && sWallpaperPath[0]) { sAc->wallpaperMode = sSavedWpMode;
dvxSetWallpaper(sAc, sWallpaperPath);
if (sSavedWallpaperPath[0]) {
dvxSetWallpaper(sAc, sSavedWallpaperPath);
} else { } else {
dvxSetWallpaper(sAc, NULL); dvxSetWallpaper(sAc, NULL);
} }

View file

@ -1,4 +1,4 @@
// dvxdemo.c DVX GUI demonstration app (DXE version) // dvxdemo.c -- DVX GUI demonstration app (DXE version)
// //
// Callback-only DXE app (hasMainLoop = false) that opens several windows // Callback-only DXE app (hasMainLoop = false) that opens several windows
// showcasing the DVX widget system, paint callbacks, menus, accelerators, etc. // showcasing the DVX widget system, paint callbacks, menus, accelerators, etc.
@ -12,7 +12,7 @@
// - setupTerminalWindow: ANSI terminal emulator widget // - setupTerminalWindow: ANSI terminal emulator widget
// //
// Each window is independent; closing one doesn't affect the others. // Each window is independent; closing one doesn't affect the others.
// The app has no persistent state it's purely a showcase. // The app has no persistent state -- it's purely a showcase.
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxDialog.h" #include "dvxDialog.h"
@ -234,7 +234,7 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
// Walk up the widget tree to find the root, then use wgtFind() to locate // Walk up the widget tree to find the root, then use wgtFind() to locate
// the named status label. This pattern avoids static coupling between the // the named status label. This pattern avoids static coupling between the
// button and the status label they're connected only by the name string. // button and the status label -- they're connected only by the name string.
static void onOkClick(WidgetT *w) { static void onOkClick(WidgetT *w) {
WidgetT *root = w; WidgetT *root = w;
@ -314,7 +314,7 @@ static void onPaintPattern(WindowT *win, RectT *dirtyArea) {
// Renders text manually using the bitmap font glyph data, bypassing the // Renders text manually using the bitmap font glyph data, bypassing the
// normal drawText helper. This demonstrates direct glyph rendering and // normal drawText helper. This demonstrates direct glyph rendering and
// also serves as a regression test for the font subsystem if the glyph // also serves as a regression test for the font subsystem -- if the glyph
// data format ever changes, this paint callback would visibly break. // data format ever changes, this paint callback would visibly break.
static void onPaintText(WindowT *win, RectT *dirtyArea) { static void onPaintText(WindowT *win, RectT *dirtyArea) {
(void)dirtyArea; (void)dirtyArea;
@ -424,7 +424,7 @@ static void onToolbarClick(WidgetT *w) {
// ============================================================ // ============================================================
// setupControlsWindow advanced widgets with tabs // setupControlsWindow -- advanced widgets with tabs
// ============================================================ // ============================================================
// Item arrays are static because dropdown/combobox widgets store pointers, // Item arrays are static because dropdown/combobox widgets store pointers,
@ -790,7 +790,7 @@ static void setupControlsWindow(void) {
// ============================================================ // ============================================================
// setupMainWindow info window + paint demos // setupMainWindow -- info window + paint demos
// ============================================================ // ============================================================
// Creates three windows that demonstrate raw onPaint rendering (no widgets): // Creates three windows that demonstrate raw onPaint rendering (no widgets):
@ -931,7 +931,7 @@ static void setupMainWindow(void) {
// ============================================================ // ============================================================
// setupTerminalWindow ANSI terminal widget demo // setupTerminalWindow -- ANSI terminal widget demo
// ============================================================ // ============================================================
// Creates an ANSI terminal widget with sample output demonstrating all // Creates an ANSI terminal widget with sample output demonstrating all
@ -1014,13 +1014,13 @@ static void setupTerminalWindow(void) {
// ============================================================ // ============================================================
// setupWidgetDemo form with accelerators // setupWidgetDemo -- form with accelerators
// ============================================================ // ============================================================
// Demonstrates the standard form pattern: labeled inputs in frames, checkbox // Demonstrates the standard form pattern: labeled inputs in frames, checkbox
// and radio groups, list boxes (single and multi-select with context menus), // and radio groups, list boxes (single and multi-select with context menus),
// and a button row. The '&' in label text marks the Alt+key mnemonic that // and a button row. The '&' in label text marks the Alt+key mnemonic that
// moves focus to the associated widget this is the accelerator mechanism. // moves focus to the associated widget -- this is the accelerator mechanism.
static void setupWidgetDemo(void) { static void setupWidgetDemo(void) {
WindowT *win = dvxCreateWindow(sAc, "Widget Demo", 80, 200, 280, 360, true); WindowT *win = dvxCreateWindow(sAc, "Widget Demo", 80, 200, 280, 360, true);

View file

@ -1,4 +1,4 @@
// imgview.c DVX Image Viewer // imgview.c -- DVX Image Viewer
// //
// Displays BMP, PNG, JPG, and GIF images. The image is scaled to fit // Displays BMP, PNG, JPG, and GIF images. The image is scaled to fit
// the window while preserving aspect ratio. Resize the window to zoom. // the window while preserving aspect ratio. Resize the window to zoom.
@ -65,15 +65,19 @@ static uint8_t *sScaled = NULL;
static int32_t sScaledW = 0; static int32_t sScaledW = 0;
static int32_t sScaledH = 0; static int32_t sScaledH = 0;
static int32_t sScaledPitch = 0; static int32_t sScaledPitch = 0;
static int32_t sLastFitW = 0; // window size the image was last scaled for
static int32_t sLastFitH = 0;
// ============================================================ // ============================================================
// buildScaled scale source image to fit window // buildScaled -- scale source image to fit window
// ============================================================ // ============================================================
static void buildScaled(int32_t fitW, int32_t fitH) { static void buildScaled(int32_t fitW, int32_t fitH) {
free(sScaled); free(sScaled);
sScaled = NULL; sScaled = NULL;
sLastFitW = fitW;
sLastFitH = fitH;
if (!sImgRgb || fitW < 1 || fitH < 1) { if (!sImgRgb || fitW < 1 || fitH < 1) {
return; return;
@ -113,7 +117,6 @@ static void buildScaled(int32_t fitW, int32_t fitH) {
int32_t srcStride = sImgW * 3; int32_t srcStride = sImgW * 3;
for (int32_t y = 0; y < dstH; y++) { for (int32_t y = 0; y < dstH; y++) {
// Yield every 32 rows so the UI stays responsive
if ((y & 31) == 0 && y > 0) { if ((y & 31) == 0 && y > 0) {
dvxUpdate(sAc); dvxUpdate(sAc);
} }
@ -236,6 +239,15 @@ static void onMenu(WindowT *win, int32_t menuId) {
static void onPaint(WindowT *win, RectT *dirty) { static void onPaint(WindowT *win, RectT *dirty) {
(void)dirty; (void)dirty;
// If the resize drag has ended and the window size changed since
// the last scale, rebuild now. During drag, just show the old
// scaled image (or dark background) to avoid expensive per-frame scaling.
if (sImgRgb && sAc->stack.resizeWindow < 0) {
if (sLastFitW != win->contentW || sLastFitH != win->contentH) {
buildScaled(win->contentW, win->contentH);
}
}
DisplayT cd = sAc->display; DisplayT cd = sAc->display;
cd.backBuf = win->contentBuf; cd.backBuf = win->contentBuf;
cd.width = win->contentW; cd.width = win->contentW;
@ -250,22 +262,42 @@ static void onPaint(WindowT *win, RectT *dirty) {
uint32_t bg = packColor(&sAc->display, 32, 32, 32); uint32_t bg = packColor(&sAc->display, 32, 32, 32);
rectFill(&cd, &sAc->blitOps, 0, 0, win->contentW, win->contentH, bg); rectFill(&cd, &sAc->blitOps, 0, 0, win->contentW, win->contentH, bg);
// Blit scaled image centered // Blit scaled image centered, clipped to content bounds
if (sScaled) { if (sScaled) {
int32_t offX = (win->contentW - sScaledW) / 2; int32_t offX = (win->contentW - sScaledW) / 2;
int32_t offY = (win->contentH - sScaledH) / 2; int32_t offY = (win->contentH - sScaledH) / 2;
int32_t bpp = sAc->display.format.bytesPerPixel; int32_t bpp = sAc->display.format.bytesPerPixel;
for (int32_t y = 0; y < sScaledH; y++) { // Compute visible region (clip source and dest)
int32_t dstY = offY + y; int32_t srcX = 0;
int32_t srcY = 0;
int32_t blitW = sScaledW;
int32_t blitH = sScaledH;
if (dstY < 0 || dstY >= win->contentH) { if (offX < 0) {
continue; srcX = -offX;
blitW += offX;
offX = 0;
} }
uint8_t *src = sScaled + y * sScaledPitch; if (offY < 0) {
uint8_t *dst = win->contentBuf + dstY * win->contentPitch + offX * bpp; srcY = -offY;
memcpy(dst, src, sScaledW * bpp); blitH += offY;
offY = 0;
}
if (offX + blitW > win->contentW) {
blitW = win->contentW - offX;
}
if (offY + blitH > win->contentH) {
blitH = win->contentH - offY;
}
for (int32_t y = 0; y < blitH; y++) {
uint8_t *src = sScaled + (srcY + y) * sScaledPitch + srcX * bpp;
uint8_t *dst = win->contentBuf + (offY + y) * win->contentPitch + offX * bpp;
memcpy(dst, src, blitW * bpp);
} }
} }
} }
@ -277,7 +309,10 @@ static void onPaint(WindowT *win, RectT *dirty) {
static void onResize(WindowT *win, int32_t contentW, int32_t contentH) { static void onResize(WindowT *win, int32_t contentW, int32_t contentH) {
(void)win; (void)win;
buildScaled(contentW, contentH); (void)contentW;
(void)contentH;
// Don't rescale here -- onPaint handles it after the drag ends.
// This avoids expensive bilinear scaling on every frame of a drag.
} }

View file

@ -1,4 +1,4 @@
// notepad.c Simple text editor DXE application (callback-only) // notepad.c -- Simple text editor DXE application (callback-only)
// //
// A callback-only DXE app (hasMainLoop = false) that provides basic text // A callback-only DXE app (hasMainLoop = false) that provides basic text
// editing with file I/O. Demonstrates the standard DXE app pattern: // editing with file I/O. Demonstrates the standard DXE app pattern:
@ -87,7 +87,7 @@ AppDescriptorT appDescriptor = {
// Dirty tracking // Dirty tracking
// ============================================================ // ============================================================
// djb2-xor hash for dirty detection. Not cryptographic just a fast way // djb2-xor hash for dirty detection. Not cryptographic -- just a fast way
// to detect changes without storing a full copy of the last-saved text. // to detect changes without storing a full copy of the last-saved text.
// False negatives are theoretically possible but vanishingly unlikely for // False negatives are theoretically possible but vanishingly unlikely for
// text edits. This avoids the memory cost of keeping a shadow buffer. // text edits. This avoids the memory cost of keeping a shadow buffer.
@ -181,7 +181,7 @@ static void doOpen(void) {
} }
// Read entire file into a temporary buffer. Files larger than the // Read entire file into a temporary buffer. Files larger than the
// TextArea's buffer are silently truncated this matches the behavior // TextArea's buffer are silently truncated -- this matches the behavior
// of Windows Notepad on large files. // of Windows Notepad on large files.
fseek(f, 0, SEEK_END); fseek(f, 0, SEEK_END);
long size = ftell(f); long size = ftell(f);
@ -368,7 +368,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// The widget tree is minimal: just a TextArea filling the entire content // The widget tree is minimal: just a TextArea filling the entire content
// area (weight=100). The TextArea widget provides editing, scrolling, // area (weight=100). The TextArea widget provides editing, scrolling,
// selection, copy/paste, and undo all driven by keyboard events that // selection, copy/paste, and undo -- all driven by keyboard events that
// the shell dispatches to the focused widget. // the shell dispatches to the focused widget.
WidgetT *root = wgtInitWindow(ac, sWin); WidgetT *root = wgtInitWindow(ac, sWin);

View file

@ -1,4 +1,4 @@
// progman.c Program Manager application for DVX Shell // progman.c -- Program Manager application for DVX Shell
// //
// Displays a grid of available apps from the apps/ directory. // Displays a grid of available apps from the apps/ directory.
// Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc). // Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc).
@ -20,8 +20,10 @@
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxDialog.h" #include "dvxDialog.h"
#include "dvxPrefs.h"
#include "dvxWidget.h" #include "dvxWidget.h"
#include "dvxWm.h" #include "dvxWm.h"
#include "platform/dvxPlatform.h"
#include "shellApp.h" #include "shellApp.h"
#include "shellTaskMgr.h" #include "shellTaskMgr.h"
#include "shellInfo.h" #include "shellInfo.h"
@ -144,7 +146,7 @@ static void buildPmWindow(void) {
wmAddMenuItem(fileMenu, "E&xit Shell", CMD_EXIT); wmAddMenuItem(fileMenu, "E&xit Shell", CMD_EXIT);
MenuT *optMenu = wmAddMenu(menuBar, "&Options"); MenuT *optMenu = wmAddMenu(menuBar, "&Options");
wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, false); wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, sMinOnRun);
MenuT *windowMenu = wmAddMenu(menuBar, "&Window"); MenuT *windowMenu = wmAddMenu(menuBar, "&Window");
wmAddMenuItem(windowMenu, "&Cascade", CMD_CASCADE); wmAddMenuItem(windowMenu, "&Cascade", CMD_CASCADE);
@ -293,6 +295,9 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
case CMD_MIN_ON_RUN: case CMD_MIN_ON_RUN:
sMinOnRun = !sMinOnRun; sMinOnRun = !sMinOnRun;
shellEnsureConfigDir(sCtx);
prefsSetBool("options", "minimizeOnRun", sMinOnRun);
prefsSave();
break; break;
case CMD_ABOUT: case CMD_ABOUT:
@ -346,7 +351,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
char fullPath[MAX_PATH_LEN]; char fullPath[MAX_PATH_LEN];
snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, ent->d_name); snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, ent->d_name);
// Check if this is a directory recurse into it // Check if this is a directory -- recurse into it
struct stat st; struct stat st;
if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) { if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) {
@ -431,7 +436,7 @@ static void showSystemInfo(void) {
ta->weight = 100; ta->weight = 100;
wgtSetText(ta, info); wgtSetText(ta, info);
// Don't disable wgtSetEnabled(false) blocks all input including scrollbar // Don't disable -- wgtSetEnabled(false) blocks all input including scrollbar
wgtSetReadOnly(ta, true); wgtSetReadOnly(ta, true);
} }
@ -467,6 +472,12 @@ int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx; sCtx = ctx;
sAc = ctx->shellCtx; sAc = ctx->shellCtx;
// Load saved preferences
char prefsPath[260];
shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath));
prefsLoad(prefsPath);
sMinOnRun = prefsGetBool("options", "minimizeOnRun", false);
scanAppsDir(); scanAppsDir();
buildPmWindow(); buildPmWindow();

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
// dvx_app.h Layer 5: Application API for DVX GUI // dvx_app.h -- Layer 5: Application API for DVX GUI
// //
// The topmost layer and the public-facing API for applications. Aggregates // The topmost layer and the public-facing API for applications. Aggregates
// all lower layers into a single AppContextT and provides a clean interface // all lower layers into a single AppContextT and provides a clean interface
// for window creation, event dispatch, and utilities. Applications interact // for window creation, event dispatch, and utilities. Applications interact
// exclusively through dvx*() functions and window callbacks they never // exclusively through dvx*() functions and window callbacks -- they never
// need to call the lower WM, compositor, or draw layers directly. // need to call the lower WM, compositor, or draw layers directly.
// //
// The event loop (dvxRun/dvxUpdate) follows a cooperative model: poll mouse // The event loop (dvxRun/dvxUpdate) follows a cooperative model: poll mouse
// and keyboard, dispatch events to the focused window, run the compositor // and keyboard, dispatch events to the focused window, run the compositor
// for dirty regions, then yield. There's no preemptive scheduling the // for dirty regions, then yield. There's no preemptive scheduling -- the
// application must return from callbacks promptly. // application must return from callbacks promptly.
#ifndef DVX_APP_H #ifndef DVX_APP_H
#define DVX_APP_H #define DVX_APP_H
@ -26,7 +26,7 @@
// ============================================================ // ============================================================
// //
// Single monolithic context that owns all GUI state. Allocated on the // Single monolithic context that owns all GUI state. Allocated on the
// caller's stack (or statically) no internal heap allocation for the // caller's stack (or statically) -- no internal heap allocation for the
// context itself. This "god struct" approach keeps the API simple (one // context itself. This "god struct" approach keeps the API simple (one
// pointer to pass everywhere) and avoids the overhead of a handle-based // pointer to pass everywhere) and avoids the overhead of a handle-based
// system with opaque lookups. The tradeoff is a large struct, but on DOS // system with opaque lookups. The tradeoff is a large struct, but on DOS
@ -82,7 +82,7 @@ typedef struct AppContextT {
void *ctrlEscCtx; void *ctrlEscCtx;
void (*onTitleChange)(void *ctx); // called when any window title changes void (*onTitleChange)(void *ctx); // called when any window title changes
void *titleChangeCtx; void *titleChangeCtx;
// Tooltip state tooltip appears after the mouse hovers over a widget // Tooltip state -- tooltip appears after the mouse hovers over a widget
// with a tooltip string for a brief delay. Pre-computing W/H avoids // with a tooltip string for a brief delay. Pre-computing W/H avoids
// re-measuring on every paint frame. // re-measuring on every paint frame.
clock_t tooltipHoverStart; // when mouse stopped moving clock_t tooltipHoverStart; // when mouse stopped moving
@ -104,12 +104,13 @@ typedef struct AppContextT {
// Available video modes (enumerated once at init) // Available video modes (enumerated once at init)
VideoModeInfoT *videoModes; // stb_ds dynamic array VideoModeInfoT *videoModes; // stb_ds dynamic array
int32_t videoModeCount; int32_t videoModeCount;
// Wallpaper — pre-scaled to screen dimensions in native pixel format. // Wallpaper -- pre-rendered to screen dimensions in native pixel format.
// NULL means no wallpaper (solid desktop color). wallpaperPath is // NULL means no wallpaper (solid desktop color). wallpaperPath is
// kept so the image can be reloaded after a video mode change. // kept so the image can be reloaded after a video mode or mode change.
uint8_t *wallpaperBuf; // pixel data (screen width * height * bpp/8) uint8_t *wallpaperBuf; // pixel data (screen width * height * bpp/8)
int32_t wallpaperPitch; // bytes per row int32_t wallpaperPitch; // bytes per row
char wallpaperPath[260]; // source image path (empty = none) char wallpaperPath[260]; // source image path (empty = none)
WallpaperModeE wallpaperMode; // stretch, tile, or center
} AppContextT; } AppContextT;
// Initialize the entire GUI stack: video mode, input devices, font, // Initialize the entire GUI stack: video mode, input devices, font,
@ -154,14 +155,21 @@ bool dvxSaveTheme(const AppContextT *ctx, const char *filename);
// Return the INI key name for a color ID (e.g. "desktop", "windowFace"). // Return the INI key name for a color ID (e.g. "desktop", "windowFace").
const char *dvxColorName(ColorIdE id); const char *dvxColorName(ColorIdE id);
// Return a human-readable display label (e.g. "Desktop", "Cursor Color").
const char *dvxColorLabel(ColorIdE id);
// ============================================================ // ============================================================
// Wallpaper // Wallpaper
// ============================================================ // ============================================================
// Load and apply a wallpaper image (stretched to screen). Pass NULL // Load and apply a wallpaper image. Uses the current wallpaperMode
// to clear the wallpaper and revert to the solid desktop color. // (stretch/tile/center). Pass NULL to clear the wallpaper.
bool dvxSetWallpaper(AppContextT *ctx, const char *path); bool dvxSetWallpaper(AppContextT *ctx, const char *path);
// Change the wallpaper display mode and re-render the buffer.
// Has no effect if no wallpaper is loaded.
void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode);
// Return the list of available video modes (enumerated at init). // Return the list of available video modes (enumerated at init).
// count receives the number of entries. // count receives the number of entries.
const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count); const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count);
@ -255,7 +263,7 @@ void dvxFreeAccelTable(AccelTableT *table);
// for menu item IDs so the same handler works for both menus and hotkeys. // for menu item IDs so the same handler works for both menus and hotkeys.
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId); void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId);
// Window arrangement functions operate on all visible, non-minimized // Window arrangement functions -- operate on all visible, non-minimized
// windows. These mirror the classic Windows 3.x "Window" menu commands. // windows. These mirror the classic Windows 3.x "Window" menu commands.
// Cascade: offset each window diagonally by the title bar height. // Cascade: offset each window diagonally by the title bar height.
@ -285,7 +293,7 @@ void dvxFreeImage(uint8_t *data);
int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path); int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);
// Copy text to the process-wide clipboard buffer. The clipboard is a // Copy text to the process-wide clipboard buffer. The clipboard is a
// simple static buffer (not inter-process) adequate for copy/paste // simple static buffer (not inter-process) -- adequate for copy/paste
// within the DVX environment on single-tasking DOS. // within the DVX environment on single-tasking DOS.
void dvxClipboardCopy(const char *text, int32_t len); void dvxClipboardCopy(const char *text, int32_t len);

View file

@ -1,9 +1,9 @@
// dvx_comp.c Layer 3: Dirty rectangle compositor for DVX GUI (optimized) // dvx_comp.c -- Layer 3: Dirty rectangle compositor for DVX GUI (optimized)
// //
// This layer implements dirty rectangle tracking and merging. The compositor // This layer implements dirty rectangle tracking and merging. The compositor
// avoids full-screen redraws, which would be prohibitively expensive on the // avoids full-screen redraws, which would be prohibitively expensive on the
// target 486/Pentium hardware over ISA bus VESA LFB. A full 640x480x16bpp // target 486/Pentium hardware over ISA bus VESA LFB. A full 640x480x16bpp
// framebuffer is ~600KB at ISA's ~8MB/s theoretical peak, a blind full // framebuffer is ~600KB -- at ISA's ~8MB/s theoretical peak, a blind full
// flush costs ~75ms (>1 frame at 60Hz). By tracking which rectangles have // flush costs ~75ms (>1 frame at 60Hz). By tracking which rectangles have
// actually changed and flushing only those regions from the system RAM // actually changed and flushing only those regions from the system RAM
// backbuffer to the LFB, the bandwidth consumed per frame scales with the // backbuffer to the LFB, the bandwidth consumed per frame scales with the
@ -11,7 +11,7 @@
// //
// The compositing loop lives in dvxApp.c (compositeAndFlush). For each dirty // The compositing loop lives in dvxApp.c (compositeAndFlush). For each dirty
// rect, it repaints the desktop, then walks the window stack bottom-to-top // rect, it repaints the desktop, then walks the window stack bottom-to-top
// painting chrome, content, scrollbars, popup menus, and the cursor all // painting chrome, content, scrollbars, popup menus, and the cursor -- all
// clipped to the dirty rect. Only then is the dirty rect flushed to the LFB. // clipped to the dirty rect. Only then is the dirty rect flushed to the LFB.
// This means each pixel in a dirty region is written to system RAM potentially // This means each pixel in a dirty region is written to system RAM potentially
// multiple times (painter's algorithm), but the expensive LFB write happens // multiple times (painter's algorithm), but the expensive LFB write happens
@ -26,7 +26,7 @@
// overlap. A small gap tolerance absorbs jitter from mouse movement and // overlap. A small gap tolerance absorbs jitter from mouse movement and
// closely-spaced UI invalidations (e.g. title bar + content during a drag) // closely-spaced UI invalidations (e.g. title bar + content during a drag)
// without bloating merged rects excessively. The value 4 was chosen to match // without bloating merged rects excessively. The value 4 was chosen to match
// the chrome border width adjacent chrome/content invalidations merge // the chrome border width -- adjacent chrome/content invalidations merge
// naturally. // naturally.
#define DIRTY_MERGE_GAP 4 #define DIRTY_MERGE_GAP 4
@ -43,7 +43,7 @@ static inline void rectUnion(const RectT *a, const RectT *b, RectT *result);
// ============================================================ // ============================================================
// //
// Appends a dirty rect to the list. Uses a fixed-size array (MAX_DIRTY_RECTS // Appends a dirty rect to the list. Uses a fixed-size array (MAX_DIRTY_RECTS
// = 128) rather than a dynamic allocation this is called on every UI // = 128) rather than a dynamic allocation -- this is called on every UI
// mutation (drag, repaint, focus change) so allocation overhead must be zero. // mutation (drag, repaint, focus change) so allocation overhead must be zero.
// //
// When the list fills up, an eager merge pass tries to consolidate rects. // When the list fills up, an eager merge pass tries to consolidate rects.
@ -54,7 +54,7 @@ static inline void rectUnion(const RectT *a, const RectT *b, RectT *result);
// GUI mutations tend to cluster spatially. // GUI mutations tend to cluster spatially.
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) { void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
// Branch hint: degenerate rects are rare callers usually validate first // Branch hint: degenerate rects are rare -- callers usually validate first
if (__builtin_expect(w <= 0 || h <= 0, 0)) { if (__builtin_expect(w <= 0 || h <= 0, 0)) {
return; return;
} }
@ -64,7 +64,7 @@ void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
dirtyListMerge(dl); dirtyListMerge(dl);
if (dl->count >= MAX_DIRTY_RECTS) { if (dl->count >= MAX_DIRTY_RECTS) {
// Still full collapse the entire list plus the new rect into one // Still full -- collapse the entire list plus the new rect into one
// bounding box. This is a last resort; it means the next flush will // bounding box. This is a last resort; it means the next flush will
// repaint a potentially large region, but at least we won't lose // repaint a potentially large region, but at least we won't lose
// dirty information or crash. // dirty information or crash.
@ -123,7 +123,7 @@ void dirtyListInit(DirtyListT *dl) {
// Algorithm: O(N^2) pairwise sweep with bounded restarts. For each rect i, // Algorithm: O(N^2) pairwise sweep with bounded restarts. For each rect i,
// scan all rects j>i and merge any that overlap or are within DIRTY_MERGE_GAP // scan all rects j>i and merge any that overlap or are within DIRTY_MERGE_GAP
// pixels. When a merge happens, rect i grows and may now overlap rects that // pixels. When a merge happens, rect i grows and may now overlap rects that
// it previously missed, so the inner scan restarts but restarts are capped // it previously missed, so the inner scan restarts -- but restarts are capped
// at 3 per slot to prevent O(N^3) cascading in pathological layouts (e.g. // at 3 per slot to prevent O(N^3) cascading in pathological layouts (e.g.
// a diagonal scatter of tiny rects). The cap of 3 was chosen empirically: // a diagonal scatter of tiny rects). The cap of 3 was chosen empirically:
// typical GUI operations produce clustered invalidations that converge in // typical GUI operations produce clustered invalidations that converge in
@ -172,7 +172,7 @@ void dirtyListMerge(DirtyListT *dl) {
// on 486+ to move aligned 32-bit words, maximizing bus utilization. // on 486+ to move aligned 32-bit words, maximizing bus utilization.
// //
// Crucially, we flush per dirty rect AFTER all painting for that rect is // Crucially, we flush per dirty rect AFTER all painting for that rect is
// complete. This avoids visible tearing the LFB is never in a half-painted // complete. This avoids visible tearing -- the LFB is never in a half-painted
// state for any given region. // state for any given region.
void flushRect(DisplayT *d, const RectT *r) { void flushRect(DisplayT *d, const RectT *r) {
@ -187,7 +187,7 @@ void flushRect(DisplayT *d, const RectT *r) {
// Used heavily in the compositing loop to test whether a window overlaps // Used heavily in the compositing loop to test whether a window overlaps
// a dirty rect before painting it. The branch hint marks the non-overlapping // a dirty rect before painting it. The branch hint marks the non-overlapping
// case as unlikely because the compositing loop already does a coarse AABB // case as unlikely because the compositing loop already does a coarse AABB
// check before calling this when we get here, intersection is expected. // check before calling this -- when we get here, intersection is expected.
// The min/max formulation avoids branches in the hot path. // The min/max formulation avoids branches in the hot path.
bool rectIntersect(const RectT *a, const RectT *b, RectT *result) { bool rectIntersect(const RectT *a, const RectT *b, RectT *result) {
@ -246,7 +246,7 @@ static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_
// //
// Axis-aligned bounding box of two rects. Supports in-place operation // Axis-aligned bounding box of two rects. Supports in-place operation
// (result == a) for the merge loop. Note that this may grow the rect // (result == a) for the merge loop. Note that this may grow the rect
// substantially if the two inputs are far apart this is the inherent // substantially if the two inputs are far apart -- this is the inherent
// cost of bounding-box merging vs. maintaining a true region (list of // cost of bounding-box merging vs. maintaining a true region (list of
// non-overlapping rects). Bounding-box was chosen because the merge // non-overlapping rects). Bounding-box was chosen because the merge
// list is bounded to 128 entries and the extra repaint cost of a few // list is bounded to 128 entries and the extra repaint cost of a few

View file

@ -1,8 +1,8 @@
// dvx_comp.h Layer 3: Dirty rectangle compositor for DVX GUI // dvx_comp.h -- Layer 3: Dirty rectangle compositor for DVX GUI
// //
// The compositor tracks which screen regions have changed and ensures // The compositor tracks which screen regions have changed and ensures
// only those regions are redrawn and flushed to video memory. This is // only those regions are redrawn and flushed to video memory. This is
// the key to acceptable frame rates on 486/Pentium hardware a full // the key to acceptable frame rates on 486/Pentium hardware -- a full
// 640x480x32bpp frame is 1.2 MB, but a typical dirty region during // 640x480x32bpp frame is 1.2 MB, but a typical dirty region during
// normal interaction (e.g. typing in a text field) might be a few KB. // normal interaction (e.g. typing in a text field) might be a few KB.
// //
@ -40,7 +40,7 @@ void dirtyListMerge(DirtyListT *dl);
void dirtyListClear(DirtyListT *dl); void dirtyListClear(DirtyListT *dl);
// Copy a rectangle from the system RAM backbuffer to the LFB (video memory). // Copy a rectangle from the system RAM backbuffer to the LFB (video memory).
// This is the final step the only place where the real framebuffer is // This is the final step -- the only place where the real framebuffer is
// written. Uses platform-specific fast copy (rep movsd on DOS) for each // written. Uses platform-specific fast copy (rep movsd on DOS) for each
// scanline of the rect. // scanline of the rect.
void flushRect(DisplayT *d, const RectT *r); void flushRect(DisplayT *d, const RectT *r);

View file

@ -1,6 +1,6 @@
// dvxCursor.h Embedded mouse cursor bitmaps for DVX GUI // dvxCursor.h -- Embedded mouse cursor bitmaps for DVX GUI
// //
// All cursor shapes are compiled in as static const data no external // All cursor shapes are compiled in as static const data -- no external
// cursor files to load. This is intentional: the cursors are needed // cursor files to load. This is intentional: the cursors are needed
// before any file I/O infrastructure is ready, and embedding them avoids // before any file I/O infrastructure is ready, and embedding them avoids
// a runtime dependency on a file path. // a runtime dependency on a file path.
@ -37,7 +37,7 @@
// XOR data: 0 = black, 1 = white (where AND = 0) // XOR data: 0 = black, 1 = white (where AND = 0)
// //
// The cursor is a left-pointing arrow with black outline and white fill. // The cursor is a left-pointing arrow with black outline and white fill.
// Hot spot is at (0, 0) top-left corner of the arrow tip. // Hot spot is at (0, 0) -- top-left corner of the arrow tip.
static const uint16_t cursorArrowAnd[16] = { static const uint16_t cursorArrowAnd[16] = {
0x3FFF, // 0011111111111111 row 0 0x3FFF, // 0011111111111111 row 0
@ -91,42 +91,57 @@ static const uint16_t cursorArrowXor[16] = {
// .X..XX..X. // .X..XX..X.
// ..XX..XX.. // ..XX..XX..
// Left-right resize cursor -- exact 90 deg rotation of the vertical cursor.
// Left arrow tip at col 2, right arrow tip at col 13, 1px stem at row 7.
// Bit 15 = leftmost pixel (col 0), bit 0 = rightmost (col 15).
//
// XOR (1=white): Outline (AND=0 border):
// row 4: ....X......X.... row 3: ...XXX....XXX...
// row 5: ...XX......XX... row 4: ..XXXX....XXXX..
// row 6: ..XXX......XXX.. row 5: .XXXXX....XXXXX.
// row 7: .XXXXXXXXXXXX.. row 6: XXXXXXXXXXXXXX.. (1px border around all)
// row 8: ..XXX......XXX.. row 7: XXXXXXXXXXXXXXXX
// row 9: ...XX......XX... row 8: XXXXXXXXXXXXXX..
// row 10: ....X......X.... row 9: .XXXXX....XXXXX.
// row 10: ..XXXX....XXXX..
// row 11: ...XXX....XXX...
static const uint16_t cursorResizeHAnd[16] = { static const uint16_t cursorResizeHAnd[16] = {
0xFFFF, // row 0 (transparent) 0xFFFF, // 1111111111111111 row 0
0xFFFF, // row 1 0xFFFF, // 1111111111111111 row 1
0xFFFF, // row 2 0xFFFF, // 1111111111111111 row 2
0xF7EF, // 1111011111101111 row 3 arrows start 0xF18F, // 1111000110001111 row 3 outline top
0xE3C7, // 1110001111000111 row 4 0xE187, // 1110000110000111 row 4
0xC1A3, // 1100000110100011 row 5 0xC183, // 1100000110000011 row 5
0x8081, // 1000000010000001 row 6 0x8001, // 1000000000000001 row 6
0x0000, // 0000000000000000 row 7 center row 0x8001, // 1000000000000001 row 7 center
0x8081, // 1000000010000001 row 8 0x8001, // 1000000000000001 row 8
0xC1A3, // 1100000110100011 row 9 0xC183, // 1100000110000011 row 9
0xE3C7, // 1110001111000111 row 10 0xE187, // 1110000110000111 row 10
0xF7EF, // 1111011111101111 row 11 0xF18F, // 1111000110001111 row 11 outline bottom
0xFFFF, // row 12 0xFFFF, // 1111111111111111 row 12
0xFFFF, // row 13 0xFFFF, // 1111111111111111 row 13
0xFFFF, // row 14 0xFFFF, // 1111111111111111 row 14
0xFFFF // row 15 0xFFFF // 1111111111111111 row 15
}; };
static const uint16_t cursorResizeHXor[16] = { static const uint16_t cursorResizeHXor[16] = {
0x0000, // row 0 0x0000, // 0000000000000000 row 0
0x0000, // row 1 0x0000, // 0000000000000000 row 1
0x0000, // row 2 0x0000, // 0000000000000000 row 2
0x0000, // row 3 0x0000, // 0000000000000000 row 3
0x0810, // 0000100000010000 row 4 0x0420, // 0000010000100000 row 4 arrow tips
0x1C38, // 0001110000111000 row 5 0x0C30, // 0000110000110000 row 5
0x3E7C, // 0011111001111100 row 6 0x1C38, // 0001110000111000 row 6
0x7FFE, // 0111111111111110 row 7 0x3FFC, // 0011111111111100 row 7 bases + stem
0x3E7C, // 0011111001111100 row 8 0x1C38, // 0001110000111000 row 8
0x1C38, // 0001110000111000 row 9 0x0C30, // 0000110000110000 row 9
0x0810, // 0000100000010000 row 10 0x0420, // 0000010000100000 row 10 arrow tips
0x0000, // row 11 0x0000, // 0000000000000000 row 11
0x0000, // row 12 0x0000, // 0000000000000000 row 12
0x0000, // row 13 0x0000, // 0000000000000000 row 13
0x0000, // row 14 0x0000, // 0000000000000000 row 14
0x0000 // row 15 0x0000 // 0000000000000000 row 15
}; };
// ============================================================ // ============================================================
@ -302,7 +317,7 @@ static const CursorT dvxCursors[CURSOR_COUNT] = {
{ 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW { 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW
}; };
// Legacy alias kept for backward compatibility with code that predates // Legacy alias -- kept for backward compatibility with code that predates
// the multi-cursor support. // the multi-cursor support.
static const CursorT dvxCursor = { 16, 16, 0, 0, cursorArrowAnd, cursorArrowXor }; static const CursorT dvxCursor = { 16, 16, 0, 0, cursorArrowAnd, cursorArrowXor };

View file

@ -1,10 +1,10 @@
// dvxDialog.c Modal dialogs for DVX GUI // dvxDialog.c -- Modal dialogs for DVX GUI
// //
// Provides two standard dialog types: message boxes and file dialogs. // Provides two standard dialog types: message boxes and file dialogs.
// Both use the nested-event-loop modal pattern: the dialog creates a // Both use the nested-event-loop modal pattern: the dialog creates a
// window, sets it as the AppContext's modalWindow, then runs dvxUpdate // window, sets it as the AppContext's modalWindow, then runs dvxUpdate
// in a tight loop until the user dismisses the dialog. This blocks the // in a tight loop until the user dismisses the dialog. This blocks the
// caller's code flow, which is the simplest possible modal API the // caller's code flow, which is the simplest possible modal API -- the
// caller gets the result as a return value, no callbacks needed. // caller gets the result as a return value, no callbacks needed.
// //
// The nested loop approach works because dvxUpdate is re-entrant: it // The nested loop approach works because dvxUpdate is re-entrant: it
@ -15,7 +15,7 @@
// //
// State for each dialog type is stored in a single static struct (sMsgBox, // State for each dialog type is stored in a single static struct (sMsgBox,
// sFd) rather than on the heap. This means only one dialog of each type // sFd) rather than on the heap. This means only one dialog of each type
// can be active at a time, but that's fine you never need two file // can be active at a time, but that's fine -- you never need two file
// dialogs simultaneously. The static approach avoids malloc/free and // dialogs simultaneously. The static approach avoids malloc/free and
// keeps the state accessible to callback functions without threading // keeps the state accessible to callback functions without threading
// context pointers through every widget callback. // context pointers through every widget callback.
@ -110,7 +110,7 @@ static MsgBoxStateT sMsgBox;
// ============================================================ // ============================================================
// drawIconGlyph draw a simple icon shape // drawIconGlyph -- draw a simple icon shape
// ============================================================ // ============================================================
// //
// Procedurally draws message box icons using only integer math: // Procedurally draws message box icons using only integer math:
@ -122,7 +122,7 @@ static MsgBoxStateT sMsgBox;
// Circles use the integer distance-squared test: for each pixel, compute // Circles use the integer distance-squared test: for each pixel, compute
// dx*dx + dy*dy and check if it falls between INNER_R2 and OUTER_R2 // dx*dx + dy*dy and check if it falls between INNER_R2 and OUTER_R2
// to draw a 2-pixel-wide ring. This is a brute-force O(n^2) approach // to draw a 2-pixel-wide ring. This is a brute-force O(n^2) approach
// but n is only 24 pixels, so it's 576 iterations trivial even on a 486. // but n is only 24 pixels, so it's 576 iterations -- trivial even on a 486.
// //
// The inner symbols (i, !, X, ?) are drawn with hardcoded rectFill calls // The inner symbols (i, !, X, ?) are drawn with hardcoded rectFill calls
// at specific pixel offsets. This is less elegant than using the font // at specific pixel offsets. This is less elegant than using the font
@ -222,7 +222,7 @@ static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y
// //
// Button labels use '&' to mark accelerator keys (e.g., "&OK" makes // Button labels use '&' to mark accelerator keys (e.g., "&OK" makes
// Alt+O activate the button). Button IDs are stored in widget->userData // Alt+O activate the button). Button IDs are stored in widget->userData
// via intptr_t cast a common pattern when you need to associate a // via intptr_t cast -- a common pattern when you need to associate a
// small integer with a widget without allocating a separate struct. // small integer with a widget without allocating a separate struct.
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) { int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
@ -381,12 +381,12 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
// Save previous modal so stacked modals work correctly. This happens // Save previous modal so stacked modals work correctly. This happens
// when a message box opens from within a file dialog (e.g., overwrite // when a message box opens from within a file dialog (e.g., overwrite
// confirmation) the file dialog's modal is pushed and restored // confirmation) -- the file dialog's modal is pushed and restored
// when the message box closes. // when the message box closes.
WindowT *prevModal = ctx->modalWindow; WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win; ctx->modalWindow = win;
// Nested event loop blocks here until user clicks a button or closes. // Nested event loop -- blocks here until user clicks a button or closes.
// dvxUpdate handles all input/rendering; sMsgBox.done is set by the // dvxUpdate handles all input/rendering; sMsgBox.done is set by the
// button onClick callback or the window close callback. // button onClick callback or the window close callback.
while (!sMsgBox.done && ctx->running) { while (!sMsgBox.done && ctx->running) {
@ -422,7 +422,7 @@ static void onMsgBoxClose(WindowT *win) {
// ============================================================ // ============================================================
// onMsgBoxPaint custom paint: background + text/icon + widgets // onMsgBoxPaint -- custom paint: background + text/icon + widgets
// ============================================================ // ============================================================
// //
// The message box uses a custom onPaint callback rather than pure widgets // The message box uses a custom onPaint callback rather than pure widgets
@ -454,7 +454,7 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
cd.clipW = win->contentW; cd.clipW = win->contentW;
cd.clipH = win->contentH; cd.clipH = win->contentH;
// Fill background with window face color (not content bg dialog style) // Fill background with window face color (not content bg -- dialog style)
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.windowFace); rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.windowFace);
// Draw word-wrapped message text // Draw word-wrapped message text
@ -488,14 +488,14 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
// ============================================================ // ============================================================
// wordWrapDraw draw word-wrapped text // wordWrapDraw -- draw word-wrapped text
// ============================================================ // ============================================================
// //
// Simple greedy word-wrap: fill each line with as many characters as fit // Simple greedy word-wrap: fill each line with as many characters as fit
// within maxW, breaking at the last space if the line overflows. If a // within maxW, breaking at the last space if the line overflows. If a
// single word is longer than maxW, it gets its own line (may be clipped). // single word is longer than maxW, it gets its own line (may be clipped).
// Explicit newlines are honored. This is a fixed-width font, so "max // Explicit newlines are honored. This is a fixed-width font, so "max
// chars per line" is just maxW / charWidth no per-character width // chars per line" is just maxW / charWidth -- no per-character width
// accumulation needed. // accumulation needed.
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) { static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) {
@ -550,12 +550,12 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
// ============================================================ // ============================================================
// wordWrapHeight compute height of word-wrapped text // wordWrapHeight -- compute height of word-wrapped text
// ============================================================ // ============================================================
// //
// Duplicates the word-wrap logic from wordWrapDraw but only counts lines // Duplicates the word-wrap logic from wordWrapDraw but only counts lines
// instead of drawing. This is needed to compute the dialog height before // instead of drawing. This is needed to compute the dialog height before
// creating the window. The duplication is intentional combining them // creating the window. The duplication is intentional -- combining them
// into a single function with a "just measure" flag would add branching // into a single function with a "just measure" flag would add branching
// to the draw path and make both harder to read. // to the draw path and make both harder to read.
@ -650,7 +650,7 @@ static FileDialogStateT sFd;
// ============================================================ // ============================================================
// fdFilterMatch check if filename matches a glob pattern // fdFilterMatch -- check if filename matches a glob pattern
// ============================================================ // ============================================================
// //
// Supports only the most common DOS file filter patterns: "*.*", "*", // Supports only the most common DOS file filter patterns: "*.*", "*",
@ -732,7 +732,7 @@ static bool fdFilterMatch(const char *name, const char *pattern) {
// ============================================================ // ============================================================
// fdFreeEntries free allocated entry name strings // fdFreeEntries -- free allocated entry name strings
// ============================================================ // ============================================================
static void fdFreeEntries(void) { static void fdFreeEntries(void) {
@ -746,7 +746,7 @@ static void fdFreeEntries(void) {
// ============================================================ // ============================================================
// fdEntryCompare sort: dirs first, then alphabetical // fdEntryCompare -- sort: dirs first, then alphabetical
// ============================================================ // ============================================================
// //
// Sort comparator for the indirect sort in fdLoadDir. Uses an index // Sort comparator for the indirect sort in fdLoadDir. Uses an index
@ -769,7 +769,7 @@ static int fdEntryCompare(const void *a, const void *b) {
// ============================================================ // ============================================================
// fdLoadDir read directory contents into state // fdLoadDir -- read directory contents into state
// ============================================================ // ============================================================
// //
// Reads the current directory, applies the active file filter, sorts // Reads the current directory, applies the active file filter, sorts
@ -872,7 +872,7 @@ static void fdLoadDir(void) {
// ============================================================ // ============================================================
// fdNavigate change to a new directory // fdNavigate -- change to a new directory
// ============================================================ // ============================================================
// //
// Handles both absolute and relative paths. Relative paths are resolved // Handles both absolute and relative paths. Relative paths are resolved
@ -918,7 +918,7 @@ static void fdNavigate(const char *path) {
// ============================================================ // ============================================================
// fdValidateFilename check filename is valid for target OS // fdValidateFilename -- check filename is valid for target OS
// ============================================================ // ============================================================
static bool fdValidateFilename(const char *name) { static bool fdValidateFilename(const char *name) {
@ -934,7 +934,7 @@ static bool fdValidateFilename(const char *name) {
// ============================================================ // ============================================================
// fdAcceptFile confirm and accept the selected filename // fdAcceptFile -- confirm and accept the selected filename
// ============================================================ // ============================================================
// //
// Validates the filename (platform-specific rules), then checks for // Validates the filename (platform-specific rules), then checks for
@ -979,7 +979,7 @@ static bool fdAcceptFile(const char *name) {
// ============================================================ // ============================================================
// fdOnListClick file list selection changed // fdOnListClick -- file list selection changed
// ============================================================ // ============================================================
static void fdOnListClick(WidgetT *w) { static void fdOnListClick(WidgetT *w) {
@ -996,7 +996,7 @@ static void fdOnListClick(WidgetT *w) {
// ============================================================ // ============================================================
// fdOnListDblClick file list double-click // fdOnListDblClick -- file list double-click
// ============================================================ // ============================================================
// //
// Double-click on a directory navigates into it. Double-click on a file // Double-click on a directory navigates into it. Double-click on a file
@ -1031,7 +1031,7 @@ static void fdOnListDblClick(WidgetT *w) {
fdNavigate(dirName); fdNavigate(dirName);
wgtSetText(sFd.nameInput, ""); wgtSetText(sFd.nameInput, "");
} else { } else {
// Double-click on file accept it (with confirmation if needed) // Double-click on file -- accept it (with confirmation if needed)
wgtSetText(sFd.nameInput, sFd.entryNames[sel]); wgtSetText(sFd.nameInput, sFd.entryNames[sel]);
fdAcceptFile(sFd.entryNames[sel]); fdAcceptFile(sFd.entryNames[sel]);
} }
@ -1039,7 +1039,7 @@ static void fdOnListDblClick(WidgetT *w) {
// ============================================================ // ============================================================
// fdOnOk OK button clicked // fdOnOk -- OK button clicked
// ============================================================ // ============================================================
// //
// OK has three behaviors depending on context: // OK has three behaviors depending on context:
@ -1055,7 +1055,7 @@ static void fdOnOk(WidgetT *w) {
// If the filename input is empty and a directory is selected in the // If the filename input is empty and a directory is selected in the
// list, navigate into it. But if the user has typed a filename, // list, navigate into it. But if the user has typed a filename,
// always accept it don't let the listbox selection override. // always accept it -- don't let the listbox selection override.
if (!name || name[0] == '\0') { if (!name || name[0] == '\0') {
int32_t sel = wgtListBoxGetSelected(sFd.fileList); int32_t sel = wgtListBoxGetSelected(sFd.fileList);
@ -1105,7 +1105,7 @@ static void fdOnOk(WidgetT *w) {
// ============================================================ // ============================================================
// fdOnCancel Cancel button clicked // fdOnCancel -- Cancel button clicked
// ============================================================ // ============================================================
static void fdOnCancel(WidgetT *w) { static void fdOnCancel(WidgetT *w) {
@ -1116,7 +1116,7 @@ static void fdOnCancel(WidgetT *w) {
// ============================================================ // ============================================================
// fdOnClose window close button // fdOnClose -- window close button
// ============================================================ // ============================================================
static void fdOnClose(WindowT *win) { static void fdOnClose(WindowT *win) {
@ -1127,7 +1127,7 @@ static void fdOnClose(WindowT *win) {
// ============================================================ // ============================================================
// fdOnFilterChange filter dropdown changed // fdOnFilterChange -- filter dropdown changed
// ============================================================ // ============================================================
static void fdOnFilterChange(WidgetT *w) { static void fdOnFilterChange(WidgetT *w) {
@ -1137,7 +1137,7 @@ static void fdOnFilterChange(WidgetT *w) {
// ============================================================ // ============================================================
// fdOnPathSubmit enter pressed in path input // fdOnPathSubmit -- enter pressed in path input
// ============================================================ // ============================================================
static void fdOnPathSubmit(WidgetT *w) { static void fdOnPathSubmit(WidgetT *w) {
@ -1223,7 +1223,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
wgtLabel(filterRow, "F&ilter:"); wgtLabel(filterRow, "F&ilter:");
sFd.filterDd = wgtDropdown(filterRow); sFd.filterDd = wgtDropdown(filterRow);
// Build filter label array (static lives for dialog lifetime) // Build filter label array (static -- lives for dialog lifetime)
static const char *filterLabels[16]; static const char *filterLabels[16];
int32_t fc = filterCount < 16 ? filterCount : 16; int32_t fc = filterCount < 16 ? filterCount : 16;

View file

@ -1,4 +1,4 @@
// dvxDialog.h Modal dialogs for DVX GUI // dvxDialog.h -- Modal dialogs for DVX GUI
// //
// Provides pre-built modal dialog boxes (message box, file dialog) that // Provides pre-built modal dialog boxes (message box, file dialog) that
// block the caller and run their own event loop via dvxUpdate() until the // block the caller and run their own event loop via dvxUpdate() until the

View file

@ -1,4 +1,4 @@
// dvx_draw.c Layer 2: Drawing primitives for DVX GUI (optimized) // dvx_draw.c -- Layer 2: Drawing primitives for DVX GUI (optimized)
// //
// This is the second layer of the DVX compositor stack, sitting on top // This is the second layer of the DVX compositor stack, sitting on top
// of dvxVideo (layer 1) and below dvxComp (layer 3). It provides all // of dvxVideo (layer 1) and below dvxComp (layer 3). It provides all
@ -39,7 +39,7 @@
// 3) For the most critical glyph paths (unclipped 32bpp and 16bpp), // 3) For the most critical glyph paths (unclipped 32bpp and 16bpp),
// the pixel loops are fully unrolled into 8 direct array stores // the pixel loops are fully unrolled into 8 direct array stores
// with literal bit masks. This eliminates the sGlyphBit[] table // with literal bit masks. This eliminates the sGlyphBit[] table
// lookup, the loop counter, and the loop branch saving ~3 cycles // lookup, the loop counter, and the loop branch -- saving ~3 cycles
// per pixel on a 486. The clipped path falls back to the table. // per pixel on a 486. The clipped path falls back to the table.
// //
// Clip rectangle handling: All draw functions clip against // Clip rectangle handling: All draw functions clip against
@ -94,7 +94,7 @@ char accelParse(const char *text) {
text++; text++;
if (*text == '&') { if (*text == '&') {
// Escaped && literal &, not an accelerator // Escaped && -- literal &, not an accelerator
text++; text++;
continue; continue;
} }
@ -136,9 +136,9 @@ char accelParse(const char *text) {
// clip region, w or h will be <= 0 and callers bail out. // clip region, w or h will be <= 0 and callers bail out.
// //
// Marked static inline because this is called on every rectFill, // Marked static inline because this is called on every rectFill,
// rectCopy, and indirectly on every glyph it must compile to // rectCopy, and indirectly on every glyph -- it must compile to
// straight-line clamp instructions with zero call overhead. // straight-line clamp instructions with zero call overhead.
// __builtin_expect(, 0) marks clipping as unlikely; in the // __builtin_expect(..., 0) marks clipping as unlikely; in the
// common case windows are fully within the clip rect and all // common case windows are fully within the clip rect and all
// four branches fall through untaken. On Pentium this keeps the // four branches fall through untaken. On Pentium this keeps the
// branch predictor happy (static not-taken prediction for forward // branch predictor happy (static not-taken prediction for forward
@ -183,7 +183,7 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *
// The implementation has special-cased fast paths for bw==2 and bw==1 // The implementation has special-cased fast paths for bw==2 and bw==1
// that emit exact spans via rectFill rather than looping. This // that emit exact spans via rectFill rather than looping. This
// matters because drawBevel is called for every window frame, button, // matters because drawBevel is called for every window frame, button,
// menu, and scrollbar element on every repaint the loop overhead // menu, and scrollbar element on every repaint -- the loop overhead
// and extra rectFill calls in the general case add up. Each rectFill // and extra rectFill calls in the general case add up. Each rectFill
// call already handles clipping internally, so the bevels clip // call already handles clipping internally, so the bevels clip
// correctly even when a window is partially off-screen. // correctly even when a window is partially off-screen.
@ -248,7 +248,7 @@ void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w
// (bit 7 = leftmost pixel). This is the standard VGA/PC BIOS font // (bit 7 = leftmost pixel). This is the standard VGA/PC BIOS font
// format. We use 8-pixel-wide glyphs exclusively because 8 bits fit // format. We use 8-pixel-wide glyphs exclusively because 8 bits fit
// in one byte per scanline, making the inner loop a single byte load // in one byte per scanline, making the inner loop a single byte load
// plus 8 bit tests no multi-byte glyph row assembly needed. // plus 8 bit tests -- no multi-byte glyph row assembly needed.
// //
// The function has six specialized code paths (3 bpp x 2 modes), // The function has six specialized code paths (3 bpp x 2 modes),
// chosen with if/else chains rather than function pointers. On 486 // chosen with if/else chains rather than function pointers. On 486
@ -319,7 +319,7 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
if (opaque) { if (opaque) {
// Opaque mode: every pixel in the cell gets written (fg or bg). // Opaque mode: every pixel in the cell gets written (fg or bg).
// The unclipped 32bpp and 16bpp paths use branchless ternary // The unclipped 32bpp and 16bpp paths use branchless ternary
// stores the compiler emits cmov or conditional-set sequences // stores -- the compiler emits cmov or conditional-set sequences
// that avoid branch misprediction penalties. Each row is 8 // that avoid branch misprediction penalties. Each row is 8
// direct array stores with no loop, no table lookup. // direct array stores with no loop, no table lookup.
if (unclipped && bpp == 4) { if (unclipped && bpp == 4) {
@ -357,7 +357,7 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
// Clipped path or 8bpp: use spanFill for bg (leveraging // Clipped path or 8bpp: use spanFill for bg (leveraging
// rep stosl), then iterate visible columns with sGlyphBit[] // rep stosl), then iterate visible columns with sGlyphBit[]
// table for fg. 8bpp always takes this path because 8-bit // table for fg. 8bpp always takes this path because 8-bit
// stores can't be branchlessly ternary'd as efficiently // stores can't be branchlessly ternary'd as efficiently --
// the compiler can't cmov into a byte store. // the compiler can't cmov into a byte store.
for (int32_t row = rowStart; row < rowEnd; row++) { for (int32_t row = rowStart; row < rowEnd; row++) {
int32_t py = y + row; int32_t py = y + row;
@ -658,7 +658,7 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
// expressed as a span fill (which writes uniform color). // expressed as a span fill (which writes uniform color).
// //
// The parity calculations on the bottom and right edges ensure the // The parity calculations on the bottom and right edges ensure the
// dot pattern is visually continuous around corners the starting // dot pattern is visually continuous around corners -- the starting
// pixel of each edge is offset so dots don't double up or gap at // pixel of each edge is offset so dots don't double up or gap at
// the corner where two edges meet. // the corner where two edges meet.
// //
@ -723,7 +723,7 @@ void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32
// drawHLine // drawHLine
// ============================================================ // ============================================================
// //
// Thin convenience wrapper a horizontal line is just a 1px-tall rect. // Thin convenience wrapper -- a horizontal line is just a 1px-tall rect.
// Delegates to rectFill which handles clipping and uses spanFill (rep // Delegates to rectFill which handles clipping and uses spanFill (rep
// stosl) for the actual write. // stosl) for the actual write.
@ -745,8 +745,8 @@ void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w
// spanFill and spanCopy are called per-scanline (not per-pixel), // spanFill and spanCopy are called per-scanline (not per-pixel),
// so the indirect call overhead (~5 cycles on Pentium for the // so the indirect call overhead (~5 cycles on Pentium for the
// mispredicted first call, then predicted afterward) is amortized // mispredicted first call, then predicted afterward) is amortized
// over an entire row of pixels. The alternative a switch inside // over an entire row of pixels. The alternative -- a switch inside
// rectFill's inner loop would branch every scanline for no gain. // rectFill's inner loop -- would branch every scanline for no gain.
// //
// The platform implementations (dvxPlatformDos.c) use inline asm: // The platform implementations (dvxPlatformDos.c) use inline asm:
// spanFill8/16/32 -> rep stosl (fills 4 bytes per clock) // spanFill8/16/32 -> rep stosl (fills 4 bytes per clock)
@ -799,7 +799,7 @@ void drawInit(BlitOpsT *ops, const DisplayT *d) {
// The colMask optimization pre-computes which bits in each row fall // The colMask optimization pre-computes which bits in each row fall
// within the visible (clipped) columns. For fully transparent rows // within the visible (clipped) columns. For fully transparent rows
// (all visible bits have andMask=1), the entire row is skipped with // (all visible bits have andMask=1), the entire row is skipped with
// a single bitwise AND + compare no per-pixel iteration needed. // a single bitwise AND + compare -- no per-pixel iteration needed.
void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor) { void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor) {
int32_t bpp = ops->bytesPerPixel; int32_t bpp = ops->bytesPerPixel;
@ -893,7 +893,7 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
// can have a different fg/bg pair, so the bg can't be filled in a // can have a different fg/bg pair, so the bg can't be filled in a
// single bulk pass. Instead each cell is rendered individually, // single bulk pass. Instead each cell is rendered individually,
// always in opaque mode (every pixel gets a write). The bpp branch // always in opaque mode (every pixel gets a write). The bpp branch
// is still hoisted outside the per-pixel loop the outer loop // is still hoisted outside the per-pixel loop -- the outer loop
// selects the bpp path once, then iterates cells within it. // selects the bpp path once, then iterates cells within it.
// //
// blinkVisible controls the blink phase: when false, fg is replaced // blinkVisible controls the blink phase: when false, fg is replaced
@ -1095,7 +1095,7 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in
text++; text++;
if (*text == '&') { if (*text == '&') {
// Escaped && draw literal & // Escaped && -- draw literal &
if (x + cw > d->clipX) { if (x + cw > d->clipX) {
drawChar(d, ops, font, x, y, '&', fg, bg, opaque); drawChar(d, ops, font, x, y, '&', fg, bg, opaque);
} }
@ -1106,7 +1106,7 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in
} }
if (*text) { if (*text) {
// Accelerator character draw it then underline // Accelerator character -- draw it then underline
if (x + cw > d->clipX) { if (x + cw > d->clipX) {
drawChar(d, ops, font, x, y, *text, fg, bg, opaque); drawChar(d, ops, font, x, y, *text, fg, bg, opaque);
drawHLine(d, ops, x, y + font->charHeight - 1, cw, fg); drawHLine(d, ops, x, y + font->charHeight - 1, cw, fg);
@ -1135,7 +1135,7 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in
// ============================================================ // ============================================================
// //
// Draws a vertical line pixel-by-pixel. Unlike drawHLine (which // Draws a vertical line pixel-by-pixel. Unlike drawHLine (which
// delegates to rectFill spanFill for a single-row span), a // delegates to rectFill -> spanFill for a single-row span), a
// vertical line can't use spanFill because each pixel is on a // vertical line can't use spanFill because each pixel is on a
// different scanline. Instead we advance by d->pitch per pixel // different scanline. Instead we advance by d->pitch per pixel
// and write directly, branching on bpp once at the top. // and write directly, branching on bpp once at the top.
@ -1224,7 +1224,7 @@ static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
// This function does NOT handle overlapping source and destination // This function does NOT handle overlapping source and destination
// regions (no memmove). That's fine because the source is always a // regions (no memmove). That's fine because the source is always a
// per-window content buffer and the destination is the shared // per-window content buffer and the destination is the shared
// backbuffer they never overlap. // backbuffer -- they never overlap.
void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, const uint8_t *srcBuf, int32_t srcPitch, int32_t srcX, int32_t srcY, int32_t w, int32_t h) { void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, const uint8_t *srcBuf, int32_t srcPitch, int32_t srcX, int32_t srcY, int32_t w, int32_t h) {
int32_t bpp = ops->bytesPerPixel; int32_t bpp = ops->bytesPerPixel;
@ -1268,7 +1268,7 @@ void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, cons
// The workhorse fill primitive. Clips to the display clip rect, // The workhorse fill primitive. Clips to the display clip rect,
// then fills one scanline at a time via the spanFill function // then fills one scanline at a time via the spanFill function
// pointer (which routes to rep stosl on DOS). This is the most // pointer (which routes to rep stosl on DOS). This is the most
// frequently called function in the draw layer it backs rectFill // frequently called function in the draw layer -- it backs rectFill
// directly, plus drawHLine, drawBevel interior fills, and the bg // directly, plus drawHLine, drawBevel interior fills, and the bg
// fill in opaque text rendering. // fill in opaque text rendering.
// //
@ -1299,7 +1299,7 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w,
// ============================================================ // ============================================================
// //
// Returns the pixel width of a null-terminated string. Because all // Returns the pixel width of a null-terminated string. Because all
// fonts are fixed-width, this is just strlen * charWidth but we // fonts are fixed-width, this is just strlen * charWidth -- but we
// iterate manually rather than calling strlen to avoid a second pass // iterate manually rather than calling strlen to avoid a second pass
// over the string. This is used heavily for layout calculations // over the string. This is used heavily for layout calculations
// (centering text in buttons, sizing menu popups, etc.). // (centering text in buttons, sizing menu popups, etc.).
@ -1334,14 +1334,14 @@ int32_t textWidthAccel(const BitmapFontT *font, const char *text) {
text++; text++;
if (*text == '&') { if (*text == '&') {
// Escaped && counts as one character // Escaped && -- counts as one character
w += font->charWidth; w += font->charWidth;
text++; text++;
continue; continue;
} }
if (*text) { if (*text) {
// Accelerator character counts as one character, & is skipped // Accelerator character -- counts as one character, & is skipped
w += font->charWidth; w += font->charWidth;
text++; text++;
continue; continue;

View file

@ -1,9 +1,9 @@
// dvx_draw.h Layer 2: Drawing primitives for DVX GUI // dvx_draw.h -- Layer 2: Drawing primitives for DVX GUI
// //
// Provides all 2D drawing operations: rectangle fills, bitmap blits, text // Provides all 2D drawing operations: rectangle fills, bitmap blits, text
// rendering, bevels, lines, and cursor/icon rendering. Every function in // rendering, bevels, lines, and cursor/icon rendering. Every function in
// this layer draws into the display's backbuffer and clips to the display's // this layer draws into the display's backbuffer and clips to the display's
// current clip rectangle no direct LFB writes happen here. // current clip rectangle -- no direct LFB writes happen here.
// //
// This layer is deliberately stateless beyond the clip rect on DisplayT. // This layer is deliberately stateless beyond the clip rect on DisplayT.
// All drawing context (colors, fonts, bevel styles) is passed explicitly // All drawing context (colors, fonts, bevel styles) is passed explicitly
@ -34,7 +34,7 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w,
void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, const uint8_t *srcBuf, int32_t srcPitch, int32_t srcX, int32_t srcY, int32_t w, int32_t h); void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, const uint8_t *srcBuf, int32_t srcPitch, int32_t srcX, int32_t srcY, int32_t w, int32_t h);
// Draw a beveled frame. The bevel is drawn as overlapping horizontal and // Draw a beveled frame. The bevel is drawn as overlapping horizontal and
// vertical spans top/left in highlight color, bottom/right in shadow. // vertical spans -- top/left in highlight color, bottom/right in shadow.
// The face color fills the interior if non-zero. // The face color fills the interior if non-zero.
void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style); void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style);

View file

@ -1,4 +1,4 @@
// dvx_font.h Embedded VGA bitmap font data (CP437) for DVX GUI // dvx_font.h -- Embedded VGA bitmap font data (CP437) for DVX GUI
// //
// Contains the raw glyph bitmaps for two standard VGA ROM fonts (8x14 and // Contains the raw glyph bitmaps for two standard VGA ROM fonts (8x14 and
// 8x16) covering the full IBM Code Page 437 character set (256 glyphs). // 8x16) covering the full IBM Code Page 437 character set (256 glyphs).
@ -15,7 +15,7 @@
// The glyph format is 1 bit per pixel, 8 pixels wide, with MSB = leftmost // The glyph format is 1 bit per pixel, 8 pixels wide, with MSB = leftmost
// pixel. Each glyph occupies charHeight consecutive bytes (14 or 16). // pixel. Each glyph occupies charHeight consecutive bytes (14 or 16).
// The drawing code in dvxDraw.c processes one byte per scanline, testing // The drawing code in dvxDraw.c processes one byte per scanline, testing
// each bit to decide foreground vs. background the 8-pixel width means // each bit to decide foreground vs. background -- the 8-pixel width means
// no bit-shifting across byte boundaries is ever needed. // no bit-shifting across byte boundaries is ever needed.
// //
// CP437 includes the full ASCII printable range (32-126) plus box-drawing // CP437 includes the full ASCII printable range (32-126) plus box-drawing
@ -29,7 +29,7 @@
#include "dvxTypes.h" #include "dvxTypes.h"
// ============================================================ // ============================================================
// 8x14 VGA ROM font standard IBM CP437 character set // 8x14 VGA ROM font -- standard IBM CP437 character set
// 256 glyphs, 14 bytes per glyph, MSB = leftmost pixel // 256 glyphs, 14 bytes per glyph, MSB = leftmost pixel
// ============================================================ // ============================================================
@ -551,7 +551,7 @@ static const uint8_t font8x14[256 * 14] = {
}; };
// ============================================================ // ============================================================
// 8x16 VGA ROM font standard IBM CP437 character set // 8x16 VGA ROM font -- standard IBM CP437 character set
// 256 glyphs, 16 bytes per glyph, MSB = leftmost pixel // 256 glyphs, 16 bytes per glyph, MSB = leftmost pixel
// ============================================================ // ============================================================

View file

@ -1,4 +1,4 @@
// dvxIcon.c stb_image implementation for DVX GUI // dvxIcon.c -- stb_image implementation for DVX GUI
// //
// This file exists solely to instantiate the stb_image implementation. // This file exists solely to instantiate the stb_image implementation.
// stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION // stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION
@ -9,7 +9,7 @@
// 3. Doesn't slow down incremental rebuilds (only recompiles if stb changes) // 3. Doesn't slow down incremental rebuilds (only recompiles if stb changes)
// //
// stb_image was chosen over libpng/libjpeg because: // stb_image was chosen over libpng/libjpeg because:
// - Single header, no external dependencies critical for DJGPP cross-compile // - Single header, no external dependencies -- critical for DJGPP cross-compile
// - Supports BMP, PNG, JPEG, GIF with one include // - Supports BMP, PNG, JPEG, GIF with one include
// - Public domain license, no linking restrictions // - Public domain license, no linking restrictions
// - Small code footprint suitable for DOS targets // - Small code footprint suitable for DOS targets

View file

@ -1,8 +1,8 @@
// dvxImageWrite.c stb_image_write implementation for DVX GUI // dvxImageWrite.c -- stb_image_write implementation for DVX GUI
// //
// Companion to dvxIcon.c: instantiates stb_image_write for PNG output // Companion to dvxIcon.c: instantiates stb_image_write for PNG output
// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as // (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as
// dvxIcon.c for using stb zero external dependencies, single header, // dvxIcon.c for using stb -- zero external dependencies, single header,
// public domain. Kept in a separate translation unit from the read side // public domain. Kept in a separate translation unit from the read side
// so projects that don't need screenshot support can omit this file and // so projects that don't need screenshot support can omit this file and
// save the code size. // save the code size.

View file

@ -1,10 +1,10 @@
// dvx_palette.h 8-bit mode palette definition for DVX GUI // dvx_palette.h -- 8-bit mode palette definition for DVX GUI
// //
// Defines the 256-color palette used in 8-bit (VGA Mode 13h / VESA 8bpp) // Defines the 256-color palette used in 8-bit (VGA Mode 13h / VESA 8bpp)
// video modes. The palette layout follows the same strategy as the X11 // video modes. The palette layout follows the same strategy as the X11
// default colormap and Netscape's web-safe colors: // default colormap and Netscape's web-safe colors:
// //
// 0-215: 6x6x6 color cube 6 levels per channel (0, 51, 102, 153, // 0-215: 6x6x6 color cube -- 6 levels per channel (0, 51, 102, 153,
// 204, 255), giving 216 uniformly distributed colors. The index // 204, 255), giving 216 uniformly distributed colors. The index
// formula (r*36 + g*6 + b) enables O(1) color lookup without // formula (r*36 + g*6 + b) enables O(1) color lookup without
// searching. // searching.

View file

@ -1,4 +1,4 @@
// dvxPrefs.c INI-based preferences system (read/write) // dvxPrefs.c -- INI-based preferences system (read/write)
// //
// Custom INI parser and writer. Stores entries as a dynamic array of // Custom INI parser and writer. Stores entries as a dynamic array of
// section/key/value triples using stb_ds. Preserves insertion order // section/key/value triples using stb_ds. Preserves insertion order
@ -210,14 +210,16 @@ const char *prefsGetString(const char *section, const char *key, const char *def
bool prefsLoad(const char *filename) { bool prefsLoad(const char *filename) {
prefsFree(); prefsFree();
// Always store the path so prefsSave can create the file
// even if it doesn't exist yet.
sFilePath = dupStr(filename);
FILE *fp = fopen(filename, "rb"); FILE *fp = fopen(filename, "rb");
if (!fp) { if (!fp) {
return false; return false;
} }
sFilePath = dupStr(filename);
char line[512]; char line[512];
char *currentSection = dupStr(""); char *currentSection = dupStr("");
@ -236,7 +238,7 @@ bool prefsLoad(const char *filename) {
p++; p++;
} }
// Blank line store as comment to preserve formatting // Blank line -- store as comment to preserve formatting
if (*p == '\0') { if (*p == '\0') {
PrefsEntryT e = {0}; PrefsEntryT e = {0};
e.section = dupStr(currentSection); e.section = dupStr(currentSection);

View file

@ -1,4 +1,4 @@
// dvxPrefs.h INI-based preferences system (read/write) // dvxPrefs.h -- INI-based preferences system (read/write)
// //
// Loads a configuration file at startup and provides typed accessors // Loads a configuration file at startup and provides typed accessors
// with caller-supplied defaults. Values can be modified at runtime // with caller-supplied defaults. Values can be modified at runtime

View file

@ -1,4 +1,4 @@
// dvx_types.h Shared type definitions for DVX GUI // dvx_types.h -- Shared type definitions for DVX GUI
// //
// Central type definitions shared across all five layers of the DVX GUI // Central type definitions shared across all five layers of the DVX GUI
// stack (video, draw, comp, wm, app). Every header includes this file, // stack (video, draw, comp, wm, app). Every header includes this file,
@ -18,7 +18,7 @@
// Describes the pixel encoding for the active VESA video mode. // Describes the pixel encoding for the active VESA video mode.
// Populated once at startup from the VBE mode info block and then // Populated once at startup from the VBE mode info block and then
// treated as read-only. Storing shift/mask/bits separately avoids // treated as read-only. Storing shift/mask/bits separately avoids
// repeated bit-scanning when packing colors packColor() uses these // repeated bit-scanning when packing colors -- packColor() uses these
// fields directly for shift-and-mask arithmetic rather than computing // fields directly for shift-and-mask arithmetic rather than computing
// them on the fly, which matters when called per-glyph during text // them on the fly, which matters when called per-glyph during text
// rendering on a 486. // rendering on a 486.
@ -49,13 +49,13 @@ typedef struct {
// The double-buffer strategy (backBuf in system RAM, lfb is the real // The double-buffer strategy (backBuf in system RAM, lfb is the real
// framebuffer) exists because writes to video memory over the PCI bus // framebuffer) exists because writes to video memory over the PCI bus
// are dramatically slower than writes to main RAM on 486/Pentium hardware // are dramatically slower than writes to main RAM on 486/Pentium hardware
// often 10-50x slower for random-access patterns. All drawing goes to // -- often 10-50x slower for random-access patterns. All drawing goes to
// backBuf, and only dirty rectangles are flushed to lfb via fast aligned // backBuf, and only dirty rectangles are flushed to lfb via fast aligned
// copies (rep movsd). This is the single most important performance // copies (rep movsd). This is the single most important performance
// decision in the entire compositor. // decision in the entire compositor.
// //
// The clip rectangle is mutable state set before each draw call so that // The clip rectangle is mutable state set before each draw call so that
// the drawing layer doesn't need per-window context it just clips to // the drawing layer doesn't need per-window context -- it just clips to
// whatever rectangle is currently set on the display. This avoids an // whatever rectangle is currently set on the display. This avoids an
// extra parameter on every draw function and mirrors how classic windowing // extra parameter on every draw function and mirrors how classic windowing
// systems (X11, GDI) handle clipping. // systems (X11, GDI) handle clipping.
@ -123,7 +123,7 @@ typedef struct {
// //
// Bevels are the defining visual element of the Motif/DESQview/X aesthetic. // Bevels are the defining visual element of the Motif/DESQview/X aesthetic.
// Swapping highlight and shadow colors flips between raised and sunken // Swapping highlight and shadow colors flips between raised and sunken
// appearance the BEVEL_RAISED and BEVEL_SUNKEN macros encode this // appearance -- the BEVEL_RAISED and BEVEL_SUNKEN macros encode this
// convention so callers don't get the colors backwards. // convention so callers don't get the colors backwards.
// //
// A width of 2 pixels is the standard Motif bevel size. Using 1 creates // A width of 2 pixels is the standard Motif bevel size. Using 1 creates
@ -142,6 +142,7 @@ typedef struct {
#define BEVEL_RAISED(cs, bw) (BevelStyleT){ (cs)->windowHighlight, (cs)->windowShadow, (cs)->windowFace, (bw) } #define BEVEL_RAISED(cs, bw) (BevelStyleT){ (cs)->windowHighlight, (cs)->windowShadow, (cs)->windowFace, (bw) }
#define BEVEL_SUNKEN(cs, face, bw) (BevelStyleT){ (cs)->windowShadow, (cs)->windowHighlight, (face), (bw) } #define BEVEL_SUNKEN(cs, face, bw) (BevelStyleT){ (cs)->windowShadow, (cs)->windowHighlight, (face), (bw) }
#define BEVEL_TROUGH(cs) (BevelStyleT){ (cs)->windowShadow, (cs)->windowHighlight, (cs)->scrollbarTrough, 1 } #define BEVEL_TROUGH(cs) (BevelStyleT){ (cs)->windowShadow, (cs)->windowHighlight, (cs)->scrollbarTrough, 1 }
#define BEVEL_SB_BUTTON(cs) (BevelStyleT){ (cs)->windowHighlight, (cs)->windowShadow, (cs)->scrollbarBg, 1 }
// ============================================================ // ============================================================
// Bitmap font // Bitmap font
@ -154,7 +155,7 @@ typedef struct {
// //
// Two font sizes are provided (8x14 and 8x16), matching the standard // Two font sizes are provided (8x14 and 8x16), matching the standard
// VGA ROM fonts and CP437 encoding. Proportional fonts would require // VGA ROM fonts and CP437 encoding. Proportional fonts would require
// glyph-width tables, kerning, and per-character positioning none of // glyph-width tables, kerning, and per-character positioning -- none of
// which is worth the complexity for a DOS-era window manager targeting // which is worth the complexity for a DOS-era window manager targeting
// 640x480 screens. The 8-pixel width also aligns nicely with byte // 640x480 screens. The 8-pixel width also aligns nicely with byte
// boundaries, enabling per-scanline glyph rendering without bit shifting. // boundaries, enabling per-scanline glyph rendering without bit shifting.
@ -182,7 +183,7 @@ typedef struct {
// The color set mirrors classic Motif/Windows 3.x conventions: // The color set mirrors classic Motif/Windows 3.x conventions:
// windowHighlight/windowShadow form bevel pairs, activeTitleBg gives // windowHighlight/windowShadow form bevel pairs, activeTitleBg gives
// the focused window's title bar its distinctive color, and so on. // the focused window's title bar its distinctive color, and so on.
// Keeping all colors in one struct makes theme support trivial just // Keeping all colors in one struct makes theme support trivial -- just
// swap the struct. // swap the struct.
typedef struct { typedef struct {
@ -204,6 +205,8 @@ typedef struct {
uint32_t scrollbarBg; uint32_t scrollbarBg;
uint32_t scrollbarFg; uint32_t scrollbarFg;
uint32_t scrollbarTrough; uint32_t scrollbarTrough;
uint32_t cursorFg;
uint32_t cursorBg;
} ColorSchemeT; } ColorSchemeT;
// Color IDs for addressing individual colors in ColorSchemeT. // Color IDs for addressing individual colors in ColorSchemeT.
@ -227,9 +230,17 @@ typedef enum {
ColorScrollbarBgE, ColorScrollbarBgE,
ColorScrollbarFgE, ColorScrollbarFgE,
ColorScrollbarTroughE, ColorScrollbarTroughE,
ColorCursorFgE,
ColorCursorBgE,
ColorCountE ColorCountE
} ColorIdE; } ColorIdE;
typedef enum {
WallpaperStretchE,
WallpaperTileE,
WallpaperCenterE
} WallpaperModeE;
// Video mode entry (enumerated at init, available to apps) // Video mode entry (enumerated at init, available to apps)
typedef struct { typedef struct {
int32_t w; int32_t w;
@ -261,7 +272,7 @@ typedef struct {
// //
// These define the pixel geometry of the window frame (border, title bar, // These define the pixel geometry of the window frame (border, title bar,
// menu bar). They're compile-time constants because the DV/X look demands // menu bar). They're compile-time constants because the DV/X look demands
// a fixed, uniform chrome thickness across all windows there's no // a fixed, uniform chrome thickness across all windows -- there's no
// per-window customization of frame geometry. // per-window customization of frame geometry.
// //
// CHROME_TOTAL_TOP/SIDE/BOTTOM are the total chrome insets from the // CHROME_TOTAL_TOP/SIDE/BOTTOM are the total chrome insets from the
@ -430,7 +441,7 @@ typedef struct {
// //
// WindowT is the central object of the window manager. Each window owns // WindowT is the central object of the window manager. Each window owns
// its own content backbuffer, which persists across frames so that // its own content backbuffer, which persists across frames so that
// applications don't need to repaint on every expose event only when // applications don't need to repaint on every expose event -- only when
// their actual content changes. This is crucial for performance on slow // their actual content changes. This is crucial for performance on slow
// hardware: dragging a window across others doesn't force every obscured // hardware: dragging a window across others doesn't force every obscured
// window to repaint. // window to repaint.
@ -533,7 +544,7 @@ typedef struct WindowT {
// Accelerator table (NULL if none, caller owns allocation) // Accelerator table (NULL if none, caller owns allocation)
AccelTableT *accelTable; AccelTableT *accelTable;
// Callbacks the application's interface to the window manager. // Callbacks -- the application's interface to the window manager.
// Using function pointers rather than a message queue keeps latency // Using function pointers rather than a message queue keeps latency
// low (no queuing/dispatching overhead) and matches the callback-driven // low (no queuing/dispatching overhead) and matches the callback-driven
// model that DOS programs expect. The widget system installs its own // model that DOS programs expect. The widget system installs its own
@ -562,7 +573,7 @@ typedef struct WindowT {
// which matters because WindowT is large. // which matters because WindowT is large.
// //
// Drag/resize/scroll state lives here rather than on individual windows // Drag/resize/scroll state lives here rather than on individual windows
// because only one interaction can be active at a time system-wide // because only one interaction can be active at a time system-wide --
// you can't drag two windows simultaneously with a single mouse. // you can't drag two windows simultaneously with a single mouse.
typedef struct { typedef struct {
@ -596,7 +607,7 @@ typedef struct {
// four pixel states: transparent (AND=1, XOR=0), inverted (AND=1, XOR=1), // four pixel states: transparent (AND=1, XOR=0), inverted (AND=1, XOR=1),
// black (AND=0, XOR=0), and white (AND=0, XOR=1). The cursor is drawn // black (AND=0, XOR=0), and white (AND=0, XOR=1). The cursor is drawn
// in software on top of the composited frame because VESA VBE doesn't // in software on top of the composited frame because VESA VBE doesn't
// provide a hardware sprite the cursor is painted into the backbuffer // provide a hardware sprite -- the cursor is painted into the backbuffer
// and the affected region is flushed to the LFB each frame. // and the affected region is flushed to the LFB each frame.
// //
// 16 pixels wide using uint16_t rows, one word per scanline. This keeps // 16 pixels wide using uint16_t rows, one word per scanline. This keeps

View file

@ -1,4 +1,4 @@
// dvx_video.c Layer 1: Video backend for DVX GUI // dvx_video.c -- Layer 1: Video backend for DVX GUI
// //
// Platform-independent video utilities. The actual VESA/VBE code // Platform-independent video utilities. The actual VESA/VBE code
// now lives in dvxPlatformDos.c (or the platform file for whatever // now lives in dvxPlatformDos.c (or the platform file for whatever
@ -18,7 +18,7 @@
// of dirty rects per frame. Requiring LFB eliminates per-scanline // of dirty rects per frame. Requiring LFB eliminates per-scanline
// bank math, allows rep movsd bulk copies, and simplifies every span // bank math, allows rep movsd bulk copies, and simplifies every span
// operation in the draw layer. The tradeoff is dropping cards that // operation in the draw layer. The tradeoff is dropping cards that
// only expose banked modes acceptable since the target is 486+ with // only expose banked modes -- acceptable since the target is 486+ with
// a VESA 2.0 BIOS (virtually universal by 1994-95). // a VESA 2.0 BIOS (virtually universal by 1994-95).
// //
// System-RAM backbuffer with dirty-rect flushing: Drawing directly // System-RAM backbuffer with dirty-rect flushing: Drawing directly
@ -63,7 +63,7 @@
// For direct-color modes (15/16/32-bit), we right-shift each channel // For direct-color modes (15/16/32-bit), we right-shift each channel
// to discard the low bits that don't fit in the mode's color field, // to discard the low bits that don't fit in the mode's color field,
// then left-shift into position. This approach truncates rather than // then left-shift into position. This approach truncates rather than
// rounds, which is a deliberate simplicity tradeoff dithering or // rounds, which is a deliberate simplicity tradeoff -- dithering or
// rounding would add per-pixel branches on a 486 for minimal visual // rounding would add per-pixel branches on a 486 for minimal visual
// benefit in a desktop GUI. // benefit in a desktop GUI.
@ -136,8 +136,8 @@ void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) {
// ============================================================ // ============================================================
// //
// Thin wrapper that delegates to the platform layer. All the heavy // Thin wrapper that delegates to the platform layer. All the heavy
// lifting VBE enumeration, mode scoring, LFB DPMI mapping, // lifting -- VBE enumeration, mode scoring, LFB DPMI mapping,
// backbuffer allocation lives in dvxPlatformDos.c so this file // backbuffer allocation -- lives in dvxPlatformDos.c so this file
// stays portable. The platform layer fills in every field of // stays portable. The platform layer fills in every field of
// DisplayT (dimensions, pitch, pixel format, lfb pointer, // DisplayT (dimensions, pitch, pixel format, lfb pointer,
// backBuf pointer, palette, initial clip rect). // backBuf pointer, palette, initial clip rect).

View file

@ -1,4 +1,4 @@
// dvx_video.h Layer 1: VESA VBE video backend for DVX GUI // dvx_video.h -- Layer 1: VESA VBE video backend for DVX GUI
// //
// The lowest layer in the DVX stack. Responsible for VESA VBE mode // The lowest layer in the DVX stack. Responsible for VESA VBE mode
// negotiation, linear framebuffer (LFB) mapping via DPMI, system RAM // negotiation, linear framebuffer (LFB) mapping via DPMI, system RAM
@ -22,7 +22,7 @@
// Probes VBE for a mode matching the requested resolution and depth, // Probes VBE for a mode matching the requested resolution and depth,
// enables it, maps the LFB into the DPMI linear address space, and // enables it, maps the LFB into the DPMI linear address space, and
// allocates a system RAM backbuffer of the same size. preferredBpp is // allocates a system RAM backbuffer of the same size. preferredBpp is
// a hint if the exact depth isn't available, the closest match is used. // a hint -- if the exact depth isn't available, the closest match is used.
int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
// Restores VGA text mode (INT 10h AH=0, mode 3), unmaps the LFB, and // Restores VGA text mode (INT 10h AH=0, mode 3), unmaps the LFB, and

View file

@ -1,4 +1,4 @@
// dvxWidget.h Widget system for DVX GUI // dvxWidget.h -- Widget system for DVX GUI
// //
// A retained-mode widget toolkit layered on top of the DVX window manager. // A retained-mode widget toolkit layered on top of the DVX window manager.
// Widgets form a tree (parent-child via firstChild/lastChild/nextSibling // Widgets form a tree (parent-child via firstChild/lastChild/nextSibling
@ -150,7 +150,7 @@ typedef enum {
// ============================================================ // ============================================================
typedef enum { typedef enum {
FrameInE, // beveled inward (sunken) default FrameInE, // beveled inward (sunken) -- default
FrameOutE, // beveled outward (raised) FrameOutE, // beveled outward (raised)
FrameFlatE // solid color line FrameFlatE // solid color line
} FrameStyleE; } FrameStyleE;
@ -176,7 +176,7 @@ typedef enum {
// state. The pointer indirection adds one dereference but saves // state. The pointer indirection adds one dereference but saves
// significant memory across a typical widget tree. // significant memory across a typical widget tree.
// AnsiTermDataT full VT100/ANSI terminal emulator state. // AnsiTermDataT -- full VT100/ANSI terminal emulator state.
// Implements a subset of DEC VT100 escape sequences sufficient for BBS // Implements a subset of DEC VT100 escape sequences sufficient for BBS
// and DOS ANSI art rendering: cursor movement, color attributes (16-color // and DOS ANSI art rendering: cursor movement, color attributes (16-color
// with bold-as-bright), scrolling regions, and blink. The parser is a // with bold-as-bright), scrolling regions, and blink. The parser is a
@ -201,7 +201,7 @@ typedef struct {
// Scrolling region (0-based, inclusive) // Scrolling region (0-based, inclusive)
int32_t scrollTop; // top row of scroll region int32_t scrollTop; // top row of scroll region
int32_t scrollBot; // bottom row of scroll region int32_t scrollBot; // bottom row of scroll region
// Scrollback circular buffer so old lines age out naturally without // Scrollback -- circular buffer so old lines age out naturally without
// memmove. Each line is cols*2 bytes (same ch/attr format as cells). // memmove. Each line is cols*2 bytes (same ch/attr format as cells).
// scrollPos tracks the view offset: when equal to scrollbackCount, // scrollPos tracks the view offset: when equal to scrollbackCount,
// the user sees the live screen; when less, they're viewing history. // the user sees the live screen; when less, they're viewing history.
@ -216,7 +216,7 @@ typedef struct {
// Cursor blink // Cursor blink
bool cursorOn; // current cursor blink phase bool cursorOn; // current cursor blink phase
clock_t cursorTime; // timestamp of last cursor toggle clock_t cursorTime; // timestamp of last cursor toggle
// Dirty tracking a 32-bit bitmask where each bit corresponds to one // Dirty tracking -- a 32-bit bitmask where each bit corresponds to one
// terminal row. Only dirty rows are repainted, which is critical because // terminal row. Only dirty rows are repainted, which is critical because
// the ANSI terminal can receive data every frame (at 9600+ baud) and // the ANSI terminal can receive data every frame (at 9600+ baud) and
// re-rendering all 25 rows of 80 columns each frame would dominate the // re-rendering all 25 rows of 80 columns each frame would dominate the
@ -235,7 +235,7 @@ typedef struct {
int32_t selEndLine; int32_t selEndLine;
int32_t selEndCol; int32_t selEndCol;
bool selecting; bool selecting;
// Communications interface abstracted so the terminal can connect to // Communications interface -- abstracted so the terminal can connect to
// different backends (serial port, secLink channel, local pipe) without // different backends (serial port, secLink channel, local pipe) without
// knowing the transport details. When all are NULL, the terminal is in // knowing the transport details. When all are NULL, the terminal is in
// offline/disconnected mode (useful for viewing .ANS files). // offline/disconnected mode (useful for viewing .ANS files).
@ -244,7 +244,7 @@ typedef struct {
int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len);
} AnsiTermDataT; } AnsiTermDataT;
// ListViewDataT multi-column list with sortable headers and optional // ListViewDataT -- multi-column list with sortable headers and optional
// multi-select. cellData is a flat array of strings indexed as // multi-select. cellData is a flat array of strings indexed as
// cellData[row * colCount + col]. The sortIndex is an indirection array // cellData[row * colCount + col]. The sortIndex is an indirection array
// that maps displayed row numbers to data row numbers, allowing sort // that maps displayed row numbers to data row numbers, allowing sort
@ -282,7 +282,7 @@ typedef struct WidgetT {
WidgetTypeE type; WidgetTypeE type;
// wclass points to the vtable for this widget type. Looked up once at // wclass points to the vtable for this widget type. Looked up once at
// creation from widgetClassTable[type]. This avoids a switch on type // creation from widgetClassTable[type]. This avoids a switch on type
// in every paint/layout/event dispatch the cost is one pointer per // in every paint/layout/event dispatch -- the cost is one pointer per
// widget, which is negligible. // widget, which is negligible.
const struct WidgetClassT *wclass; const struct WidgetClassT *wclass;
char name[MAX_WIDGET_NAME]; char name[MAX_WIDGET_NAME];
@ -304,7 +304,7 @@ typedef struct WidgetT {
int32_t w; int32_t w;
int32_t h; int32_t h;
// Computed minimum size set bottom-up by calcMinSize during layout. // Computed minimum size -- set bottom-up by calcMinSize during layout.
// These represent the smallest possible size for this widget (including // These represent the smallest possible size for this widget (including
// its children if it's a container). The layout engine uses these as // its children if it's a container). The layout engine uses these as
// the starting point for space allocation. // the starting point for space allocation.
@ -356,7 +356,7 @@ typedef struct WidgetT {
void (*onFocus)(struct WidgetT *w); void (*onFocus)(struct WidgetT *w);
void (*onBlur)(struct WidgetT *w); void (*onBlur)(struct WidgetT *w);
// Type-specific data tagged union keyed by the `type` field. // Type-specific data -- tagged union keyed by the `type` field.
// Only the member corresponding to `type` is valid. This is the C // Only the member corresponding to `type` is valid. This is the C
// equivalent of a discriminated union / variant type. Using a union // equivalent of a discriminated union / variant type. Using a union
// instead of separate structs per widget type keeps all widget data // instead of separate structs per widget type keeps all widget data
@ -365,6 +365,7 @@ typedef struct WidgetT {
union { union {
struct { struct {
const char *text; const char *text;
WidgetAlignE textAlign; // AlignStartE=left, AlignCenterE, AlignEndE=right
} label; } label;
struct { struct {
@ -389,7 +390,7 @@ typedef struct WidgetT {
// Text input has its own edit buffer (not a pointer to external // Text input has its own edit buffer (not a pointer to external
// storage) so the widget fully owns its text lifecycle. The undo // storage) so the widget fully owns its text lifecycle. The undo
// buffer holds a single-level snapshot taken before each edit // buffer holds a single-level snapshot taken before each edit
// operation Ctrl+Z restores to the snapshot. This is simpler // operation -- Ctrl+Z restores to the snapshot. This is simpler
// than a full undo stack but sufficient for single-line fields. // than a full undo stack but sufficient for single-line fields.
struct { struct {
char *buf; char *buf;
@ -605,7 +606,7 @@ typedef struct WidgetT {
// that fills the window's content area, and installs callback handlers // that fills the window's content area, and installs callback handlers
// (onPaint, onMouse, onKey, onResize) that dispatch events to the widget // (onPaint, onMouse, onKey, onResize) that dispatch events to the widget
// tree. After this call, the window is fully managed by the widget system // tree. After this call, the window is fully managed by the widget system
// the application builds its UI by adding child widgets to the returned // -- the application builds its UI by adding child widgets to the returned
// root container. The window's userData is set to the AppContextT pointer. // root container. The window's userData is set to the AppContextT pointer.
WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win); WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win);
@ -628,6 +629,7 @@ WidgetT *wgtFrame(WidgetT *parent, const char *title);
// ============================================================ // ============================================================
WidgetT *wgtLabel(WidgetT *parent, const char *text); WidgetT *wgtLabel(WidgetT *parent, const char *text);
void wgtLabelSetAlign(WidgetT *w, WidgetAlignE align);
WidgetT *wgtButton(WidgetT *parent, const char *text); WidgetT *wgtButton(WidgetT *parent, const char *text);
WidgetT *wgtCheckbox(WidgetT *parent, const char *text); WidgetT *wgtCheckbox(WidgetT *parent, const char *text);
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen);
@ -651,7 +653,7 @@ WidgetT *wgtRadio(WidgetT *parent, const char *text);
// ============================================================ // ============================================================
// //
// Spacer is an invisible flexible widget (weight=100) that absorbs // Spacer is an invisible flexible widget (weight=100) that absorbs
// extra space useful for pushing subsequent siblings to the end of // extra space -- useful for pushing subsequent siblings to the end of
// a container (like CSS flex: 1 auto). Separators are thin beveled // a container (like CSS flex: 1 auto). Separators are thin beveled
// lines for visual grouping. // lines for visual grouping.
@ -895,7 +897,7 @@ int32_t wgtAnsiTermPoll(WidgetT *w);
// full widget paint pipeline (which would repaint the entire widget), this // full widget paint pipeline (which would repaint the entire widget), this
// renders only the dirty rows (tracked via the dirtyRows bitmask) directly // renders only the dirty rows (tracked via the dirtyRows bitmask) directly
// into the window's content buffer. This is essential for responsive terminal // into the window's content buffer. This is essential for responsive terminal
// output incoming serial data can dirty a few rows per frame, and // output -- incoming serial data can dirty a few rows per frame, and
// repainting only those rows keeps the cost proportional to the actual // repainting only those rows keeps the cost proportional to the actual
// change rather than the full 80x25 grid. Returns the number of rows // change rather than the full 80x25 grid. Returns the number of rows
// repainted; outY/outH report the affected region for dirty-rect tracking. // repainted; outY/outH report the affected region for dirty-rect tracking.
@ -972,7 +974,7 @@ void wgtListBoxSetReorderable(WidgetT *w, bool reorderable);
// ============================================================ // ============================================================
// Set tooltip text for a widget (NULL to remove). // Set tooltip text for a widget (NULL to remove).
// Caller owns the string it must outlive the widget. // Caller owns the string -- it must outlive the widget.
void wgtSetTooltip(WidgetT *w, const char *text); void wgtSetTooltip(WidgetT *w, const char *text);
// ============================================================ // ============================================================

View file

@ -1,4 +1,4 @@
// dvx_wm.c Layer 4: Window manager for DVX GUI // dvx_wm.c -- Layer 4: Window manager for DVX GUI
// //
// This layer manages the window stack (z-order), window chrome rendering // This layer manages the window stack (z-order), window chrome rendering
// (title bars, borders, bevels, gadgets, menu bars, scrollbars), and user // (title bars, borders, bevels, gadgets, menu bars, scrollbars), and user
@ -21,7 +21,7 @@
// //
// - Each window has its own content backbuffer (contentBuf). The WM blits // - Each window has its own content backbuffer (contentBuf). The WM blits
// this to the display backbuffer during compositing. This means window // this to the display backbuffer during compositing. This means window
// content survives being occluded apps don't get expose events and don't // content survives being occluded -- apps don't get expose events and don't
// need to repaint when uncovered, which hugely simplifies app code and // need to repaint when uncovered, which hugely simplifies app code and
// avoids the latency of synchronous repaint-on-expose. // avoids the latency of synchronous repaint-on-expose.
// //
@ -66,7 +66,7 @@
// Extracted into a struct so that both drawTitleBar() and wmHitTest() compute // Extracted into a struct so that both drawTitleBar() and wmHitTest() compute
// identical geometry from the same code path (computeTitleGeom). Without this, // identical geometry from the same code path (computeTitleGeom). Without this,
// a discrepancy between draw and hit-test coordinates would cause clicks to // a discrepancy between draw and hit-test coordinates would cause clicks to
// land on the wrong gadget a subtle bug that's hard to reproduce visually. // land on the wrong gadget -- a subtle bug that's hard to reproduce visually.
typedef struct { typedef struct {
int32_t titleX; int32_t titleX;
@ -110,7 +110,7 @@ static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH);
// padding (CHROME_TITLE_PAD) on both sides and MENU_BAR_GAP between labels. // padding (CHROME_TITLE_PAD) on both sides and MENU_BAR_GAP between labels.
// Positions are cached and only recomputed when positionsDirty is set (after // Positions are cached and only recomputed when positionsDirty is set (after
// adding/removing a menu), avoiding redundant textWidthAccel calls on every // adding/removing a menu), avoiding redundant textWidthAccel calls on every
// paint font metric calculation is expensive relative to a simple flag check. // paint -- font metric calculation is expensive relative to a simple flag check.
// barX values are relative to the window, not the screen; the draw path adds // barX values are relative to the window, not the screen; the draw path adds
// the window's screen position. // the window's screen position.
static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) {
@ -198,7 +198,7 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) {
// Row 0: highlight (bright edge catches the "light" from top-left) // Row 0: highlight (bright edge catches the "light" from top-left)
// Row 1: highlight (doubled for visual weight at low resolutions) // Row 1: highlight (doubled for visual weight at low resolutions)
// Row 2: face (flat middle band) // Row 2: face (flat middle band)
// Row 3: shadow (inner crease makes the border feel like two beveled // Row 3: shadow (inner crease -- makes the border feel like two beveled
// ridges rather than one flat edge) // ridges rather than one flat edge)
// //
// Bottom/right are mirror-reversed (shadow, shadow, face, highlight). This // Bottom/right are mirror-reversed (shadow, shadow, face, highlight). This
@ -253,7 +253,7 @@ static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
// Draws a small raised beveled square (1px bevel) used as the base for // Draws a small raised beveled square (1px bevel) used as the base for
// close, minimize, and maximize buttons. The icon (bar, box, dot) is drawn // close, minimize, and maximize buttons. The icon (bar, box, dot) is drawn
// on top by the caller. Using 1px bevels on gadgets (vs 4px on the frame) // on top by the caller. Using 1px bevels on gadgets (vs 4px on the frame)
// keeps them visually subordinate to the window border a Motif convention // keeps them visually subordinate to the window border -- a Motif convention
// that helps users distinguish clickable controls from structural chrome. // that helps users distinguish clickable controls from structural chrome.
static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size) { static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size) {
@ -278,7 +278,7 @@ static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
// that long label text doesn't bleed into the window border. The clip is // that long label text doesn't bleed into the window border. The clip is
// saved/restored rather than set to the full dirty rect because this // saved/restored rather than set to the full dirty rect because this
// function is called from wmDrawChrome which has already set the clip to // function is called from wmDrawChrome which has already set the clip to
// the dirty rect we need to intersect with both. The separator line at // the dirty rect -- we need to intersect with both. The separator line at
// the bottom visually separates the menu bar from the content area. // the bottom visually separates the menu bar from the content area.
static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) { static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) {
@ -331,7 +331,7 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon
// ============================================================ // ============================================================
// //
// GEOS Ensemble Motif-style resize indicators: perpendicular grooves that // GEOS Ensemble Motif-style resize indicators: perpendicular grooves that
// cut across the window border near each corner. Two breaks per corner // cut across the window border near each corner. Two breaks per corner --
// one on each edge meeting at that corner. Each groove is a 2px sunken // one on each edge meeting at that corner. Each groove is a 2px sunken
// notch (shadow line + highlight line) cutting across the full 4px border // notch (shadow line + highlight line) cutting across the full 4px border
// width, perpendicular to the edge direction. // width, perpendicular to the edge direction.
@ -479,11 +479,11 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW
// over the trough ends, and finally the thumb bevel in the middle. Arrow // over the trough ends, and finally the thumb bevel in the middle. Arrow
// buttons and thumb use 1px raised bevels for a clickable appearance; // buttons and thumb use 1px raised bevels for a clickable appearance;
// the trough uses a 1px sunken bevel (swapped highlight/shadow) to appear // the trough uses a 1px sunken bevel (swapped highlight/shadow) to appear
// recessed standard Motif scrollbar convention. // recessed -- standard Motif scrollbar convention.
// //
// winX/winY are the window's screen position; sb->x/y are relative to the // winX/winY are the window's screen position; sb->x/y are relative to the
// window origin. This split lets scrollbar positions survive window drags // window origin. This split lets scrollbar positions survive window drags
// without recalculation only winX/winY change. // without recalculation -- only winX/winY change.
static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) { static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) {
int32_t x = winX + sb->x; int32_t x = winX + sb->x;
@ -492,7 +492,7 @@ static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
BevelStyleT btnBevel; BevelStyleT btnBevel;
btnBevel.highlight = colors->windowHighlight; btnBevel.highlight = colors->windowHighlight;
btnBevel.shadow = colors->windowShadow; btnBevel.shadow = colors->windowShadow;
btnBevel.face = colors->buttonFace; btnBevel.face = colors->scrollbarBg;
btnBevel.width = 1; btnBevel.width = 1;
if (sb->orient == ScrollbarVerticalE) { if (sb->orient == ScrollbarVerticalE) {
@ -563,13 +563,13 @@ static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
// row is 2 pixels wider (1 pixel on each side), producing a symmetric // row is 2 pixels wider (1 pixel on each side), producing a symmetric
// isoceles triangle. SB_ARROW_ROWS (4) rows gives a 7-pixel-wide base, // isoceles triangle. SB_ARROW_ROWS (4) rows gives a 7-pixel-wide base,
// which fits well inside SCROLLBAR_WIDTH (16px) buttons. The glyph is // which fits well inside SCROLLBAR_WIDTH (16px) buttons. The glyph is
// centered on the button using integer division no sub-pixel alignment // centered on the button using integer division -- no sub-pixel alignment
// needed at these sizes. // needed at these sizes.
static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir) { static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir) {
int32_t cx = x + size / 2; int32_t cx = x + size / 2;
int32_t cy = y + size / 2; int32_t cy = y + size / 2;
uint32_t fg = colors->contentFg; uint32_t fg = colors->scrollbarFg;
// Draw a small triangle // Draw a small triangle
for (int32_t i = 0; i < SB_ARROW_ROWS; i++) { for (int32_t i = 0; i < SB_ARROW_ROWS; i++) {
@ -602,7 +602,7 @@ static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSche
// and maximize gadgets (right), and centered title text. // and maximize gadgets (right), and centered title text.
// //
// The title bar background uses active/inactive colors to provide a strong // The title bar background uses active/inactive colors to provide a strong
// visual cue for which window has keyboard focus the same convention used // visual cue for which window has keyboard focus -- the same convention used
// by Windows 3.x, Motif, and CDE. Only the title bar changes color on // by Windows 3.x, Motif, and CDE. Only the title bar changes color on
// focus change; the rest of the chrome stays the same. This is why // focus change; the rest of the chrome stays the same. This is why
// wmSetFocus dirties only the title bar area, not the entire window. // wmSetFocus dirties only the title bar area, not the entire window.
@ -667,7 +667,7 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
// Title text is centered in the available space between gadgets. If the // Title text is centered in the available space between gadgets. If the
// title is too long, it's truncated by character count (not pixel width) // title is too long, it's truncated by character count (not pixel width)
// since the font is fixed-width. No ellipsis is added at these sizes, // since the font is fixed-width. No ellipsis is added -- at these sizes,
// ellipsis would consume 3 characters that could show useful text instead. // ellipsis would consume 3 characters that could show useful text instead.
int32_t availW = g.textRightEdge - g.textLeftEdge; int32_t availW = g.textRightEdge - g.textLeftEdge;
@ -1003,7 +1003,7 @@ void wmAddMenuSeparator(MenuT *menu) {
// Creates a cascading submenu. Unlike top-level menus (inline in MenuBarT), // Creates a cascading submenu. Unlike top-level menus (inline in MenuBarT),
// submenus are heap-allocated because the nesting depth is unpredictable and // submenus are heap-allocated because the nesting depth is unpredictable and
// the submenu is owned by its parent item. The item's id is set to -1 to // the submenu is owned by its parent item. The item's id is set to -1 to
// distinguish it from leaf items during event dispatch clicking a submenu // distinguish it from leaf items during event dispatch -- clicking a submenu
// item opens the child rather than firing a command. // item opens the child rather than firing a command.
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) { MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) {
@ -1070,13 +1070,13 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// the frame. // the frame.
// //
// Each window gets a unique monotonic ID (static nextId) used for lookup // Each window gets a unique monotonic ID (static nextId) used for lookup
// by the event loop and DXE app system. IDs are never reused with 2^31 // by the event loop and DXE app system. IDs are never reused -- with 2^31
// IDs available and windows being created/destroyed interactively, this // IDs available and windows being created/destroyed interactively, this
// will never wrap in practice. // will never wrap in practice.
// //
// The content buffer is initialized to 0xFF (white) so newly created // The content buffer is initialized to 0xFF (white) so newly created
// windows have a clean background before the app's first onPaint fires. // windows have a clean background before the app's first onPaint fires.
// maxW/maxH default to -1 meaning "use screen dimensions" apps can // maxW/maxH default to -1 meaning "use screen dimensions" -- apps can
// override this to constrain maximized size (e.g. for dialog-like windows). // override this to constrain maximized size (e.g. for dialog-like windows).
// //
// New windows are added at stack->count (the top), so they appear in front // New windows are added at stack->count (the top), so they appear in front
@ -1213,7 +1213,7 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
// //
// Initiates a window drag by recording the mouse offset from the window // Initiates a window drag by recording the mouse offset from the window
// origin. This offset is maintained throughout the drag so that the window // origin. This offset is maintained throughout the drag so that the window
// tracks the mouse cursor without jumping the window position under the // tracks the mouse cursor without jumping -- the window position under the
// cursor stays consistent from mousedown to mouseup. // cursor stays consistent from mousedown to mouseup.
void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouseY) { void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouseY) {
@ -1244,7 +1244,7 @@ void wmDragEnd(WindowStackT *stack) {
// //
// Unlike some WMs that use an "outline drag" (drawing a wireframe while // Unlike some WMs that use an "outline drag" (drawing a wireframe while
// dragging and moving the real window on mouse-up), we do full content // dragging and moving the real window on mouse-up), we do full content
// dragging. This is feasible because the content buffer is persistent // dragging. This is feasible because the content buffer is persistent --
// we don't need to ask the app to repaint during the drag, just blit from // we don't need to ask the app to repaint during the drag, just blit from
// its buffer at the new position. // its buffer at the new position.
@ -1261,6 +1261,10 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou
win->x = mouseX - stack->dragOffX; win->x = mouseX - stack->dragOffX;
win->y = mouseY - stack->dragOffY; win->y = mouseY - stack->dragOffY;
if (win->maximized) {
win->maximized = false;
}
// Clamp: keep title bar reachable // Clamp: keep title bar reachable
if (win->y < 0) { if (win->y < 0) {
win->y = 0; win->y = 0;
@ -1293,7 +1297,7 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou
// //
// The clip rect is set to the dirty rect so all draw operations are // The clip rect is set to the dirty rect so all draw operations are
// automatically clipped. This is the mechanism by which partial chrome // automatically clipped. This is the mechanism by which partial chrome
// repaints work if only the title bar is dirty, the border fill runs // repaints work -- if only the title bar is dirty, the border fill runs
// but its output is clipped away for scanlines outside the dirty rect. // but its output is clipped away for scanlines outside the dirty rect.
// The clip rect is saved/restored because the caller (compositeAndFlush) // The clip rect is saved/restored because the caller (compositeAndFlush)
// manages its own clip state across multiple windows. // manages its own clip state across multiple windows.
@ -1359,7 +1363,7 @@ void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, con
// The blit uses direct memcpy rather than going through the rectCopy // The blit uses direct memcpy rather than going through the rectCopy
// drawing primitive, because we've already computed the exact intersection // drawing primitive, because we've already computed the exact intersection
// and don't need the general-purpose clip logic. On 486/Pentium, memcpy // and don't need the general-purpose clip logic. On 486/Pentium, memcpy
// compiles to rep movsd which saturates the memory bus no further // compiles to rep movsd which saturates the memory bus -- no further
// optimization is possible at this level. // optimization is possible at this level.
void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo) { void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo) {
@ -1406,7 +1410,7 @@ void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *
// 2. A live thumbnail of the window's content buffer (also scaled) // 2. A live thumbnail of the window's content buffer (also scaled)
// 3. A grey fill if neither is available // 3. A grey fill if neither is available
// //
// The live thumbnail approach (option 2) is a DESQview/X homage DV/X // The live thumbnail approach (option 2) is a DESQview/X homage -- DV/X
// showed miniature window contents in its icon/task view. The thumbnail // showed miniature window contents in its icon/task view. The thumbnail
// is rendered from the existing content buffer, so no extra rendering // is rendered from the existing content buffer, so no extra rendering
// pass is needed. contentDirty tracks whether the content has changed // pass is needed. contentDirty tracks whether the content has changed
@ -1459,7 +1463,7 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE,
win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp); win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp);
} else { } else {
// No content draw grey fill // No content -- draw grey fill
rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace); rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace);
} }
} }
@ -1557,7 +1561,7 @@ int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hi
return i; return i;
} }
// Title bar (drag area between gadgets) // Title bar (drag area -- between gadgets)
if (my >= g.titleY && my < g.titleY + CHROME_TITLE_HEIGHT && if (my >= g.titleY && my < g.titleY + CHROME_TITLE_HEIGHT &&
mx >= g.titleX && mx < g.titleX + g.titleW) { mx >= g.titleX && mx < g.titleX + g.titleW) {
*hitPart = HIT_TITLE; *hitPart = HIT_TITLE;
@ -1653,7 +1657,7 @@ void wmInit(WindowStackT *stack) {
// The content buffer must be reallocated because the content area changes // The content buffer must be reallocated because the content area changes
// size. After reallocation, onResize notifies the app of the new dimensions, // size. After reallocation, onResize notifies the app of the new dimensions,
// then onPaint requests a full repaint into the new buffer. This is // then onPaint requests a full repaint into the new buffer. This is
// synchronous the maximize completes in one frame, avoiding flicker. // synchronous -- the maximize completes in one frame, avoiding flicker.
// //
// Both old and new positions are dirtied: old to expose what was behind the // Both old and new positions are dirtied: old to expose what was behind the
// window at its previous size, new to paint the window at its maximized size. // window at its previous size, new to paint the window at its maximized size.
@ -1706,7 +1710,7 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
// //
// Minimizes a window: marks it minimized so it's skipped during compositing // Minimizes a window: marks it minimized so it's skipped during compositing
// (except for icon drawing) and moves focus to the next available window. // (except for icon drawing) and moves focus to the next available window.
// The window's geometry and content buffer are preserved no reallocation // The window's geometry and content buffer are preserved -- no reallocation
// is needed since the window retains its size and will be restored to the // is needed since the window retains its size and will be restored to the
// same position. // same position.
// //
@ -1788,7 +1792,7 @@ int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t
// + menu bar (if present) + scrollbar space (if present). // + menu bar (if present) + scrollbar space (if present).
// //
// This function is called on every resize move event, so it must be cheap. // This function is called on every resize move event, so it must be cheap.
// No allocations, no string operations just arithmetic on cached values. // No allocations, no string operations -- just arithmetic on cached values.
static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2; int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2;
@ -1932,7 +1936,7 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
// //
// Frees and reallocates the content buffer to match the current contentW/H. // Frees and reallocates the content buffer to match the current contentW/H.
// Called after any geometry change (resize, maximize, restore). The old // Called after any geometry change (resize, maximize, restore). The old
// buffer contents are discarded the caller is expected to trigger // buffer contents are discarded -- the caller is expected to trigger
// onResize + onPaint to refill it. This is simpler (and on 486, faster) // onResize + onPaint to refill it. This is simpler (and on 486, faster)
// than copying and scaling the old content. // than copying and scaling the old content.
// //
@ -1993,7 +1997,7 @@ void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouse
// The grab area extends 2 pixels beyond the visual border (CHROME_BORDER_WIDTH // The grab area extends 2 pixels beyond the visual border (CHROME_BORDER_WIDTH
// + 2 = 6px) to make edges easier to grab, especially the 4px-wide border // + 2 = 6px) to make edges easier to grab, especially the 4px-wide border
// which would be frustratingly small on its own. This is a common usability // which would be frustratingly small on its own. This is a common usability
// trick the visual border is narrower than the hit zone. // trick -- the visual border is narrower than the hit zone.
int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my) { int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my) {
int32_t edge = RESIZE_NONE; int32_t edge = RESIZE_NONE;
@ -2047,12 +2051,12 @@ void wmResizeEnd(WindowStackT *stack) {
// only on axes where the resize was actually applied. If clamped (window at // only on axes where the resize was actually applied. If clamped (window at
// min/max size), dragOff is NOT updated on that axis, so the accumulated // min/max size), dragOff is NOT updated on that axis, so the accumulated
// delta tracks how far the mouse moved past the border. When the user // delta tracks how far the mouse moved past the border. When the user
// reverses direction, the border immediately follows it "sticks" to // reverses direction, the border immediately follows -- it "sticks" to
// the mouse pointer instead of creating a dead zone. // the mouse pointer instead of creating a dead zone.
// //
// If the user resizes while maximized, the maximized flag is cleared. // If the user resizes while maximized, the maximized flag is cleared.
// This prevents wmRestore from snapping back to the pre-maximize geometry, // This prevents wmRestore from snapping back to the pre-maximize geometry,
// which would be confusing the user's manual resize represents their // which would be confusing -- the user's manual resize represents their
// new intent. // new intent.
void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_t *mouseX, int32_t *mouseY) { void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_t *mouseX, int32_t *mouseY) {
@ -2665,7 +2669,7 @@ void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) {
// scrollbar's presence. This creates the standard L-shaped layout where the // scrollbar's presence. This creates the standard L-shaped layout where the
// scrollbars meet at the bottom-right corner with a small dead zone between // scrollbars meet at the bottom-right corner with a small dead zone between
// them (the corner square where both scrollbars would overlap is simply not // them (the corner square where both scrollbars would overlap is simply not
// covered it shows the window face color from drawBorderFrame). // covered -- it shows the window face color from drawBorderFrame).
void wmUpdateContentRect(WindowT *win) { void wmUpdateContentRect(WindowT *win) {
int32_t topChrome = CHROME_TOTAL_TOP; int32_t topChrome = CHROME_TOTAL_TOP;

View file

@ -1,4 +1,4 @@
// dvx_wm.h Layer 4: Window manager for DVX GUI // dvx_wm.h -- Layer 4: Window manager for DVX GUI
// //
// Manages the window lifecycle, Z-order stack, chrome drawing, hit testing, // Manages the window lifecycle, Z-order stack, chrome drawing, hit testing,
// and interactive operations (drag, resize, scroll). This layer bridges the // and interactive operations (drag, resize, scroll). This layer bridges the
@ -8,7 +8,7 @@
// Design philosophy: the WM owns window geometry and chrome, but the // Design philosophy: the WM owns window geometry and chrome, but the
// window's content is owned by the application (via callbacks or the widget // window's content is owned by the application (via callbacks or the widget
// system). This separation means the WM can move, resize, and repaint // system). This separation means the WM can move, resize, and repaint
// window frames without involving the application at all only content // window frames without involving the application at all -- only content
// changes trigger application callbacks. // changes trigger application callbacks.
// //
// All WM operations that change visible screen state accept a DirtyListT* // All WM operations that change visible screen state accept a DirtyListT*
@ -24,7 +24,7 @@ void wmInit(WindowStackT *stack);
// Allocate a new WindowT, initialize its geometry and content buffer, and // Allocate a new WindowT, initialize its geometry and content buffer, and
// push it to the top of the Z-order stack. The returned window has default // push it to the top of the Z-order stack. The returned window has default
// callbacks (all NULL) the caller should set onPaint/onKey/etc. before // callbacks (all NULL) -- the caller should set onPaint/onKey/etc. before
// the next event loop iteration. // the next event loop iteration.
WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable); WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable);
@ -50,7 +50,7 @@ void wmUpdateContentRect(WindowT *win);
// Reallocate the per-window content backbuffer to match the current // Reallocate the per-window content backbuffer to match the current
// contentW/H. Returns 0 on success, -1 if allocation fails. The old // contentW/H. Returns 0 on success, -1 if allocation fails. The old
// buffer contents are lost the caller should trigger a full repaint // buffer contents are lost -- the caller should trigger a full repaint
// via onPaint after a successful realloc. // via onPaint after a successful realloc.
int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d); int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d);
@ -72,7 +72,7 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id);
void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked); void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked);
// Add a radio-style item. Radio groups are defined implicitly by // Add a radio-style item. Radio groups are defined implicitly by
// consecutive radio items in the same menu selecting one unchecks // consecutive radio items in the same menu -- selecting one unchecks
// the others in the group. // the others in the group.
void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked); void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked);
@ -101,7 +101,7 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo); void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo);
// Blit the window's content backbuffer into the display backbuffer, // Blit the window's content backbuffer into the display backbuffer,
// clipped to the dirty rect. This is a pure copy (no drawing) the // clipped to the dirty rect. This is a pure copy (no drawing) -- the
// content was already rendered by the application into contentBuf. // content was already rendered by the application into contentBuf.
void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo); void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo);

View file

@ -1,13 +1,13 @@
// dvxPlatform.h Platform abstraction layer for DVX GUI // dvxPlatform.h -- Platform abstraction layer for DVX GUI
// //
// All OS-specific and CPU-specific code is isolated behind this // All OS-specific and CPU-specific code is isolated behind this
// interface. To port DVX to a new platform, implement a new // interface. To port DVX to a new platform, implement a new
// dvxPlatformXxx.c against this header. // dvxPlatformXxx.c against this header.
// //
// Currently two implementations exist: // Currently two implementations exist:
// dvxPlatformDos.c DJGPP/DPMI: real VESA VBE, INT 33h mouse, // dvxPlatformDos.c -- DJGPP/DPMI: real VESA VBE, INT 33h mouse,
// INT 16h keyboard, rep movsd/stosl asm spans // INT 16h keyboard, rep movsd/stosl asm spans
// dvxPlatformLinux.c SDL2: software rendering to an SDL window, // dvxPlatformLinux.c -- SDL2: software rendering to an SDL window,
// used for development and testing on Linux // used for development and testing on Linux
// //
// The abstraction covers five areas: video mode setup, framebuffer // The abstraction covers five areas: video mode setup, framebuffer
@ -45,7 +45,7 @@ typedef struct {
// for clean shutdown on Ctrl+C/Ctrl+Break. On Linux this initializes SDL. // for clean shutdown on Ctrl+C/Ctrl+Break. On Linux this initializes SDL.
void platformInit(void); void platformInit(void);
// Cooperative yield give up the CPU timeslice when the event loop has // Cooperative yield -- give up the CPU timeslice when the event loop has
// nothing to do. On DOS this calls __dpmi_yield() to be friendly to // nothing to do. On DOS this calls __dpmi_yield() to be friendly to
// multitaskers (Windows 3.x, OS/2, DESQview). On Linux this calls // multitaskers (Windows 3.x, OS/2, DESQview). On Linux this calls
// SDL_Delay(1) to avoid busy-spinning at 100% CPU. // SDL_Delay(1) to avoid busy-spinning at 100% CPU.
@ -82,7 +82,7 @@ void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t cou
// Copy a rectangle from the system RAM backbuffer (d->backBuf) to the // Copy a rectangle from the system RAM backbuffer (d->backBuf) to the
// display surface (d->lfb). On DOS this copies to real video memory via // display surface (d->lfb). On DOS this copies to real video memory via
// the LFB mapping the critical path where PCI bus write speed matters. // the LFB mapping -- the critical path where PCI bus write speed matters.
// On Linux this copies to the SDL surface, then SDL_UpdateRect is called. // On Linux this copies to the SDL surface, then SDL_UpdateRect is called.
// Each scanline is copied as a contiguous block; rep movsd on DOS gives // Each scanline is copied as a contiguous block; rep movsd on DOS gives
// near-optimal bus utilization for aligned 32-bit writes. // near-optimal bus utilization for aligned 32-bit writes.
@ -92,7 +92,7 @@ void platformFlushRect(const DisplayT *d, const RectT *r);
// Optimised memory operations (span fill / copy) // Optimised memory operations (span fill / copy)
// ============================================================ // ============================================================
// //
// These are the innermost loops of the renderer called once per // These are the innermost loops of the renderer -- called once per
// scanline of every rectangle fill, blit, and text draw. On DOS they // scanline of every rectangle fill, blit, and text draw. On DOS they
// use inline assembly: rep stosl for fills (one instruction fills an // use inline assembly: rep stosl for fills (one instruction fills an
// entire scanline) and rep movsd for copies. On Linux they use memset/ // entire scanline) and rep movsd for copies. On Linux they use memset/
@ -112,7 +112,7 @@ void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count); void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count);
// ============================================================ // ============================================================
// Input Mouse // Input -- Mouse
// ============================================================ // ============================================================
// Initialize the mouse driver and constrain movement to the screen bounds. // Initialize the mouse driver and constrain movement to the screen bounds.
@ -122,14 +122,14 @@ void platformMouseInit(int32_t screenW, int32_t screenH);
// Poll the current mouse state. Buttons is a bitmask: bit 0 = left, // Poll the current mouse state. Buttons is a bitmask: bit 0 = left,
// bit 1 = right, bit 2 = middle. Polling (rather than event-driven // bit 1 = right, bit 2 = middle. Polling (rather than event-driven
// callbacks) is the natural model for a cooperative event loop the // callbacks) is the natural model for a cooperative event loop -- the
// main loop polls once per frame and compares with the previous state // main loop polls once per frame and compares with the previous state
// to detect press/release edges. // to detect press/release edges.
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons); void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons);
// Detect and activate mouse wheel support. Returns true if the mouse // Detect and activate mouse wheel support. Returns true if the mouse
// driver supports the CuteMouse Wheel API (INT 33h AX=0011h). This // driver supports the CuteMouse Wheel API (INT 33h AX=0011h). This
// call also activates wheel reporting after it returns true, function // call also activates wheel reporting -- after it returns true, function
// 03h will return wheel delta in BH. Must be called after platformMouseInit. // 03h will return wheel delta in BH. Must be called after platformMouseInit.
bool platformMouseWheelInit(void); bool platformMouseWheelInit(void);
@ -150,7 +150,7 @@ void platformMouseSetAccel(int32_t threshold);
void platformMouseWarp(int32_t x, int32_t y); void platformMouseWarp(int32_t x, int32_t y);
// ============================================================ // ============================================================
// Input Keyboard // Input -- Keyboard
// ============================================================ // ============================================================
// Return the current modifier key state in BIOS shift-state format: // Return the current modifier key state in BIOS shift-state format:
@ -167,7 +167,7 @@ int32_t platformKeyboardGetModifiers(void);
bool platformKeyboardRead(PlatformKeyEventT *evt); bool platformKeyboardRead(PlatformKeyEventT *evt);
// Translate an Alt+key scancode to its corresponding ASCII character. // Translate an Alt+key scancode to its corresponding ASCII character.
// When Alt is held, DOS doesn't provide the ASCII value only the // When Alt is held, DOS doesn't provide the ASCII value -- only the
// scancode. This function contains a lookup table mapping scancodes // scancode. This function contains a lookup table mapping scancodes
// to their unshifted letter/digit. Returns 0 for scancodes that don't // to their unshifted letter/digit. Returns 0 for scancodes that don't
// map to a printable character (e.g. Alt+F1). // map to a printable character (e.g. Alt+F1).
@ -204,6 +204,11 @@ const char *platformValidateFilename(const char *name);
// and free physical memory in kilobytes. Returns false if unavailable. // and free physical memory in kilobytes. Returns false if unavailable.
bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb); bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb);
// Create a directory and all parent directories (like mkdir -p).
// Returns 0 on success, -1 on failure. Existing directories are not
// an error.
int32_t platformMkdirRecursive(const char *path);
// Change the working directory, including drive letter on DOS. Standard // Change the working directory, including drive letter on DOS. Standard
// chdir() does not switch drives under DJGPP; this wrapper calls setdisk() // chdir() does not switch drives under DJGPP; this wrapper calls setdisk()
// first when the path contains a drive prefix (e.g. "A:\DVX"). // first when the path contains a drive prefix (e.g. "A:\DVX").

View file

@ -1,4 +1,4 @@
// dvxPlatformDos.c DOS/DJGPP platform implementation for DVX GUI // dvxPlatformDos.c -- DOS/DJGPP platform implementation for DVX GUI
// //
// All BIOS calls, DPMI functions, port I/O, inline assembly, and // All BIOS calls, DPMI functions, port I/O, inline assembly, and
// DOS-specific file handling are isolated in this single file. // DOS-specific file handling are isolated in this single file.
@ -21,7 +21,7 @@
// IRQ1 to feed the BIOS buffer. The BIOS approach is simpler and more // IRQ1 to feed the BIOS buffer. The BIOS approach is simpler and more
// portable across emulators (DOSBox, 86Box, PCem all handle it correctly). // portable across emulators (DOSBox, 86Box, PCem all handle it correctly).
// //
// Why INT 33h for mouse: same rationale the mouse driver handles // Why INT 33h for mouse: same rationale -- the mouse driver handles
// PS/2 and serial mice transparently, and every DOS emulator provides // PS/2 and serial mice transparently, and every DOS emulator provides
// a compatible driver. Polling via function 03h avoids the complexity // a compatible driver. Polling via function 03h avoids the complexity
// of installing a real-mode callback for mouse events. // of installing a real-mode callback for mouse events.
@ -31,14 +31,16 @@
#include <ctype.h> #include <ctype.h>
#include <dir.h> #include <dir.h>
#include <errno.h>
#include <signal.h> #include <signal.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
// DJGPP-specific headers this is the ONLY file that includes these // DJGPP-specific headers -- this is the ONLY file that includes these
#include <dpmi.h> #include <dpmi.h>
#include <go32.h> #include <go32.h>
#include <pc.h> #include <pc.h>
@ -132,7 +134,7 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
memset(&bestDisplay, 0, sizeof(bestDisplay)); memset(&bestDisplay, 0, sizeof(bestDisplay));
// Get VBE controller info the transfer buffer (__tb) is the DJGPP- // Get VBE controller info -- the transfer buffer (__tb) is the DJGPP-
// provided region in conventional memory that real-mode BIOS calls // provided region in conventional memory that real-mode BIOS calls
// can read/write. We split it into seg:off for the INT 10h call. // can read/write. We split it into seg:off for the INT 10h call.
uint32_t infoSeg = __tb >> 4; uint32_t infoSeg = __tb >> 4;
@ -172,7 +174,7 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
// VBE 2.0+ is required for LFB (Linear Frame Buffer) support. // VBE 2.0+ is required for LFB (Linear Frame Buffer) support.
// VBE 1.x only supports bank switching, which we explicitly don't // VBE 1.x only supports bank switching, which we explicitly don't
// implement the complexity isn't worth it for 486+ targets. // implement -- the complexity isn't worth it for 486+ targets.
uint16_t vbeVersion = _farpeekw(_dos_ds, __tb + 4); uint16_t vbeVersion = _farpeekw(_dos_ds, __tb + 4);
if (vbeVersion < 0x0200) { if (vbeVersion < 0x0200) {
fprintf(stderr, "VBE: Version %d.%d too old (need 2.0+)\n", fprintf(stderr, "VBE: Version %d.%d too old (need 2.0+)\n",
@ -323,7 +325,7 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
// pixels can't use dword-aligned rep stosl fills without masking. // pixels can't use dword-aligned rep stosl fills without masking.
// //
// The physical LFB address is temporarily stored in d->lfb as a raw // The physical LFB address is temporarily stored in d->lfb as a raw
// integer cast it will be properly mapped via DPMI in mapLfb() later. // integer cast -- it will be properly mapped via DPMI in mapLfb() later.
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
__dpmi_regs r; __dpmi_regs r;
@ -343,7 +345,7 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ
// VBE mode attribute word at offset 0: // VBE mode attribute word at offset 0:
// bit 7 = LFB available, bit 4 = graphics mode (not text) // bit 7 = LFB available, bit 4 = graphics mode (not text)
// Both are required we never bank-switch and never want text modes. // Both are required -- we never bank-switch and never want text modes.
uint16_t attr = _farpeekw(_dos_ds, __tb + 0); uint16_t attr = _farpeekw(_dos_ds, __tb + 0);
if (!(attr & 0x0080)) { if (!(attr & 0x0080)) {
@ -442,14 +444,14 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ
// access. // access.
// //
// The mapping process has three steps: // The mapping process has three steps:
// 1. __dpmi_physical_address_mapping() asks the DPMI host to // 1. __dpmi_physical_address_mapping() -- asks the DPMI host to
// create a linear address mapping for the physical framebuffer. // create a linear address mapping for the physical framebuffer.
// This is necessary because DPMI runs in protected mode with // This is necessary because DPMI runs in protected mode with
// paging; physical addresses aren't directly accessible. // paging; physical addresses aren't directly accessible.
// 2. __dpmi_lock_linear_region() pins the mapped pages so they // 2. __dpmi_lock_linear_region() -- pins the mapped pages so they
// can't be swapped out. The LFB is memory-mapped I/O to the // can't be swapped out. The LFB is memory-mapped I/O to the
// video card; paging it would be catastrophic. // video card; paging it would be catastrophic.
// 3. __djgpp_nearptr_enable() disables DJGPP's default segment // 3. __djgpp_nearptr_enable() -- disables DJGPP's default segment
// limit checking so we can use plain C pointers to access the // limit checking so we can use plain C pointers to access the
// LFB address. Without this, all LFB access would require far // LFB address. Without this, all LFB access would require far
// pointer calls (_farpokeb etc.), which are much slower because // pointer calls (_farpokeb etc.), which are much slower because
@ -457,7 +459,7 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ
// //
// Why near pointers: the performance difference is dramatic. // Why near pointers: the performance difference is dramatic.
// platformFlushRect() copies thousands of dwords per frame using // platformFlushRect() copies thousands of dwords per frame using
// rep movsl this only works with near pointers. Far pointer access // rep movsl -- this only works with near pointers. Far pointer access
// would add ~10 cycles per byte and make 60fps impossible on a 486. // would add ~10 cycles per byte and make 60fps impossible on a 486.
// //
// The final pointer calculation adds __djgpp_conventional_base, which // The final pointer calculation adds __djgpp_conventional_base, which
@ -529,7 +531,7 @@ void platformChdir(const char *path) {
// ============================================================ // ============================================================
// //
// Copies a dirty rectangle from the system RAM backbuffer to the LFB. // Copies a dirty rectangle from the system RAM backbuffer to the LFB.
// This is the critical path for display updates the compositor calls // This is the critical path for display updates -- the compositor calls
// it once per dirty rect per frame. // it once per dirty rect per frame.
// //
// Two code paths: // Two code paths:
@ -580,7 +582,7 @@ void platformFlushRect(const DisplayT *d, const RectT *r) {
*dst++ = *src++; *dst++ = *src++;
} }
} else { } else {
// Partial scanlines copy row by row with rep movsd // Partial scanlines -- copy row by row with rep movsd
int32_t dwords = rowBytes >> 2; int32_t dwords = rowBytes >> 2;
int32_t remainder = rowBytes & 3; int32_t remainder = rowBytes & 3;
for (int32_t i = 0; i < h; i++) { for (int32_t i = 0; i < h; i++) {
@ -607,7 +609,7 @@ void platformFlushRect(const DisplayT *d, const RectT *r) {
// ============================================================ // ============================================================
// System information static buffer and helpers // System information -- static buffer and helpers
// ============================================================ // ============================================================
static char sSysInfoBuf[PLATFORM_SYSINFO_MAX]; static char sSysInfoBuf[PLATFORM_SYSINFO_MAX];
@ -637,7 +639,7 @@ static void sysInfoAppend(const char *fmt, ...) {
// ============================================================ // ============================================================
// estimateClockMhz RDTSC calibration via BIOS timer // estimateClockMhz -- RDTSC calibration via BIOS timer
// ============================================================ // ============================================================
// //
// Measures TSC ticks over 3 BIOS timer ticks (~165 ms). The BIOS timer // Measures TSC ticks over 3 BIOS timer ticks (~165 ms). The BIOS timer
@ -688,7 +690,7 @@ static uint32_t estimateClockMhz(void) {
// ============================================================ // ============================================================
// hasCpuid check if CPUID instruction is available // hasCpuid -- check if CPUID instruction is available
// ============================================================ // ============================================================
// //
// The CPUID instruction exists if bit 21 (ID flag) of EFLAGS can be // The CPUID instruction exists if bit 21 (ID flag) of EFLAGS can be
@ -1079,15 +1081,67 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) {
} }
// ============================================================
// platformMkdirRecursive
// ============================================================
//
// Creates a directory and all parent directories that don't exist.
// Works by walking the path from left to right, creating each
// component. mkdir() on an existing directory returns EEXIST which
// is silently ignored.
int32_t platformMkdirRecursive(const char *path) {
char buf[260];
strncpy(buf, path, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
for (char *p = buf; *p; p++) {
// Skip drive letter (e.g. "C:\")
if (p == buf + 1 && *p == ':') {
continue;
}
if (*p == '/' || *p == '\\') {
*p = '\0';
if (buf[0] != '\0') {
mkdir(buf, 0755);
}
*p = '/';
}
}
// Create the final directory
if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
// ============================================================ // ============================================================
// platformInit // platformInit
// ============================================================ // ============================================================
void platformInit(void) { void platformInit(void) {
// Disable Ctrl+C/Break so the user can't accidentally kill the // Disable Ctrl+C/Break at every level so the user can't
// GUI while in graphics mode (which would leave the display in // accidentally kill the GUI while in graphics mode.
// an unusable state without restoring text mode first).
// 1. DJGPP-level: disable Ctrl+C detection in the runtime
__djgpp_set_ctrl_c(0);
// 2. C-level: ignore SIGINT if it somehow fires
signal(SIGINT, SIG_IGN); signal(SIGINT, SIG_IGN);
// 3. DOS-level: disable break checking during INT 21h calls
// (function 33h, subfunction 01h, DL=0 = break off)
__dpmi_regs r;
memset(&r, 0, sizeof(r));
r.x.ax = 0x3301;
r.x.dx = 0x0000;
__dpmi_int(0x21, &r);
} }
@ -1125,7 +1179,7 @@ int32_t platformKeyboardGetModifiers(void) {
// functions have been standard since AT-class machines (1984). // functions have been standard since AT-class machines (1984).
// //
// The two-step peek-then-read is necessary because function 10h // The two-step peek-then-read is necessary because function 10h
// (read key) blocks until a key is available there's no non-blocking // (read key) blocks until a key is available -- there's no non-blocking
// read in the BIOS API. Function 11h (check key) peeks without // read in the BIOS API. Function 11h (check key) peeks without
// consuming, letting us poll without blocking the event loop. // consuming, letting us poll without blocking the event loop.
@ -1200,7 +1254,7 @@ int32_t platformStripLineEndings(char *buf, int32_t len) {
// because the default range may be 640x200 (CGA text mode). // because the default range may be 640x200 (CGA text mode).
// Without this, mouse coordinates would be wrong or clipped. // Without this, mouse coordinates would be wrong or clipped.
// //
// The hardware cursor is never shown DVX composites its own // The hardware cursor is never shown -- DVX composites its own
// software cursor on top of the backbuffer. We only use INT 33h // software cursor on top of the backbuffer. We only use INT 33h
// for position/button state via polling (function 03h). // for position/button state via polling (function 03h).
@ -1390,7 +1444,7 @@ char *platformPathDirEnd(const char *path) {
// bus speed. // bus speed.
// //
// rep movsl moves 4 bytes per iteration with hardware loop decrement, // rep movsl moves 4 bytes per iteration with hardware loop decrement,
// which is faster than a C for-loop the CPU string move pipeline // which is faster than a C for-loop -- the CPU string move pipeline
// optimizes sequential memory access patterns. // optimizes sequential memory access patterns.
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) { void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) {
@ -1461,7 +1515,7 @@ void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) {
// ============================================================ // ============================================================
// //
// 32-bit pixels are inherently dword-aligned, so no alignment // 32-bit pixels are inherently dword-aligned, so no alignment
// preamble is needed straight to rep movsl. // preamble is needed -- straight to rep movsl.
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) { void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) {
__asm__ __volatile__ ( __asm__ __volatile__ (
@ -1487,7 +1541,7 @@ void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) {
uint8_t c = (uint8_t)color; uint8_t c = (uint8_t)color;
uint32_t dword = (uint32_t)c | ((uint32_t)c << 8) | ((uint32_t)c << 16) | ((uint32_t)c << 24); uint32_t dword = (uint32_t)c | ((uint32_t)c << 8) | ((uint32_t)c << 16) | ((uint32_t)c << 24);
// Align to 4 bytes skip if already aligned // Align to 4 bytes -- skip if already aligned
if (__builtin_expect((uintptr_t)dst & 3, 0)) { if (__builtin_expect((uintptr_t)dst & 3, 0)) {
while (((uintptr_t)dst & 3) && count > 0) { while (((uintptr_t)dst & 3) && count > 0) {
*dst++ = c; *dst++ = c;
@ -1555,7 +1609,7 @@ void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) {
// platformSpanFill32 // platformSpanFill32
// ============================================================ // ============================================================
// //
// 32-bit fill is the simplest case each pixel is already a dword, // 32-bit fill is the simplest case -- each pixel is already a dword,
// so rep stosl writes exactly one pixel per iteration with no // so rep stosl writes exactly one pixel per iteration with no
// alignment or packing concerns. // alignment or packing concerns.
@ -1570,7 +1624,7 @@ void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) {
// ============================================================ // ============================================================
// platformValidateFilename DOS 8.3 filename validation // platformValidateFilename -- DOS 8.3 filename validation
// ============================================================ // ============================================================
// //
// Validates that a filename conforms to DOS 8.3 conventions: // Validates that a filename conforms to DOS 8.3 conventions:
@ -1580,7 +1634,7 @@ void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) {
// //
// The reserved name check compares the base name only (before the // The reserved name check compares the base name only (before the
// dot), case-insensitive, because DOS treats "CON.TXT" the same // dot), case-insensitive, because DOS treats "CON.TXT" the same
// as the CON device the extension is ignored for device names. // as the CON device -- the extension is ignored for device names.
// //
// Returns NULL on success, or a human-readable error string on failure. // Returns NULL on success, or a human-readable error string on failure.
// On non-DOS platforms, this function would be replaced with one that // On non-DOS platforms, this function would be replaced with one that
@ -1664,9 +1718,9 @@ const char *platformValidateFilename(const char *name) {
// ============================================================ // ============================================================
// //
// Complete video initialization sequence: // Complete video initialization sequence:
// 1. findBestMode() enumerate VESA modes and pick the best match // 1. findBestMode() -- enumerate VESA modes and pick the best match
// 2. setVesaMode() actually switch to the chosen mode with LFB // 2. setVesaMode() -- actually switch to the chosen mode with LFB
// 3. mapLfb() DPMI-map the physical framebuffer into linear memory // 3. mapLfb() -- DPMI-map the physical framebuffer into linear memory
// 4. Allocate system RAM backbuffer (same size as LFB) // 4. Allocate system RAM backbuffer (same size as LFB)
// 5. Set up 8-bit palette if needed // 5. Set up 8-bit palette if needed
// 6. Initialize clip rect to full display // 6. Initialize clip rect to full display
@ -1755,7 +1809,7 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i
// //
// Direct port I/O is used instead of VBE function 09h (set palette) // Direct port I/O is used instead of VBE function 09h (set palette)
// because the VGA DAC ports are faster (no BIOS call overhead) and // because the VGA DAC ports are faster (no BIOS call overhead) and
// universally compatible even VBE 3.0 cards still have the standard // universally compatible -- even VBE 3.0 cards still have the standard
// VGA DAC at ports 0x3C8/0x3C9. // VGA DAC at ports 0x3C8/0x3C9.
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count) { void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count) {

View file

@ -1,4 +1,4 @@
// widgetAnsiTerm.c ANSI BBS terminal emulator widget // widgetAnsiTerm.c -- ANSI BBS terminal emulator widget
// //
// Implements a VT100/ANSI-compatible terminal emulator widget designed for // Implements a VT100/ANSI-compatible terminal emulator widget designed for
// connecting to BBS systems over the serial link. The terminal uses a // connecting to BBS systems over the serial link. The terminal uses a
@ -7,15 +7,15 @@
// chosen because: // chosen because:
// 1. It maps directly to the BBS/ANSI art paradigm (CP437 character set, // 1. It maps directly to the BBS/ANSI art paradigm (CP437 character set,
// 16-color CGA palette, blink attribute) // 16-color CGA palette, blink attribute)
// 2. Cell-based storage is extremely compact 2 bytes per cell means an // 2. Cell-based storage is extremely compact -- 2 bytes per cell means an
// 80x25 screen is only 4000 bytes, fitting in L1 cache on a 486 // 80x25 screen is only 4000 bytes, fitting in L1 cache on a 486
// 3. Dirty-row tracking via a 32-bit bitmask allows sub-millisecond // 3. Dirty-row tracking via a 32-bit bitmask allows sub-millisecond
// incremental repaints without scanning the entire buffer // incremental repaints without scanning the entire buffer
// //
// The ANSI parser is a 3-state machine (NORMAL → ESC → CSI) that handles // The ANSI parser is a 3-state machine (NORMAL -> ESC -> CSI) that handles
// the subset of sequences commonly used by DOS BBS software: cursor movement, // the subset of sequences commonly used by DOS BBS software: cursor movement,
// screen/line erase, scrolling regions, SGR colors, and a few DEC private modes. // screen/line erase, scrolling regions, SGR colors, and a few DEC private modes.
// Full VT100 conformance is explicitly NOT a goal only sequences actually // Full VT100 conformance is explicitly NOT a goal -- only sequences actually
// emitted by real BBS systems are implemented. // emitted by real BBS systems are implemented.
// //
// Scrollback is implemented as a circular buffer of row snapshots. Only // Scrollback is implemented as a circular buffer of row snapshots. Only
@ -28,7 +28,7 @@
// - Fast repaint (wgtAnsiTermRepaint): bypasses the widget pipeline // - Fast repaint (wgtAnsiTermRepaint): bypasses the widget pipeline
// entirely, rendering dirty rows directly into the window's content // entirely, rendering dirty rows directly into the window's content
// buffer. This is critical for serial communication where ACK turnaround // buffer. This is critical for serial communication where ACK turnaround
// time matters the fewer milliseconds between receiving data and // time matters -- the fewer milliseconds between receiving data and
// displaying it, the higher the effective throughput. // displaying it, the higher the effective throughput.
// //
// Communication is abstracted through read/write function pointers, allowing // Communication is abstracted through read/write function pointers, allowing
@ -135,7 +135,7 @@ static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t
// ============================================================ // ============================================================
// //
// Copy a screen row into the scrollback circular buffer. // Copy a screen row into the scrollback circular buffer.
// The circular buffer avoids any need for memmove when the buffer fills // The circular buffer avoids any need for memmove when the buffer fills --
// the head index simply wraps around, overwriting the oldest entry. This // the head index simply wraps around, overwriting the oldest entry. This
// is O(1) per row regardless of scrollback size, which matters when BBS // is O(1) per row regardless of scrollback size, which matters when BBS
// software rapidly dumps text (e.g. file listings, ANSI art). // software rapidly dumps text (e.g. file listings, ANSI art).
@ -225,7 +225,7 @@ static void ansiTermCopySelection(WidgetT *w) {
// Build text from selected cells (strip trailing spaces per line). // Build text from selected cells (strip trailing spaces per line).
// Trailing spaces are stripped because BBS text mode fills the entire // Trailing spaces are stripped because BBS text mode fills the entire
// row with spaces without stripping, pasting would include many // row with spaces -- without stripping, pasting would include many
// unwanted trailing blanks. Fixed 4KB buffer is sufficient for typical // unwanted trailing blanks. Fixed 4KB buffer is sufficient for typical
// terminal selections (80 cols * 50 rows = 4000 chars max). // terminal selections (80 cols * 50 rows = 4000 chars max).
char buf[4096]; char buf[4096];
@ -269,7 +269,7 @@ static void ansiTermCopySelection(WidgetT *w) {
// ============================================================ // ============================================================
// //
// Mark rows dirty that are touched by a cell range [startCell, startCell+count). // Mark rows dirty that are touched by a cell range [startCell, startCell+count).
// Uses a 32-bit bitmask — one bit per row — which limits tracking to the first // Uses a 32-bit bitmask -- one bit per row -- which limits tracking to the first
// 32 rows. This is fine because standard terminal sizes are 24-25 rows, and // 32 rows. This is fine because standard terminal sizes are 24-25 rows, and
// bitmask operations are single-cycle on the target CPU. The bitmask approach // bitmask operations are single-cycle on the target CPU. The bitmask approach
// is much cheaper than maintaining a dirty rect list for per-row tracking. // is much cheaper than maintaining a dirty rect list for per-row tracking.
@ -345,7 +345,7 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
// Central CSI dispatcher. After the parser accumulates parameters in the CSI // Central CSI dispatcher. After the parser accumulates parameters in the CSI
// state, the final byte triggers dispatch here. Only sequences commonly used // state, the final byte triggers dispatch here. Only sequences commonly used
// by BBS software are implemented exotic VT220+ sequences are silently ignored. // by BBS software are implemented -- exotic VT220+ sequences are silently ignored.
// DEC private modes (ESC[?...) are handled separately since they use a different // DEC private modes (ESC[?...) are handled separately since they use a different
// parameter namespace than standard ECMA-48 sequences. // parameter namespace than standard ECMA-48 sequences.
static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
@ -474,7 +474,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
if (w->as.ansiTerm->commWrite) { if (w->as.ansiTerm->commWrite) {
// Respond as VT100 with advanced video option (AVO). // Respond as VT100 with advanced video option (AVO).
// Many BBS door games query DA to detect terminal capabilities. // Many BBS door games query DA to detect terminal capabilities.
// Claiming VT100+AVO is the safest response it tells the remote // Claiming VT100+AVO is the safest response -- it tells the remote
// side we support ANSI color without implying VT220+ features. // side we support ANSI color without implying VT220+ features.
const uint8_t reply[] = "\033[?1;2c"; const uint8_t reply[] = "\033[?1;2c";
w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, reply, 7); w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, reply, 7);
@ -632,7 +632,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
if (w->as.ansiTerm->commWrite) { if (w->as.ansiTerm->commWrite) {
if (mode == 6) { if (mode == 6) {
// CPR cursor position report: ESC[row;colR (1-based). // CPR -- cursor position report: ESC[row;colR (1-based).
// BBS software uses this for screen-size detection and // BBS software uses this for screen-size detection and
// to synchronize cursor positioning in door games. // to synchronize cursor positioning in door games.
char reply[16]; char reply[16];
@ -673,7 +673,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
w->as.ansiTerm->scrollTop = top; w->as.ansiTerm->scrollTop = top;
w->as.ansiTerm->scrollBot = bot; w->as.ansiTerm->scrollBot = bot;
} else { } else {
// Invalid or reset restore full screen // Invalid or reset -- restore full screen
w->as.ansiTerm->scrollTop = 0; w->as.ansiTerm->scrollTop = 0;
w->as.ansiTerm->scrollBot = rows - 1; w->as.ansiTerm->scrollBot = rows - 1;
} }
@ -706,7 +706,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
// Erase Display (ED): mode 0 = cursor to end, 1 = start to cursor, 2 = all. // Erase Display (ED): mode 0 = cursor to end, 1 = start to cursor, 2 = all.
// Mode 2 pushes all visible lines to scrollback before clearing, preserving // Mode 2 pushes all visible lines to scrollback before clearing, preserving
// content that was on screen this is what users expect when a BBS sends // content that was on screen -- this is what users expect when a BBS sends
// a clear-screen sequence (they can scroll back to see previous content). // a clear-screen sequence (they can scroll back to see previous content).
// The wasAtBottom check ensures auto-scroll tracking: if the user was // The wasAtBottom check ensures auto-scroll tracking: if the user was
// already viewing the latest content, they stay at the bottom after new // already viewing the latest content, they stay at the bottom after new
@ -723,7 +723,7 @@ static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) {
// Erase from start to cursor // Erase from start to cursor
ansiTermFillCells(w, 0, cur + 1); ansiTermFillCells(w, 0, cur + 1);
} else if (mode == 2) { } else if (mode == 2) {
// Erase entire screen push all lines to scrollback first // Erase entire screen -- push all lines to scrollback first
bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount);
for (int32_t r = 0; r < rows; r++) { for (int32_t r = 0; r < rows; r++) {
@ -768,7 +768,7 @@ static void ansiTermEraseLine(WidgetT *w, int32_t mode) {
// //
// Fill a range of cells with space + current attribute. // Fill a range of cells with space + current attribute.
// Uses the current attribute (not default) so that erasing respects the // Uses the current attribute (not default) so that erasing respects the
// currently active background color this is correct per ECMA-48 and // currently active background color -- this is correct per ECMA-48 and
// matches what BBS software expects (e.g., colored backgrounds that // matches what BBS software expects (e.g., colored backgrounds that
// persist after a line erase). // persist after a line erase).
@ -801,10 +801,10 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) {
// //
// Get a pointer to the cell data for a given line index in the // Get a pointer to the cell data for a given line index in the
// combined scrollback+screen view. // combined scrollback+screen view.
// lineIndex < scrollbackCount scrollback line // lineIndex < scrollbackCount -> scrollback line
// lineIndex >= scrollbackCount screen line // lineIndex >= scrollbackCount -> screen line
// //
// The unified line index simplifies paint and selection code they don't // The unified line index simplifies paint and selection code -- they don't
// need to know whether a given line is in scrollback or on screen. The // need to know whether a given line is in scrollback or on screen. The
// circular buffer index computation uses modular arithmetic to map from // circular buffer index computation uses modular arithmetic to map from
// logical scrollback line number to physical buffer position. // logical scrollback line number to physical buffer position.
@ -909,15 +909,15 @@ static void ansiTermNewline(WidgetT *w) {
// ============================================================ // ============================================================
// //
// Feed one byte through the ANSI parser state machine. // Feed one byte through the ANSI parser state machine.
// This is the hot path for data reception called once per byte received // This is the hot path for data reception -- called once per byte received
// from the serial link. The state machine is kept as simple as possible // from the serial link. The state machine is kept as simple as possible
// (no tables, no indirect calls) because this runs on every incoming byte // (no tables, no indirect calls) because this runs on every incoming byte
// and branch prediction on a 486/Pentium benefits from straightforward // and branch prediction on a 486/Pentium benefits from straightforward
// if/switch chains. // if/switch chains.
// //
// In PARSE_NORMAL, C0 control characters (CR, LF, BS, TAB, FF, BEL, ESC) // In PARSE_NORMAL, C0 control characters (CR, LF, BS, TAB, FF, BEL, ESC)
// are handled first. All other bytes including CP437 graphic characters // are handled first. All other bytes -- including CP437 graphic characters
// in the 0x01-0x1F range that aren't C0 controls, plus 0x80-0xFF are // in the 0x01-0x1F range that aren't C0 controls, plus 0x80-0xFF -- are
// treated as printable and placed at the cursor via ansiTermPutChar. // treated as printable and placed at the cursor via ansiTermPutChar.
static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
@ -942,12 +942,12 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
w->as.ansiTerm->cursorCol = w->as.ansiTerm->cols - 1; w->as.ansiTerm->cursorCol = w->as.ansiTerm->cols - 1;
} }
} else if (ch == '\f') { } else if (ch == '\f') {
// Form feed clear screen and home cursor // Form feed -- clear screen and home cursor
ansiTermEraseDisplay(w, 2); ansiTermEraseDisplay(w, 2);
w->as.ansiTerm->cursorRow = 0; w->as.ansiTerm->cursorRow = 0;
w->as.ansiTerm->cursorCol = 0; w->as.ansiTerm->cursorCol = 0;
} else if (ch == '\a') { } else if (ch == '\a') {
// Bell ignored // Bell -- ignored
} else { } else {
// CP437 graphic characters (smileys, card suits, etc.) // CP437 graphic characters (smileys, card suits, etc.)
// and all printable characters // and all printable characters
@ -962,15 +962,15 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
w->as.ansiTerm->csiPrivate = false; w->as.ansiTerm->csiPrivate = false;
memset(w->as.ansiTerm->params, 0, sizeof(w->as.ansiTerm->params)); memset(w->as.ansiTerm->params, 0, sizeof(w->as.ansiTerm->params));
} else if (ch == 'D') { } else if (ch == 'D') {
// IND scroll up one line // IND -- scroll up one line
ansiTermScrollUp(w); ansiTermScrollUp(w);
w->as.ansiTerm->parseState = PARSE_NORMAL; w->as.ansiTerm->parseState = PARSE_NORMAL;
} else if (ch == 'M') { } else if (ch == 'M') {
// RI scroll down one line // RI -- scroll down one line
ansiTermScrollDown(w); ansiTermScrollDown(w);
w->as.ansiTerm->parseState = PARSE_NORMAL; w->as.ansiTerm->parseState = PARSE_NORMAL;
} else if (ch == 'c') { } else if (ch == 'c') {
// RIS terminal reset // RIS -- terminal reset
w->as.ansiTerm->cursorRow = 0; w->as.ansiTerm->cursorRow = 0;
w->as.ansiTerm->cursorCol = 0; w->as.ansiTerm->cursorCol = 0;
w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR;
@ -983,7 +983,7 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
ansiTermEraseDisplay(w, 2); ansiTermEraseDisplay(w, 2);
w->as.ansiTerm->parseState = PARSE_NORMAL; w->as.ansiTerm->parseState = PARSE_NORMAL;
} else { } else {
// Unknown escape return to normal // Unknown escape -- return to normal
w->as.ansiTerm->parseState = PARSE_NORMAL; w->as.ansiTerm->parseState = PARSE_NORMAL;
} }
break; break;
@ -1011,11 +1011,11 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
w->as.ansiTerm->paramCount++; w->as.ansiTerm->paramCount++;
} }
} else if (ch >= 0x40 && ch <= 0x7E) { } else if (ch >= 0x40 && ch <= 0x7E) {
// Final byte dispatch the CSI sequence // Final byte -- dispatch the CSI sequence
ansiTermDispatchCsi(w, ch); ansiTermDispatchCsi(w, ch);
w->as.ansiTerm->parseState = PARSE_NORMAL; w->as.ansiTerm->parseState = PARSE_NORMAL;
} else { } else {
// Unexpected byte abort sequence // Unexpected byte -- abort sequence
w->as.ansiTerm->parseState = PARSE_NORMAL; w->as.ansiTerm->parseState = PARSE_NORMAL;
} }
break; break;
@ -1061,7 +1061,7 @@ static void ansiTermProcessSgr(WidgetT *w) {
w->as.ansiTerm->bold = true; w->as.ansiTerm->bold = true;
fg |= 8; fg |= 8;
} else if (code == 5) { } else if (code == 5) {
// Blink sets bit 7 of attr byte via bg bit 3 // Blink -- sets bit 7 of attr byte via bg bit 3
bg |= 8; bg |= 8;
} else if (code == 25) { } else if (code == 25) {
// Blink off // Blink off
@ -1072,7 +1072,7 @@ static void ansiTermProcessSgr(WidgetT *w) {
fg = bg; fg = bg;
bg = tmp; bg = tmp;
} else if (code == 8) { } else if (code == 8) {
// Invisible foreground same as background // Invisible -- foreground same as background
fg = bg & 0x07; fg = bg & 0x07;
} else if (code == 22) { } else if (code == 22) {
// Normal intensity // Normal intensity
@ -1267,7 +1267,7 @@ static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t
// ============================================================ // ============================================================
// Create a new ANSI terminal widget. The cell buffer is allocated as a // Create a new ANSI terminal widget. The cell buffer is allocated as a
// flat array of (char, attr) pairs rows * cols * 2 bytes. The separate // flat array of (char, attr) pairs -- rows * cols * 2 bytes. The separate
// heap allocation for AnsiTermDataT (via calloc) keeps the WidgetT union // heap allocation for AnsiTermDataT (via calloc) keeps the WidgetT union
// small; terminal state is substantial (~100+ bytes of fields plus the // small; terminal state is substantial (~100+ bytes of fields plus the
// cell and scrollback buffers) so it's pointed to rather than inlined. // cell and scrollback buffers) so it's pointed to rather than inlined.
@ -1434,19 +1434,19 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) {
// Poll the terminal for incoming data and update timers. This should be called // Poll the terminal for incoming data and update timers. This should be called
// from the application's main loop at a reasonable frequency. It handles three // from the application's main loop at a reasonable frequency. It handles three
// things: // things:
// 1. Text blink timer (500ms) toggles visibility of cells with the blink // 1. Text blink timer (500ms) -- toggles visibility of cells with the blink
// attribute. Only rows containing blink cells are dirtied, avoiding // attribute. Only rows containing blink cells are dirtied, avoiding
// unnecessary repaints of static content. // unnecessary repaints of static content.
// 2. Cursor blink timer (250ms) faster than text blink to feel responsive. // 2. Cursor blink timer (250ms) -- faster than text blink to feel responsive.
// Only the cursor's row is dirtied. // Only the cursor's row is dirtied.
// 3. Comm read pulls up to 256 bytes from the transport and feeds them // 3. Comm read -- pulls up to 256 bytes from the transport and feeds them
// through the ANSI parser. The 256-byte chunk size balances between // through the ANSI parser. The 256-byte chunk size balances between
// responsiveness (smaller = more frequent repaints) and throughput // responsiveness (smaller = more frequent repaints) and throughput
// (larger = fewer function call overhead per byte). // (larger = fewer function call overhead per byte).
int32_t wgtAnsiTermPoll(WidgetT *w) { int32_t wgtAnsiTermPoll(WidgetT *w) {
VALIDATE_WIDGET(w, WidgetAnsiTermE, 0); VALIDATE_WIDGET(w, WidgetAnsiTermE, 0);
// Text blink timer toggle visibility and dirty rows with blinking cells // Text blink timer -- toggle visibility and dirty rows with blinking cells
clock_t now = clock(); clock_t now = clock();
clock_t blinkInterval = (clock_t)BLINK_MS * CLOCKS_PER_SEC / 1000; clock_t blinkInterval = (clock_t)BLINK_MS * CLOCKS_PER_SEC / 1000;
clock_t curInterval = (clock_t)CURSOR_MS * CLOCKS_PER_SEC / 1000; clock_t curInterval = (clock_t)CURSOR_MS * CLOCKS_PER_SEC / 1000;
@ -1509,15 +1509,15 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
// for the serial link. // for the serial link.
// Fast repaint: renders dirty rows directly into the window's content buffer, // Fast repaint: renders dirty rows directly into the window's content buffer,
// completely bypassing the normal widget paint pipeline (widgetOnPaint // completely bypassing the normal widget paint pipeline (widgetOnPaint ->
// widgetPaintOne full tree walk). Returns the number of rows repainted // widgetPaintOne -> full tree walk). Returns the number of rows repainted
// and optionally reports the vertical extent via outY/outH so the caller // and optionally reports the vertical extent via outY/outH so the caller
// can issue a minimal compositor dirty rect. // can issue a minimal compositor dirty rect.
// //
// This exists because the normal paint path clears the entire content area // This exists because the normal paint path clears the entire content area
// and repaints all widgets, which is far too expensive to do on every // and repaints all widgets, which is far too expensive to do on every
// incoming serial byte. With fast repaint, the path from data reception // incoming serial byte. With fast repaint, the path from data reception
// to LFB flush is: poll → write → dirtyRows → repaint → compositor dirty, // to LFB flush is: poll -> write -> dirtyRows -> repaint -> compositor dirty,
// keeping the round-trip under 1ms on a Pentium. // keeping the round-trip under 1ms on a Pentium.
int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
if (!w || w->type != WidgetAnsiTermE || !w->window) { if (!w || w->type != WidgetAnsiTermE || !w->window) {
@ -1626,7 +1626,7 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
// ============================================================ // ============================================================
// Attach communication callbacks to the terminal. The read/write function // Attach communication callbacks to the terminal. The read/write function
// pointers are transport-agnostic the terminal doesn't care whether // pointers are transport-agnostic -- the terminal doesn't care whether
// data comes from a raw UART, the secLink encrypted channel, or a // data comes from a raw UART, the secLink encrypted channel, or a
// socket-based proxy. The ctx pointer is passed through opaquely. // socket-based proxy. The ctx pointer is passed through opaquely.
void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) {
@ -1645,7 +1645,7 @@ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t
// Write raw bytes into the terminal for parsing and display. Any active // Write raw bytes into the terminal for parsing and display. Any active
// selection is cleared first since the screen content is changing and the // selection is cleared first since the screen content is changing and the
// selection coordinates would become stale. Each byte is fed through the // selection coordinates would become stale. Each byte is fed through the
// ANSI parser individually the parser maintains state between calls so // ANSI parser individually -- the parser maintains state between calls so
// multi-byte sequences split across writes are handled correctly. // multi-byte sequences split across writes are handled correctly.
void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) {
if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) { if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) {
@ -1677,7 +1677,7 @@ void widgetAnsiTermDestroy(WidgetT *w) {
// ============================================================ // ============================================================
// Min size = exact pixel dimensions needed for the grid plus border and // Min size = exact pixel dimensions needed for the grid plus border and
// scrollbar. The terminal is not designed to be resizable the grid // scrollbar. The terminal is not designed to be resizable -- the grid
// dimensions (cols x rows) are fixed at creation time, matching the BBS // dimensions (cols x rows) are fixed at creation time, matching the BBS
// convention of 80x25 or similar fixed screen sizes. // convention of 80x25 or similar fixed screen sizes.
void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) {
@ -1763,7 +1763,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// No selection fall through to send ^C to terminal // No selection -- fall through to send ^C to terminal
} }
// Ctrl+V: paste from clipboard to terminal // Ctrl+V: paste from clipboard to terminal
@ -1806,39 +1806,39 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
buf[0] = 0x08; buf[0] = 0x08;
len = 1; len = 1;
} else if (key == (0x48 | 0x100)) { } else if (key == (0x48 | 0x100)) {
// Up arrow ESC[A // Up arrow -> ESC[A
buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A';
len = 3; len = 3;
} else if (key == (0x50 | 0x100)) { } else if (key == (0x50 | 0x100)) {
// Down arrow ESC[B // Down arrow -> ESC[B
buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B';
len = 3; len = 3;
} else if (key == (0x4D | 0x100)) { } else if (key == (0x4D | 0x100)) {
// Right arrow ESC[C // Right arrow -> ESC[C
buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C';
len = 3; len = 3;
} else if (key == (0x4B | 0x100)) { } else if (key == (0x4B | 0x100)) {
// Left arrow ESC[D // Left arrow -> ESC[D
buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D';
len = 3; len = 3;
} else if (key == (0x47 | 0x100)) { } else if (key == (0x47 | 0x100)) {
// Home ESC[H // Home -> ESC[H
buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H';
len = 3; len = 3;
} else if (key == (0x4F | 0x100)) { } else if (key == (0x4F | 0x100)) {
// End ESC[F // End -> ESC[F
buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F';
len = 3; len = 3;
} else if (key == (0x49 | 0x100)) { } else if (key == (0x49 | 0x100)) {
// PgUp ESC[5~ // PgUp -> ESC[5~
buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~';
len = 4; len = 4;
} else if (key == (0x51 | 0x100)) { } else if (key == (0x51 | 0x100)) {
// PgDn ESC[6~ // PgDn -> ESC[6~
buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~';
len = 4; len = 4;
} else if (key == (0x53 | 0x100)) { } else if (key == (0x53 | 0x100)) {
// Delete ESC[3~ // Delete -> ESC[3~
buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~';
len = 4; len = 4;
} else if (key >= 1 && key < 32) { } else if (key >= 1 && key < 32) {
@ -1866,7 +1866,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
// The scrollbar area uses direct hit testing on up/down arrow buttons and // The scrollbar area uses direct hit testing on up/down arrow buttons and
// page-up/page-down regions, with proportional thumb positioning. // page-up/page-down regions, with proportional thumb positioning.
// Selection drag is handled externally by the widget event dispatcher via // Selection drag is handled externally by the widget event dispatcher via
// sDragTextSelect on mouse-down we set the anchor and enable drag mode. // sDragTextSelect -- on mouse-down we set the anchor and enable drag mode.
void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
AppContextT *actx = (AppContextT *)root->userData; AppContextT *actx = (AppContextT *)root->userData;
const BitmapFontT *font = &actx->font; const BitmapFontT *font = &actx->font;
@ -1880,7 +1880,7 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
int32_t sbH = rows * font->charHeight; int32_t sbH = rows * font->charHeight;
int32_t arrowH = ANSI_SB_W; int32_t arrowH = ANSI_SB_W;
// Click in text area start selection // Click in text area -- start selection
if (vx < sbX) { if (vx < sbX) {
int32_t baseX = hit->x + ANSI_BORDER; int32_t baseX = hit->x + ANSI_BORDER;
int32_t baseY = hit->y + ANSI_BORDER; int32_t baseY = hit->y + ANSI_BORDER;
@ -1964,7 +1964,7 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
// Down arrow // Down arrow
hit->as.ansiTerm->scrollPos++; hit->as.ansiTerm->scrollPos++;
} else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { } else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) {
// Track area compute thumb position to determine page direction // Track area -- compute thumb position to determine page direction
int32_t trackY = sbY + arrowH; int32_t trackY = sbY + arrowH;
int32_t trackH = sbH - arrowH * 2; int32_t trackH = sbH - arrowH * 2;
@ -2014,12 +2014,12 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
// rows, selection overlay, and scrollbar. This is called through the normal // rows, selection overlay, and scrollbar. This is called through the normal
// widget paint pipeline (e.g., on window expose or full invalidation). // widget paint pipeline (e.g., on window expose or full invalidation).
// For incremental updates during data reception, wgtAnsiTermRepaint is used // For incremental updates during data reception, wgtAnsiTermRepaint is used
// instead it's much faster since it only repaints dirty rows and skips // instead -- it's much faster since it only repaints dirty rows and skips
// the border/scrollbar. // the border/scrollbar.
// //
// The terminal renders its own scrollbar rather than using the shared // The terminal renders its own scrollbar rather than using the shared
// widgetDrawScrollbarV because the scrollbar's total/visible/position // widgetDrawScrollbarV because the scrollbar's total/visible/position
// semantics are different the terminal scrollbar represents scrollback // semantics are different -- the terminal scrollbar represents scrollback
// lines (historical content above the screen), not a viewport over a // lines (historical content above the screen), not a viewport over a
// virtual content area. // virtual content area.
void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
@ -2082,7 +2082,7 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t arrowH = ANSI_SB_W; int32_t arrowH = ANSI_SB_W;
if (sbCount == 0) { if (sbCount == 0) {
// No scrollback fill scrollbar area with trough color // No scrollback -- fill scrollbar area with trough color
rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough); rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough);
return; return;
} }

View file

@ -1,21 +1,21 @@
// widgetBox.c VBox, HBox, and Frame container widgets // widgetBox.c -- VBox, HBox, and Frame container widgets
// //
// VBox and HBox are the primary layout containers. They have no visual // VBox and HBox are the primary layout containers. They have no visual
// representation of their own they exist purely to arrange children // representation of their own -- they exist purely to arrange children
// vertically or horizontally. The actual layout algorithm lives in // vertically or horizontally. The actual layout algorithm lives in
// widgetLayout.c (widgetCalcMinSizeBox / widgetLayoutBox) which handles // widgetLayout.c (widgetCalcMinSizeBox / widgetLayoutBox) which handles
// weight-based space distribution, spacing, padding, and alignment. // weight-based space distribution, spacing, padding, and alignment.
// //
// VBox and HBox are distinguished by a flag (WCLASS_HORIZ_CONTAINER) in // VBox and HBox are distinguished by a flag (WCLASS_HORIZ_CONTAINER) in
// the class table rather than having separate code. This keeps the layout // the class table rather than having separate code. This keeps the layout
// engine unified the same algorithm works in both orientations by // engine unified -- the same algorithm works in both orientations by
// swapping which axis is "major" vs "minor". // swapping which axis is "major" vs "minor".
// //
// Frame is a labeled grouping box with a Motif-style beveled border. // Frame is a labeled grouping box with a Motif-style beveled border.
// It acts as a VBox for layout purposes (children stack vertically inside // It acts as a VBox for layout purposes (children stack vertically inside
// the frame's padded interior). The title text sits centered vertically // the frame's padded interior). The title text sits centered vertically
// on the top border line, with a small background-filled gap to visually // on the top border line, with a small background-filled gap to visually
// "break" the border behind the title this is the classic Win3.1/Motif // "break" the border behind the title -- this is the classic Win3.1/Motif
// group box appearance. // group box appearance.
#include "widgetInternal.h" #include "widgetInternal.h"
@ -27,14 +27,14 @@
// Paint the frame border and optional title. The border is offset down by // Paint the frame border and optional title. The border is offset down by
// half the font height so the title text can sit centered on the top edge. // half the font height so the title text can sit centered on the top edge.
// This creates the illusion of the title "interrupting" the border a // This creates the illusion of the title "interrupting" the border -- a
// background-colored rectangle is drawn behind the title to erase the // background-colored rectangle is drawn behind the title to erase the
// border pixels, then the title is drawn on top. // border pixels, then the title is drawn on top.
// //
// Three border styles are supported: // Three border styles are supported:
// FrameFlatE single-pixel solid color outline // FrameFlatE -- single-pixel solid color outline
// FrameInE Motif "groove" (inset bevel: shadow-then-highlight) // FrameInE -- Motif "groove" (inset bevel: shadow-then-highlight)
// FrameOutE Motif "ridge" (outset bevel: highlight-then-shadow) // FrameOutE -- Motif "ridge" (outset bevel: highlight-then-shadow)
// The groove/ridge are each two nested 1px bevels with swapped colors. // The groove/ridge are each two nested 1px bevels with swapped colors.
void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t fb = widgetFrameBorderWidth(w); int32_t fb = widgetFrameBorderWidth(w);
@ -100,7 +100,7 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
// Create a Frame container. The title string supports accelerator keys // Create a Frame container. The title string supports accelerator keys
// (prefixed with '&') which are parsed by accelParse. The title pointer // (prefixed with '&') which are parsed by accelParse. The title pointer
// is stored directly (not copied) the caller must ensure it remains valid. // is stored directly (not copied) -- the caller must ensure it remains valid.
WidgetT *wgtFrame(WidgetT *parent, const char *title) { WidgetT *wgtFrame(WidgetT *parent, const char *title) {
WidgetT *w = widgetAlloc(parent, WidgetFrameE); WidgetT *w = widgetAlloc(parent, WidgetFrameE);

View file

@ -1,9 +1,9 @@
// widgetButton.c Button widget // widgetButton.c -- Button widget
// //
// Standard push button with text label, Motif-style 2px beveled border, // Standard push button with text label, Motif-style 2px beveled border,
// and press animation. The button uses a two-phase press model: // and press animation. The button uses a two-phase press model:
// - Mouse press: sets pressed=true and stores the widget in sPressedButton. // - Mouse press: sets pressed=true and stores the widget in sPressedButton.
// The event dispatcher tracks mouse movement if the mouse leaves the // The event dispatcher tracks mouse movement -- if the mouse leaves the
// button bounds, pressed is cleared (visual feedback), and if it re-enters, // button bounds, pressed is cleared (visual feedback), and if it re-enters,
// pressed is re-set. The onClick callback fires only on mouse-up while // pressed is re-set. The onClick callback fires only on mouse-up while
// still inside the button. This gives the user a chance to cancel by // still inside the button. This gives the user a chance to cancel by
@ -105,10 +105,10 @@ void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// ============================================================ // ============================================================
// Paint: draws the beveled border, centered text, and optional focus rect. // Paint: draws the beveled border, centered text, and optional focus rect.
// When pressed, the bevel colors swap (highlightshadow) creating the sunken // When pressed, the bevel colors swap (highlight<->shadow) creating the sunken
// appearance, and the text shifts by BUTTON_PRESS_OFFSET pixels. Disabled // appearance, and the text shifts by BUTTON_PRESS_OFFSET pixels. Disabled
// buttons use the "embossed" text technique (highlight color at +1,+1, then // buttons use the "embossed" text technique (highlight color at +1,+1, then
// shadow color at 0,0) to create a chiseled/etched look this is the // shadow color at 0,0) to create a chiseled/etched look -- this is the
// standard Windows 3.1 disabled control appearance. // standard Windows 3.1 disabled control appearance.
void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;

View file

@ -1,9 +1,9 @@
// widgetCanvas.c Drawable canvas widget (freehand draw, PNG save/load) // widgetCanvas.c -- Drawable canvas widget (freehand draw, PNG save/load)
// //
// The canvas widget provides a pixel buffer in the display's native pixel // The canvas widget provides a pixel buffer in the display's native pixel
// format that applications can draw into directly. It stores pixels in // format that applications can draw into directly. It stores pixels in
// display format (not always RGB) to avoid per-pixel conversion on every // display format (not always RGB) to avoid per-pixel conversion on every
// repaint the paint function just does a straight rectCopy blit from // repaint -- the paint function just does a straight rectCopy blit from
// the canvas buffer to the display. This is critical on a 486 where // the canvas buffer to the display. This is critical on a 486 where
// per-pixel format conversion during repaint would be prohibitively slow. // per-pixel format conversion during repaint would be prohibitively slow.
// //
@ -17,7 +17,7 @@
// Bresenham's algorithm placing dots along the path. This gives smooth // Bresenham's algorithm placing dots along the path. This gives smooth
// freehand drawing with variable pen widths. // freehand drawing with variable pen widths.
// //
// Canvas coordinates are independent of widget position (0,0) is the // Canvas coordinates are independent of widget position -- (0,0) is the
// top-left of the canvas content, not the widget. Mouse events translate // top-left of the canvas content, not the widget. Mouse events translate
// widget-space coordinates to canvas-space by subtracting the border offset. // widget-space coordinates to canvas-space by subtracting the border offset.
@ -41,7 +41,7 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32
// Read/write a single pixel at the given address, respecting the display's // Read/write a single pixel at the given address, respecting the display's
// bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor). // bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor).
// These are inline because they're called per-pixel in tight loops (circle fill, // These are inline because they're called per-pixel in tight loops (circle fill,
// line draw) the function call overhead would dominate on a 486. The bpp // line draw) -- the function call overhead would dominate on a 486. The bpp
// branch is predictable since it doesn't change within a single draw operation. // branch is predictable since it doesn't change within a single draw operation.
static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) { static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) {
@ -96,7 +96,7 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
// center), compute the horizontal extent using the circle equation // center), compute the horizontal extent using the circle equation
// dx^2 + dy^2 <= r^2. The horizontal half-span is floor(sqrt(r^2 - dy^2)). // dx^2 + dy^2 <= r^2. The horizontal half-span is floor(sqrt(r^2 - dy^2)).
// This approach is faster than checking each pixel individually because // This approach is faster than checking each pixel individually because
// the inner loop just fills a horizontal run no per-pixel distance check. // the inner loop just fills a horizontal run -- no per-pixel distance check.
int32_t r2 = rad * rad; int32_t r2 = rad * rad;
for (int32_t dy = -rad; dy <= rad; dy++) { for (int32_t dy = -rad; dy <= rad; dy++) {
@ -106,7 +106,7 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
continue; continue;
} }
// Compute horizontal half-span: dx² <= r² - dy² // Compute horizontal half-span: dx^2 <= r^2 - dy^2
int32_t dy2 = dy * dy; int32_t dy2 = dy * dy;
int32_t rem = r2 - dy2; int32_t rem = r2 - dy2;
int32_t hspan = 0; int32_t hspan = 0;
@ -158,7 +158,7 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
// Bresenham line from (x0,y0) to (x1,y1), placing dots along the path. // Bresenham line from (x0,y0) to (x1,y1), placing dots along the path.
// Each point on the line gets a full pen dot (canvasDrawDot), which means // Each point on the line gets a full pen dot (canvasDrawDot), which means
// lines with large pen sizes are smooth rather than aliased. Bresenham was // lines with large pen sizes are smooth rather than aliased. Bresenham was
// chosen over DDA because it's pure integer arithmetic no floating point // chosen over DDA because it's pure integer arithmetic -- no floating point
// needed, which matters on 486SX (no FPU). // needed, which matters on 486SX (no FPU).
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
@ -202,7 +202,7 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32
// is allocated in the display's native pixel format by walking up the widget // is allocated in the display's native pixel format by walking up the widget
// tree to find the AppContextT (which holds the display format info). This // tree to find the AppContextT (which holds the display format info). This
// tree-walk pattern is necessary because the widget doesn't have direct access // tree-walk pattern is necessary because the widget doesn't have direct access
// to the display only the root widget's userData points to the AppContextT. // to the display -- only the root widget's userData points to the AppContextT.
// The buffer is initialized to white using spanFill for performance. // The buffer is initialized to white using spanFill for performance.
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
if (!parent || w <= 0 || h <= 0) { if (!parent || w <= 0 || h <= 0) {
@ -401,7 +401,7 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) {
continue; continue;
} }
// Compute horizontal half-span: dx² <= r² - dy² // Compute horizontal half-span: dx^2 <= r^2 - dy^2
int32_t dy2 = dy * dy; int32_t dy2 = dy * dy;
int32_t rem = r2 - dy2; int32_t rem = r2 - dy2;
int32_t hspan = 0; int32_t hspan = 0;
@ -526,7 +526,7 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) {
// Load an image file into the canvas, replacing the current content. // Load an image file into the canvas, replacing the current content.
// Delegates to dvxLoadImage for format decoding and pixel conversion. // Delegates to dvxLoadImage for format decoding and pixel conversion.
// The old buffer is freed and replaced with the new one canvas // The old buffer is freed and replaced with the new one -- canvas
// dimensions change to match the loaded image. // dimensions change to match the loaded image.
int32_t wgtCanvasLoad(WidgetT *w, const char *path) { int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
if (!w || w->type != WidgetCanvasE || !path) { if (!w || w->type != WidgetCanvasE || !path) {
@ -648,7 +648,7 @@ void widgetCanvasDestroy(WidgetT *w) {
// The canvas requests exactly its pixel dimensions plus the sunken bevel // The canvas requests exactly its pixel dimensions plus the sunken bevel
// border. The font parameter is unused since the canvas has no text content. // border. The font parameter is unused since the canvas has no text content.
// The canvas is not designed to scale it reports its exact size as the // The canvas is not designed to scale -- it reports its exact size as the
// minimum, and the layout engine should respect that. // minimum, and the layout engine should respect that.
void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) { void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font; (void)font;
@ -697,7 +697,7 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
// Paint: draws a sunken bevel border then blits the canvas buffer. Because // Paint: draws a sunken bevel border then blits the canvas buffer. Because
// the canvas stores pixels in the display's native format, rectCopy is a // the canvas stores pixels in the display's native format, rectCopy is a
// straight memcpy per scanline no per-pixel conversion needed. This makes // straight memcpy per scanline -- no per-pixel conversion needed. This makes
// repaint essentially free relative to the display bandwidth. // repaint essentially free relative to the display bandwidth.
void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font; (void)font;

View file

@ -1,8 +1,8 @@
// widgetCheckbox.c Checkbox widget // widgetCheckbox.c -- Checkbox widget
// //
// Classic checkbox: a small box with a sunken bevel (1px) on the left, a text // Classic checkbox: a small box with a sunken bevel (1px) on the left, a text
// label to the right. The check mark is drawn as two diagonal lines forming an // label to the right. The check mark is drawn as two diagonal lines forming an
// "X" pattern rather than a traditional checkmark glyph this is simpler to // "X" pattern rather than a traditional checkmark glyph -- this is simpler to
// render with drawHLine primitives and matches the DV/X aesthetic. // render with drawHLine primitives and matches the DV/X aesthetic.
// //
// State management is simple: a boolean 'checked' flag toggles on each click // State management is simple: a boolean 'checked' flag toggles on each click

View file

@ -1,13 +1,13 @@
// widgetClass.c Widget class vtable definitions // widgetClass.c -- Widget class vtable definitions
// //
// This file implements a C vtable pattern for the widget type system. // This file implements a C vtable pattern for the widget type system.
// Each widget type has a static const WidgetClassT struct that defines // Each widget type has a static const WidgetClassT struct that defines
// its behavior paint, layout, mouse handling, keyboard handling, etc. // its behavior -- paint, layout, mouse handling, keyboard handling, etc.
// A master table (widgetClassTable[]) maps WidgetTypeE enum values to // A master table (widgetClassTable[]) maps WidgetTypeE enum values to
// their class definitions, enabling O(1) dispatch. // their class definitions, enabling O(1) dispatch.
// //
// Why a vtable approach instead of switch statements: // Why a vtable approach instead of switch statements:
// 1. Adding a new widget type is purely additive define a new class // 1. Adding a new widget type is purely additive -- define a new class
// struct and add one entry to the table. No existing switch // struct and add one entry to the table. No existing switch
// statements need modification, reducing the risk of forgetting // statements need modification, reducing the risk of forgetting
// a case. // a case.
@ -20,13 +20,13 @@
// like hit testing and layout. // like hit testing and layout.
// //
// Class flags: // Class flags:
// WCLASS_FOCUSABLE widget can receive keyboard focus (Tab order) // WCLASS_FOCUSABLE -- widget can receive keyboard focus (Tab order)
// WCLASS_BOX_CONTAINER uses the generic box layout engine (VBox/HBox) // WCLASS_BOX_CONTAINER -- uses the generic box layout engine (VBox/HBox)
// WCLASS_HORIZ_CONTAINER lays out children horizontally (HBox variant) // WCLASS_HORIZ_CONTAINER -- lays out children horizontally (HBox variant)
// WCLASS_PAINTS_CHILDREN widget handles its own child painting (TabControl, // WCLASS_PAINTS_CHILDREN -- widget handles its own child painting (TabControl,
// TreeView, ScrollPane, Splitter). The default paint // TreeView, ScrollPane, Splitter). The default paint
// walker skips children for these widgets. // walker skips children for these widgets.
// WCLASS_NO_HIT_RECURSE hit testing stops at this widget instead of // WCLASS_NO_HIT_RECURSE -- hit testing stops at this widget instead of
// recursing into children. Used by widgets that // recursing into children. Used by widgets that
// manage their own internal click regions (TreeView, // manage their own internal click regions (TreeView,
// ScrollPane, ListView, Splitter). // ScrollPane, ListView, Splitter).
@ -328,9 +328,9 @@ static const WidgetClassT sClassToolbar = {
}; };
// TreeView uses all three special flags: // TreeView uses all three special flags:
// PAINTS_CHILDREN it renders tree items itself (with indentation, // PAINTS_CHILDREN -- it renders tree items itself (with indentation,
// expand/collapse buttons, and selection highlighting) // expand/collapse buttons, and selection highlighting)
// NO_HIT_RECURSE mouse clicks go to the TreeView widget, which // NO_HIT_RECURSE -- mouse clicks go to the TreeView widget, which
// figures out which tree item was clicked based on scroll position // figures out which tree item was clicked based on scroll position
// and item Y coordinates, rather than letting the hit tester // and item Y coordinates, rather than letting the hit tester
// recurse into child TreeItem widgets // recurse into child TreeItem widgets
@ -347,7 +347,7 @@ static const WidgetClassT sClassTreeView = {
.setText = NULL .setText = NULL
}; };
// TreeItem has no paint/mouse/key handlers it's a data-only node. // TreeItem has no paint/mouse/key handlers -- it's a data-only node.
// The TreeView parent widget handles all rendering and interaction. // The TreeView parent widget handles all rendering and interaction.
// TreeItem exists as a WidgetT so it can participate in the tree // TreeItem exists as a WidgetT so it can participate in the tree
// structure (parent/child/sibling links) for hierarchical data. // structure (parent/child/sibling links) for hierarchical data.
@ -485,14 +485,14 @@ static const WidgetClassT sClassSpinner = {
}; };
// ============================================================ // ============================================================
// Class table indexed by WidgetTypeE // Class table -- indexed by WidgetTypeE
// ============================================================ // ============================================================
// //
// This array is the central dispatch table for the widget system. // This array is the central dispatch table for the widget system.
// Indexed by the WidgetTypeE enum, it provides O(1) lookup of any // Indexed by the WidgetTypeE enum, it provides O(1) lookup of any
// widget type's class definition. Every WidgetT stores a pointer // widget type's class definition. Every WidgetT stores a pointer
// to its class (w->wclass) set at allocation time, so per-widget // to its class (w->wclass) set at allocation time, so per-widget
// dispatch doesn't even need to index this table it's a direct // dispatch doesn't even need to index this table -- it's a direct
// pointer dereference through the vtable. // pointer dereference through the vtable.
// //
// Using C99 designated initializers ensures the array slots match // Using C99 designated initializers ensures the array slots match

View file

@ -1,4 +1,4 @@
// widgetComboBox.c ComboBox widget (editable text + dropdown list) // widgetComboBox.c -- ComboBox widget (editable text + dropdown list)
// //
// Combines a single-line text input with a dropdown list. The text area // Combines a single-line text input with a dropdown list. The text area
// supports full editing (cursor movement, selection, undo, clipboard) via // supports full editing (cursor movement, selection, undo, clipboard) via
@ -12,7 +12,7 @@
// //
// The popup list is painted as an overlay (widgetComboBoxPaintPopup) that // The popup list is painted as an overlay (widgetComboBoxPaintPopup) that
// renders on top of all other widgets. Popup visibility is coordinated // renders on top of all other widgets. Popup visibility is coordinated
// through the sOpenPopup global only one popup can be open at a time. // through the sOpenPopup global -- only one popup can be open at a time.
// The sClosedPopup mechanism prevents click-to-close from immediately // The sClosedPopup mechanism prevents click-to-close from immediately
// reopening the popup when the close click lands on the dropdown button. // reopening the popup when the close click lands on the dropdown button.
// //
@ -83,7 +83,7 @@ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) {
// Cache max item string length so calcMinSize doesn't need to re-scan // Cache max item string length so calcMinSize doesn't need to re-scan
// the entire item array on every layout pass. Items are stored as // the entire item array on every layout pass. Items are stored as
// external pointers (not copied) the caller owns the string data. // external pointers (not copied) -- the caller owns the string data.
int32_t maxLen = 0; int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
@ -282,12 +282,12 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
return; return;
} }
// Button click toggle popup // Button click -- toggle popup
w->as.comboBox.open = !w->as.comboBox.open; w->as.comboBox.open = !w->as.comboBox.open;
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx; w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
sOpenPopup = w->as.comboBox.open ? w : NULL; sOpenPopup = w->as.comboBox.open ? w : NULL;
} else { } else {
// Text area click focus for editing // Text area click -- focus for editing
clearOtherSelections(w); clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData; AppContextT *ctx = (AppContextT *)root->userData;
@ -300,7 +300,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// widgetComboBoxPaint // widgetComboBoxPaint
// ============================================================ // ============================================================
// Paint: two regions side-by-side a sunken text area (left) and a raised // Paint: two regions side-by-side -- a sunken text area (left) and a raised
// dropdown button (right). The text area renders the edit buffer with optional // dropdown button (right). The text area renders the edit buffer with optional
// selection highlighting (up to 3 text runs: pre-selection, selection, // selection highlighting (up to 3 text runs: pre-selection, selection,
// post-selection). The dropdown button has a small triangular arrow glyph // post-selection). The dropdown button has a small triangular arrow glyph

View file

@ -1,4 +1,4 @@
// widgetCore.c Core widget infrastructure (alloc, tree ops, helpers) // widgetCore.c -- Core widget infrastructure (alloc, tree ops, helpers)
// //
// This file provides the foundation for the widget tree: allocation, // This file provides the foundation for the widget tree: allocation,
// parent-child linking, focus management, hit testing, and shared // parent-child linking, focus management, hit testing, and shared
@ -30,7 +30,7 @@
// //
// Each pointer is set when an interaction begins (e.g. mouse-down on a // Each pointer is set when an interaction begins (e.g. mouse-down on a
// slider) and cleared when it ends (mouse-up). The event dispatcher in // slider) and cleared when it ends (mouse-up). The event dispatcher in
// widgetEvent.c checks these before normal hit testing active drags // widgetEvent.c checks these before normal hit testing -- active drags
// take priority over everything else. // take priority over everything else.
// //
// All of these must be NULLed when the pointed-to widget is destroyed, // All of these must be NULLed when the pointed-to widget is destroyed,
@ -90,7 +90,7 @@ void widgetAddChild(WidgetT *parent, WidgetT *child) {
// class vtable via widgetClassTable[], and optionally adds it as // class vtable via widgetClassTable[], and optionally adds it as
// a child of the given parent. // a child of the given parent.
// //
// The memset to 0 is intentional it establishes sane defaults // The memset to 0 is intentional -- it establishes sane defaults
// for all fields: NULL pointers, zero coordinates, no focus, // for all fields: NULL pointers, zero coordinates, no focus,
// no accel key, etc. Only visible and enabled default to true. // no accel key, etc. Only visible and enabled default to true.
// //
@ -282,7 +282,7 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten
// Special case for TabPage widgets: even if the tab page itself is // Special case for TabPage widgets: even if the tab page itself is
// not visible (inactive tab), its accelKey is still checked. This // not visible (inactive tab), its accelKey is still checked. This
// allows Alt+key to switch to a different tab. However, children // allows Alt+key to switch to a different tab. However, children
// of invisible tab pages are NOT searched their accelerators // of invisible tab pages are NOT searched -- their accelerators
// should not be active when the tab is hidden. // should not be active when the tab is hidden.
WidgetT *widgetFindByAccel(WidgetT *root, char key) { WidgetT *widgetFindByAccel(WidgetT *root, char key) {
@ -323,12 +323,12 @@ WidgetT *widgetFindByAccel(WidgetT *root, char key) {
// Implements Tab-order navigation: finds the next focusable widget // Implements Tab-order navigation: finds the next focusable widget
// after 'after' in depth-first tree order. The two-pass approach // after 'after' in depth-first tree order. The two-pass approach
// (search from 'after' to end, then wrap to start) ensures circular // (search from 'after' to end, then wrap to start) ensures circular
// tabbing Tab on the last focusable widget wraps to the first. // tabbing -- Tab on the last focusable widget wraps to the first.
// //
// The pastAfter flag tracks whether we've passed the 'after' widget // The pastAfter flag tracks whether we've passed the 'after' widget
// during traversal. Once past it, the next focusable widget is the // during traversal. Once past it, the next focusable widget is the
// answer. This avoids collecting all focusable widgets into an array // answer. This avoids collecting all focusable widgets into an array
// just to find the next one the common case returns quickly. // just to find the next one -- the common case returns quickly.
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) { static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
if (!w->visible || !w->enabled) { if (!w->visible || !w->enabled) {
@ -364,7 +364,7 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
return found; return found;
} }
// Wrap around search from the beginning // Wrap around -- search from the beginning
pastAfter = true; pastAfter = true;
return findNextFocusableImpl(root, NULL, &pastAfter); return findNextFocusableImpl(root, NULL, &pastAfter);
} }
@ -476,7 +476,7 @@ int32_t widgetFrameBorderWidth(const WidgetT *w) {
// on top of earlier ones). This is important for overlapping widgets, // on top of earlier ones). This is important for overlapping widgets,
// though in practice the layout engine rarely produces overlap. // though in practice the layout engine rarely produces overlap.
// //
// Widgets with WCLASS_NO_HIT_RECURSE stop the recursion the parent // Widgets with WCLASS_NO_HIT_RECURSE stop the recursion -- the parent
// widget handles all mouse events for its children. This is used by // widget handles all mouse events for its children. This is used by
// TreeView, ScrollPane, ListView, and Splitter, which need to manage // TreeView, ScrollPane, ListView, and Splitter, which need to manage
// their own internal regions (scrollbars, column headers, tree // their own internal regions (scrollbars, column headers, tree
@ -496,7 +496,7 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
return w; return w;
} }
// Check children take the last match (topmost in Z-order) // Check children -- take the last match (topmost in Z-order)
WidgetT *hit = NULL; WidgetT *hit = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
@ -551,7 +551,7 @@ bool widgetIsHorizContainer(WidgetTypeE type) {
// so each widget doesn't have to reimplement index clamping. // so each widget doesn't have to reimplement index clamping.
// //
// Key values use the 0x100 flag to mark extended scan codes (arrow // Key values use the 0x100 flag to mark extended scan codes (arrow
// keys, Home, End, etc.) this is the DVX convention for passing // keys, Home, End, etc.) -- this is the DVX convention for passing
// scan codes through the same int32_t channel as ASCII values. // scan codes through the same int32_t channel as ASCII values.
// //
// Returns -1 for unrecognized keys so callers can check whether the // Returns -1 for unrecognized keys so callers can check whether the
@ -648,7 +648,7 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f
// Used by both the WM-level scrollbars and widget-internal scrollbars // Used by both the WM-level scrollbars and widget-internal scrollbars
// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior. // (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior.
// //
// The thumb size is proportional to visibleSize/totalSize a larger // The thumb size is proportional to visibleSize/totalSize -- a larger
// visible area means a larger thumb, giving visual feedback about how // visible area means a larger thumb, giving visual feedback about how
// much content is scrollable. SB_MIN_THUMB prevents the thumb from // much content is scrollable. SB_MIN_THUMB prevents the thumb from
// becoming too small to grab with a mouse. // becoming too small to grab with a mouse.

View file

@ -1,7 +1,7 @@
// widgetDropdown.c Dropdown (select) widget // widgetDropdown.c -- Dropdown (select) widget
// //
// A non-editable dropdown list (HTML <select> equivalent). Unlike ComboBox, // A non-editable dropdown list (HTML <select> equivalent). Unlike ComboBox,
// the user cannot type a custom value they can only choose from the // the user cannot type a custom value -- they can only choose from the
// predefined items. This simplifies the widget considerably: no text buffer, // predefined items. This simplifies the widget considerably: no text buffer,
// no undo, no cursor, no text editing key handling. // no undo, no cursor, no text editing key handling.
// //
@ -135,7 +135,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod; (void)mod;
if (w->as.dropdown.open) { if (w->as.dropdown.open) {
// Popup is open navigate items // Popup is open -- navigate items
if (key == (0x48 | 0x100)) { if (key == (0x48 | 0x100)) {
if (w->as.dropdown.hoverIdx > 0) { if (w->as.dropdown.hoverIdx > 0) {
w->as.dropdown.hoverIdx--; w->as.dropdown.hoverIdx--;
@ -254,7 +254,7 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
btnBevel.width = 2; btnBevel.width = 2;
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel); drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
// Down arrow glyph in button a small filled triangle drawn as horizontal // Down arrow glyph in button -- a small filled triangle drawn as horizontal
// lines of decreasing width (7, 5, 3, 1 pixels). This creates a 4-pixel // lines of decreasing width (7, 5, 3, 1 pixels). This creates a 4-pixel
// tall downward-pointing triangle centered in the button. // tall downward-pointing triangle centered in the button.
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow; uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;

View file

@ -1,4 +1,4 @@
// widgetEvent.c Window event handlers for widget system // widgetEvent.c -- Window event handlers for widget system
// //
// This file routes window-level events (mouse, keyboard, paint, resize, // This file routes window-level events (mouse, keyboard, paint, resize,
// scroll) into the widget tree. It serves as the bridge between the // scroll) into the widget tree. It serves as the bridge between the
@ -19,7 +19,7 @@
#include "widgetInternal.h" #include "widgetInternal.h"
// Widget whose popup was just closed by click-outside prevents // Widget whose popup was just closed by click-outside -- prevents
// immediate re-open on the same click. Without this, clicking the // immediate re-open on the same click. Without this, clicking the
// dropdown button to close its popup would immediately hit-test the // dropdown button to close its popup would immediately hit-test the
// button again and re-open the popup in the same event. // button again and re-open the popup in the same event.
@ -148,7 +148,7 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
// The cached pointer avoids an O(n) tree walk to find the focused // The cached pointer avoids an O(n) tree walk to find the focused
// widget on every keypress. // widget on every keypress.
// //
// There is no keyboard event bubbling if the focused widget doesn't // There is no keyboard event bubbling -- if the focused widget doesn't
// handle a key, it's simply dropped. Accelerators (Alt+key) are // handle a key, it's simply dropped. Accelerators (Alt+key) are
// handled at a higher level in the app event loop, not here. // handled at a higher level in the app event loop, not here.
@ -159,7 +159,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return; return;
} }
// Use cached focus pointer O(1) instead of O(n) tree walk // Use cached focus pointer -- O(1) instead of O(n) tree walk
WidgetT *focus = sFocusedWidget; WidgetT *focus = sFocusedWidget;
if (!focus || !focus->focused || focus->window != win) { if (!focus || !focus->focused || focus->window != win) {
@ -191,7 +191,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
// Only if no interaction is active does the event fall through to // Only if no interaction is active does the event fall through to
// normal hit testing. // normal hit testing.
// //
// Coordinates (x, y) are in content-buffer space the window manager // Coordinates (x, y) are in content-buffer space -- the window manager
// has already subtracted the window chrome offset. Widget positions // has already subtracted the window chrome offset. Widget positions
// are also in content-buffer space (set during layout), so no // are also in content-buffer space (set during layout), so no
// coordinate transform is needed for hit testing. // coordinate transform is needed for hit testing.
@ -412,7 +412,15 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
// Handle scrollbar thumb drag (mouse move while pressed) // Handle scrollbar thumb drag (mouse move while pressed)
if (sDragScrollbar && (buttons & MOUSE_LEFT)) { if (sDragScrollbar && (buttons & MOUSE_LEFT)) {
widgetScrollbarDragUpdate(sDragScrollbar, sDragScrollbarOrient, sDragScrollbarOff, x, y); widgetScrollbarDragUpdate(sDragScrollbar, sDragScrollbarOrient, sDragScrollbarOff, x, y);
// ScrollPane needs full relayout because it positions children
// at absolute coordinates incorporating the scroll offset.
if (sDragScrollbar->type == WidgetScrollPaneE) {
wgtInvalidate(sDragScrollbar);
} else {
wgtInvalidatePaint(root); wgtInvalidatePaint(root);
}
return; return;
} }
@ -523,7 +531,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
return; return;
} }
// Click outside popup close it and remember which widget it was // Click outside popup -- close it and remember which widget it was
sClosedPopup = sOpenPopup; sClosedPopup = sOpenPopup;
if (sOpenPopup->type == WidgetDropdownE) { if (sOpenPopup->type == WidgetDropdownE) {
@ -554,7 +562,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
// Clear focus from the previously focused widget. This is done via // Clear focus from the previously focused widget. This is done via
// the cached sFocusedWidget pointer rather than walking the tree to // the cached sFocusedWidget pointer rather than walking the tree to
// find the focused widget an O(1) operation vs O(n). // find the focused widget -- an O(1) operation vs O(n).
WidgetT *prevFocus = sFocusedWidget; WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) { if (sFocusedWidget) {
@ -568,7 +576,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
hit->wclass->onMouse(hit, root, vx, vy); hit->wclass->onMouse(hit, root, vx, vy);
} }
// Universal click/double-click callbacks fire for ALL widget types // Universal click/double-click callbacks -- fire for ALL widget types
// after the type-specific handler has run. Buttons and image buttons // after the type-specific handler has run. Buttons and image buttons
// are excluded from onClick because they use press-release semantics // are excluded from onClick because they use press-release semantics
// (onClick fires on button-up, not button-down) and already handle it // (onClick fires on button-up, not button-down) and already handle it
@ -619,7 +627,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
// Scroll offset is applied by shifting the root widget's position // Scroll offset is applied by shifting the root widget's position
// to negative coordinates (-scrollX, -scrollY). This elegantly makes // to negative coordinates (-scrollX, -scrollY). This elegantly makes
// scrolling work without any special scroll handling in individual // scrolling work without any special scroll handling in individual
// widgets their positions are simply offset, and the clip rect // widgets -- their positions are simply offset, and the clip rect
// on the DisplayT limits drawing to the visible area. // on the DisplayT limits drawing to the visible area.
// //
// The conditional re-layout avoids redundant layout passes when only // The conditional re-layout avoids redundant layout passes when only
@ -728,7 +736,7 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
(void)orient; (void)orient;
(void)value; (void)value;
// Repaint with new scroll position dvxInvalidateWindow calls onPaint // Repaint with new scroll position -- dvxInvalidateWindow calls onPaint
if (win->widgetRoot) { if (win->widgetRoot) {
AppContextT *ctx = wgtGetContext(win->widgetRoot); AppContextT *ctx = wgtGetContext(win->widgetRoot);
@ -740,7 +748,7 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
// ============================================================ // ============================================================
// widgetReorderDrop finalize drag-reorder on mouse release // widgetReorderDrop -- finalize drag-reorder on mouse release
// ============================================================ // ============================================================
// //
// Completes a drag-reorder operation for ListBox, ListView, or TreeView. // Completes a drag-reorder operation for ListBox, ListView, or TreeView.
@ -963,7 +971,7 @@ void widgetReorderDrop(WidgetT *w) {
// ============================================================ // ============================================================
// widgetReorderUpdate update drop position during drag // widgetReorderUpdate -- update drop position during drag
// ============================================================ // ============================================================
// //
// Tracks the mouse during a drag-reorder, updating the drop indicator // Tracks the mouse during a drag-reorder, updating the drop indicator

View file

@ -1,4 +1,4 @@
// widgetImage.c Image widget (displays bitmap, responds to clicks) // widgetImage.c -- Image widget (displays bitmap, responds to clicks)
// //
// Displays a bitmap image, optionally responding to clicks. The image data // Displays a bitmap image, optionally responding to clicks. The image data
// must be in the display's native pixel format (pre-converted). Two creation // must be in the display's native pixel format (pre-converted). Two creation
@ -9,11 +9,11 @@
// //
// The widget supports a simple press effect (1px offset on click) and fires // The widget supports a simple press effect (1px offset on click) and fires
// onClick immediately on mouse-down. Unlike Button which has press/release // onClick immediately on mouse-down. Unlike Button which has press/release
// tracking, Image fires instantly this is intentional for image-based // tracking, Image fires instantly -- this is intentional for image-based
// click targets where visual press feedback is less important than // click targets where visual press feedback is less important than
// responsiveness. // responsiveness.
// //
// No border or bevel is drawn the image fills its widget bounds with // No border or bevel is drawn -- the image fills its widget bounds with
// centering if the widget is larger than the image. // centering if the widget is larger than the image.
#include "widgetInternal.h" #include "widgetInternal.h"
@ -114,7 +114,7 @@ void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// Mouse click: briefly sets pressed=true (for the 1px offset effect), // Mouse click: briefly sets pressed=true (for the 1px offset effect),
// invalidates to show the press, fires onClick, then immediately clears // invalidates to show the press, fires onClick, then immediately clears
// pressed. The press is purely visual there's no release tracking like // pressed. The press is purely visual -- there's no release tracking like
// Button has, since Image clicks are meant for instant actions (e.g., // Button has, since Image clicks are meant for instant actions (e.g.,
// clicking a logo or icon area). // clicking a logo or icon area).
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {

View file

@ -1,4 +1,4 @@
// widgetImageButton.c Image button widget (button with image instead of text) // widgetImageButton.c -- Image button widget (button with image instead of text)
// //
// Combines a Button's beveled border and press behavior with an Image's // Combines a Button's beveled border and press behavior with an Image's
// bitmap rendering. The image is centered within the button bounds and // bitmap rendering. The image is centered within the button bounds and
@ -9,11 +9,11 @@
// and the event dispatcher handles release/cancel. The onClick callback // and the event dispatcher handles release/cancel. The onClick callback
// fires on release, not press. // fires on release, not press.
// //
// The widget takes ownership of the image data buffer if widget creation // The widget takes ownership of the image data buffer -- if widget creation
// fails, the data is freed to prevent leaks. // fails, the data is freed to prevent leaks.
// //
// The 4px added to min size (widgetImageButtonCalcMinSize) accounts for // The 4px added to min size (widgetImageButtonCalcMinSize) accounts for
// the 2px bevel on each side no extra padding is added beyond that, // the 2px bevel on each side -- no extra padding is added beyond that,
// keeping image buttons compact for toolbar use. // keeping image buttons compact for toolbar use.
#include "widgetInternal.h" #include "widgetInternal.h"

View file

@ -1,4 +1,4 @@
// widgetInternal.h Shared internal header for widget implementation files // widgetInternal.h -- Shared internal header for widget implementation files
// //
// This header is included only by widget implementation .c files (in the // This header is included only by widget implementation .c files (in the
// widgets/ directory), never by application code. It exposes the vtable // widgets/ directory), never by application code. It exposes the vtable
@ -6,11 +6,11 @@
// widget implementations need but the public API should not expose. // widget implementations need but the public API should not expose.
// //
// The widget system is split across multiple .c files by concern: // The widget system is split across multiple .c files by concern:
// widgetCore.c allocation, tree ops, focus management, shared helpers // widgetCore.c -- allocation, tree ops, focus management, shared helpers
// widgetLayout.c measure + layout algorithms for box containers // widgetLayout.c -- measure + layout algorithms for box containers
// widgetEvent.c mouse/keyboard dispatch, scrollbar management // widgetEvent.c -- mouse/keyboard dispatch, scrollbar management
// widgetOps.c paint dispatch, overlay rendering // widgetOps.c -- paint dispatch, overlay rendering
// widget*.c per-type paint, event, and layout implementations // widget*.c -- per-type paint, event, and layout implementations
#ifndef WIDGET_INTERNAL_H #ifndef WIDGET_INTERNAL_H
#define WIDGET_INTERNAL_H #define WIDGET_INTERNAL_H
@ -35,12 +35,12 @@
// //
// Flags encode static properties that the framework needs without calling // Flags encode static properties that the framework needs without calling
// into the vtable: // into the vtable:
// FOCUSABLE can receive keyboard focus (Tab navigation) // FOCUSABLE -- can receive keyboard focus (Tab navigation)
// BOX_CONTAINER uses the generic VBox/HBox layout algorithm // BOX_CONTAINER -- uses the generic VBox/HBox layout algorithm
// HORIZ_CONTAINER lays out children horizontally (vs. default vertical) // HORIZ_CONTAINER -- lays out children horizontally (vs. default vertical)
// PAINTS_CHILDREN the widget's paint function handles child rendering // PAINTS_CHILDREN -- the widget's paint function handles child rendering
// (e.g. TabControl only paints the active tab page) // (e.g. TabControl only paints the active tab page)
// NO_HIT_RECURSE hit testing stops at this widget and doesn't recurse // NO_HIT_RECURSE -- hit testing stops at this widget and doesn't recurse
// into children (e.g. ListBox handles its own items) // into children (e.g. ListBox handles its own items)
// //
// NULL function pointers are valid and mean "no-op" for that operation. // NULL function pointers are valid and mean "no-op" for that operation.
@ -66,7 +66,7 @@ typedef struct WidgetClassT {
void (*setText)(WidgetT *w, const char *text); void (*setText)(WidgetT *w, const char *text);
} WidgetClassT; } WidgetClassT;
// Global vtable array one entry per WidgetTypeE value. Defined in // Global vtable array -- one entry per WidgetTypeE value. Defined in
// widgetCore.c. Must stay in sync with the WidgetTypeE enum order. // widgetCore.c. Must stay in sync with the WidgetTypeE enum order.
extern const WidgetClassT *widgetClassTable[]; extern const WidgetClassT *widgetClassTable[];
@ -149,7 +149,7 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
// The illusion works by drawing the text twice: first offset +1,+1 in // The illusion works by drawing the text twice: first offset +1,+1 in
// the highlight color (creating a light "shadow" behind the text), then // the highlight color (creating a light "shadow" behind the text), then
// at the original position in the shadow color. The result is text that // at the original position in the shadow color. The result is text that
// appears chiseled into the surface the universal visual indicator for // appears chiseled into the surface -- the universal visual indicator for
// "greyed out" in Motif and Windows 3.x era GUIs. // "greyed out" in Motif and Windows 3.x era GUIs.
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) { static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
@ -170,7 +170,7 @@ static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const
// slider and resize a listview column simultaneously). Using globals // slider and resize a listview column simultaneously). Using globals
// rather than per-widget state avoids bloating every widget with fields // rather than per-widget state avoids bloating every widget with fields
// that are only relevant during a single drag operation. The tradeoff // that are only relevant during a single drag operation. The tradeoff
// is that these are truly global but since the GUI is single-threaded // is that these are truly global -- but since the GUI is single-threaded
// and only one mouse can exist, this is safe. // and only one mouse can exist, this is safe.
extern bool sCursorBlinkOn; // text cursor blink phase (toggled by wgtUpdateCursorBlink) extern bool sCursorBlinkOn; // text cursor blink phase (toggled by wgtUpdateCursorBlink)
@ -209,13 +209,13 @@ void widgetDestroyChildren(WidgetT *w);
// Allocate a new widget, set its type and vtable, and add it as a child. // Allocate a new widget, set its type and vtable, and add it as a child.
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type); WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type);
// Focus management Tab/Shift-Tab navigation walks the tree in creation // Focus management -- Tab/Shift-Tab navigation walks the tree in creation
// order, skipping non-focusable and hidden widgets. // order, skipping non-focusable and hidden widgets.
void widgetClearFocus(WidgetT *root); void widgetClearFocus(WidgetT *root);
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
// Alt+key accelerator lookup searches the tree for a widget whose // Alt+key accelerator lookup -- searches the tree for a widget whose
// accelKey matches, used for keyboard navigation of labeled controls. // accelKey matches, used for keyboard navigation of labeled controls.
WidgetT *widgetFindByAccel(WidgetT *root, char key); WidgetT *widgetFindByAccel(WidgetT *root, char key);
@ -226,7 +226,7 @@ bool widgetIsFocusable(WidgetTypeE type);
bool widgetIsBoxContainer(WidgetTypeE type); bool widgetIsBoxContainer(WidgetTypeE type);
bool widgetIsHorizContainer(WidgetTypeE type); bool widgetIsHorizContainer(WidgetTypeE type);
// Hit testing find the deepest widget containing the point (x,y). // Hit testing -- find the deepest widget containing the point (x,y).
// Respects WCLASS_NO_HIT_RECURSE to stop at list/tree widgets. // Respects WCLASS_NO_HIT_RECURSE to stop at list/tree widgets.
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
@ -498,7 +498,7 @@ void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTimerDestroy(WidgetT *w); void widgetTimerDestroy(WidgetT *w);
// Drag-reorder helpers generic drag-and-drop reordering shared by // Drag-reorder helpers -- generic drag-and-drop reordering shared by
// ListBox, ListView, and TreeView. The update function tracks the mouse // ListBox, ListView, and TreeView. The update function tracks the mouse
// position and computes the drop target, the drop function commits the // position and computes the drop target, the drop function commits the
// reorder by relinking the item in the data structure. // reorder by relinking the item in the data structure.

View file

@ -1,12 +1,12 @@
// widgetLabel.c Label widget // widgetLabel.c -- Label widget
// //
// Static text display the simplest widget. Not focusable, not interactive. // Static text display -- the simplest widget. Not focusable, not interactive.
// Supports accelerator keys via '&' prefix: when the user presses Alt+key, // Supports accelerator keys via '&' prefix: when the user presses Alt+key,
// focus moves to the next focusable widget after this label. This follows // focus moves to the next focusable widget after this label. This follows
// the Win3.1 convention where labels act as keyboard shortcuts for adjacent // the Win3.1 convention where labels act as keyboard shortcuts for adjacent
// controls (e.g., a label "&Name:" before a text input). // controls (e.g., a label "&Name:" before a text input).
// //
// The text pointer is stored directly (not copied) the caller must ensure // The text pointer is stored directly (not copied) -- the caller must ensure
// the string remains valid for the widget's lifetime, or use widgetLabelSetText // the string remains valid for the widget's lifetime, or use widgetLabelSetText
// to update it. This avoids unnecessary allocations for the common case of // to update it. This avoids unnecessary allocations for the common case of
// literal string labels. // literal string labels.
@ -68,15 +68,34 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t textW = textWidthAccel(font, w->as.label.text);
int32_t textX = w->x;
int32_t textY = w->y + (w->h - font->charHeight) / 2; int32_t textY = w->y + (w->h - font->charHeight) / 2;
if (w->as.label.textAlign == AlignCenterE) {
textX = w->x + (w->w - textW) / 2;
} else if (w->as.label.textAlign == AlignEndE) {
textX = w->x + w->w - textW;
}
if (!w->enabled) { if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, w->x, textY, w->as.label.text, colors); drawTextAccelEmbossed(d, ops, font, textX, textY, w->as.label.text, colors);
return; return;
} }
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
drawTextAccel(d, ops, font, w->x, textY, w->as.label.text, fg, bg, false); drawTextAccel(d, ops, font, textX, textY, w->as.label.text, fg, bg, false);
}
// ============================================================
// wgtLabelSetAlign
// ============================================================
void wgtLabelSetAlign(WidgetT *w, WidgetAlignE align) {
if (w && w->type == WidgetLabelE) {
w->as.label.textAlign = align;
}
} }

View file

@ -1,4 +1,4 @@
// widgetLayout.c Layout engine (measure + arrange) // widgetLayout.c -- Layout engine (measure + arrange)
// //
// Implements a two-pass layout algorithm inspired by CSS flexbox: // Implements a two-pass layout algorithm inspired by CSS flexbox:
// Pass 1 (Measure): bottom-up calculation of each widget's minimum // Pass 1 (Measure): bottom-up calculation of each widget's minimum
@ -15,7 +15,7 @@
// be bottom-up before arrange can be top-down. // be bottom-up before arrange can be top-down.
// //
// Why flexbox-like rather than constraint-based: // Why flexbox-like rather than constraint-based:
// Flexbox maps naturally to the VBox/HBox container model it's simple // Flexbox maps naturally to the VBox/HBox container model -- it's simple
// to understand, implement, and debug. Constraint-based layout (like // to understand, implement, and debug. Constraint-based layout (like
// Cassowary) would add significant complexity for little benefit in a // Cassowary) would add significant complexity for little benefit in a
// DOS GUI where most layouts are linear stacks of widgets. // DOS GUI where most layouts are linear stacks of widgets.
@ -406,7 +406,7 @@ void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT
// The tradeoff is that pixel values are limited to 30 bits (~1 billion), // The tradeoff is that pixel values are limited to 30 bits (~1 billion),
// which is far more than any supported display resolution. // which is far more than any supported display resolution.
// //
// A value of 0 means "auto" (use intrinsic/calculated size) the // A value of 0 means "auto" (use intrinsic/calculated size) -- the
// caller checks for 0 before calling this function. // caller checks for 0 before calling this function.
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) { int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {

View file

@ -1,4 +1,4 @@
// widgetListBox.c ListBox widget (single and multi-select) // widgetListBox.c -- ListBox widget (single and multi-select)
// //
// Scrollable list of text items with single-select or multi-select modes. // Scrollable list of text items with single-select or multi-select modes.
// Items are stored as external string pointers (not copied), with a vertical // Items are stored as external string pointers (not copied), with a vertical
@ -50,7 +50,7 @@ static void selectRange(WidgetT *w, int32_t from, int32_t to);
// ============================================================ // ============================================================
// Allocate (or re-allocate) the selection bits array. Only allocated in // Allocate (or re-allocate) the selection bits array. Only allocated in
// multi-select mode in single-select, selectedIdx alone tracks the // multi-select mode -- in single-select, selectedIdx alone tracks the
// selection. calloc initializes all bits to 0 (nothing selected). // selection. calloc initializes all bits to 0 (nothing selected).
static void allocSelBits(WidgetT *w) { static void allocSelBits(WidgetT *w) {
if (w->as.listBox.selBits) { if (w->as.listBox.selBits) {
@ -362,13 +362,13 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
bool ctrl = (mod & KEY_MOD_CTRL) != 0; bool ctrl = (mod & KEY_MOD_CTRL) != 0;
int32_t sel = w->as.listBox.selectedIdx; int32_t sel = w->as.listBox.selectedIdx;
// Ctrl+A select all (multi-select only) // Ctrl+A -- select all (multi-select only)
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) { if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
wgtListBoxSelectAll(w); wgtListBoxSelectAll(w);
return; return;
} }
// Space toggle current item (multi-select only) // Space -- toggle current item (multi-select only)
if (multi && key == ' ') { if (multi && key == ' ') {
if (sel >= 0 && w->as.listBox.selBits) { if (sel >= 0 && w->as.listBox.selBits) {
w->as.listBox.selBits[sel] ^= 1; w->as.listBox.selBits[sel] ^= 1;
@ -383,7 +383,7 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Enter activate selected item (same as double-click) // Enter -- activate selected item (same as double-click)
if (key == '\r' || key == '\n') { if (key == '\r' || key == '\n') {
if (w->onDblClick && sel >= 0) { if (w->onDblClick && sel >= 0) {
w->onDblClick(w); w->onDblClick(w);
@ -557,7 +557,7 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
// Paint: sunken bevel border, then iterate visible rows drawing text. // Paint: sunken bevel border, then iterate visible rows drawing text.
// Selected items get highlight background (full row width) with contrasting // Selected items get highlight background (full row width) with contrasting
// text color. In multi-select mode, the cursor item (selectedIdx) gets a // text color. In multi-select mode, the cursor item (selectedIdx) gets a
// dotted focus rect overlay this is separate from the selection highlight // dotted focus rect overlay -- this is separate from the selection highlight
// so the user can see which item the keyboard cursor is on even when multiple // so the user can see which item the keyboard cursor is on even when multiple
// items are selected. The scrollbar is drawn only when needed (item count // items are selected. The scrollbar is drawn only when needed (item count
// exceeds visible rows), and the content width is reduced accordingly. // exceeds visible rows), and the content width is reduced accordingly.

View file

@ -1,4 +1,4 @@
// widgetListView.c ListView (multi-column list) widget // widgetListView.c -- ListView (multi-column list) widget
// //
// A multi-column list with clickable/sortable column headers, horizontal // A multi-column list with clickable/sortable column headers, horizontal
// and vertical scrolling, column resize by dragging header borders, and // and vertical scrolling, column resize by dragging header borders, and
@ -11,16 +11,16 @@
// //
// ListView state is heap-allocated separately (ListViewDataT*) rather than // ListView state is heap-allocated separately (ListViewDataT*) rather than
// inlined in the widget union because the state is too large for a union // inlined in the widget union because the state is too large for a union
// member it includes per-column resolved widths, sort index, selection // member -- it includes per-column resolved widths, sort index, selection
// bits, and numerous scroll/drag state fields. // bits, and numerous scroll/drag state fields.
// //
// Sort: clicking a column header toggles sort direction. Sorting uses an // Sort: clicking a column header toggles sort direction. Sorting uses an
// indirection array (sortIndex) rather than rearranging the cellData. This // indirection array (sortIndex) rather than rearranging the cellData. This
// is critical because: // is critical because:
// 1. The application owns cellData and may not expect it to be mutated // 1. The application owns cellData and may not expect it to be mutated
// 2. Multi-select selBits are indexed by data row, not display row // 2. Multi-select selBits are indexed by data row, not display row --
// rearranging data would invalidate all selection state // rearranging data would invalidate all selection state
// 3. The indirection allows O(1) displaydata row mapping // 3. The indirection allows O(1) display<->data row mapping
// //
// Insertion sort is used because it's stable (preserves original order for // Insertion sort is used because it's stable (preserves original order for
// equal keys) and has good performance for the typical case of partially-sorted // equal keys) and has good performance for the typical case of partially-sorted
@ -44,7 +44,7 @@
// Horizontal scrolling is needed when total column width exceeds the widget's // Horizontal scrolling is needed when total column width exceeds the widget's
// inner width. Both headers and data rows are offset by scrollPosH. The // inner width. Both headers and data rows are offset by scrollPosH. The
// horizontal scrollbar appears automatically when needed, and its presence // horizontal scrollbar appears automatically when needed, and its presence
// may trigger the vertical scrollbar to appear (and vice versa) this // may trigger the vertical scrollbar to appear (and vice versa) -- this
// two-pass logic handles the interdependency correctly. // two-pass logic handles the interdependency correctly.
#include "widgetInternal.h" #include "widgetInternal.h"
@ -71,7 +71,7 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font);
// allocListViewSelBits // allocListViewSelBits
// ============================================================ // ============================================================
// Same allocation strategy as ListBox's selBits one byte per row, // Same allocation strategy as ListBox's selBits -- one byte per row,
// only allocated when multiSelect is enabled. // only allocated when multiSelect is enabled.
static void allocListViewSelBits(WidgetT *w) { static void allocListViewSelBits(WidgetT *w) {
if (w->as.listView->selBits) { if (w->as.listView->selBits) {
@ -97,16 +97,16 @@ static void allocListViewSelBits(WidgetT *w) {
// Build the sort indirection array. Rather than rearranging cellData (which // Build the sort indirection array. Rather than rearranging cellData (which
// the application owns and indexes into), we maintain a separate array that // the application owns and indexes into), we maintain a separate array that
// maps display-row data-row. Paint and keyboard navigation work in display-row // maps display-row -> data-row. Paint and keyboard navigation work in display-row
// space, then map through sortIndex to get the data row for cell lookups and // space, then map through sortIndex to get the data row for cell lookups and
// selection operations. This decoupling is essential because selBits are // selection operations. This decoupling is essential because selBits are
// indexed by data row sorting must not invalidate selection state. // indexed by data row -- sorting must not invalidate selection state.
static void listViewBuildSortIndex(WidgetT *w) { static void listViewBuildSortIndex(WidgetT *w) {
int32_t rowCount = w->as.listView->rowCount; int32_t rowCount = w->as.listView->rowCount;
int32_t sortCol = w->as.listView->sortCol; int32_t sortCol = w->as.listView->sortCol;
int32_t colCount = w->as.listView->colCount; int32_t colCount = w->as.listView->colCount;
// No sort active clear index // No sort active -- clear index
if (sortCol < 0 || w->as.listView->sortDir == ListViewSortNoneE || rowCount <= 0) { if (sortCol < 0 || w->as.listView->sortDir == ListViewSortNoneE || rowCount <= 0) {
if (w->as.listView->sortIndex) { if (w->as.listView->sortIndex) {
free(w->as.listView->sortIndex); free(w->as.listView->sortIndex);
@ -134,7 +134,7 @@ static void listViewBuildSortIndex(WidgetT *w) {
idx[i] = i; idx[i] = i;
} }
// Insertion sort stable (equal elements keep their original order), // Insertion sort -- stable (equal elements keep their original order),
// O(n^2) worst case but with very low constant factors. For typical // O(n^2) worst case but with very low constant factors. For typical
// ListView row counts (10s to low 100s), this beats quicksort because // ListView row counts (10s to low 100s), this beats quicksort because
// there's no recursion overhead, no stack usage, and the inner loop // there's no recursion overhead, no stack usage, and the inner loop
@ -491,7 +491,7 @@ void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
w->as.listView->reorderable = reorderable; w->as.listView->reorderable = reorderable;
// Disable sorting when reorderable sort order conflicts with manual order // Disable sorting when reorderable -- sort order conflicts with manual order
if (reorderable) { if (reorderable) {
w->as.listView->sortCol = -1; w->as.listView->sortCol = -1;
w->as.listView->sortDir = ListViewSortNoneE; w->as.listView->sortDir = ListViewSortNoneE;
@ -596,7 +596,7 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t rowCount = w->as.listView->rowCount; int32_t rowCount = w->as.listView->rowCount;
int32_t *sortIdx = w->as.listView->sortIndex; int32_t *sortIdx = w->as.listView->sortIndex;
// Enter activate selected item (same as double-click) // Enter -- activate selected item (same as double-click)
if (key == '\r' || key == '\n') { if (key == '\r' || key == '\n') {
if (w->onDblClick && w->as.listView->selectedIdx >= 0) { if (w->onDblClick && w->as.listView->selectedIdx >= 0) {
w->onDblClick(w); w->onDblClick(w);
@ -605,13 +605,13 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Ctrl+A select all (multi-select only) // Ctrl+A -- select all (multi-select only)
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) { if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
wgtListViewSelectAll(w); wgtListViewSelectAll(w);
return; return;
} }
// Space toggle current item (multi-select only) // Space -- toggle current item (multi-select only)
if (multi && key == ' ') { if (multi && key == ' ') {
int32_t sel = w->as.listView->selectedIdx; int32_t sel = w->as.listView->selectedIdx;
@ -738,7 +738,7 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
// distinct hit regions (checked in priority order): // distinct hit regions (checked in priority order):
// 1. Vertical scrollbar (right edge, if visible) // 1. Vertical scrollbar (right edge, if visible)
// 2. Horizontal scrollbar (bottom edge, if visible) // 2. Horizontal scrollbar (bottom edge, if visible)
// 3. Dead corner (when both scrollbars present no action) // 3. Dead corner (when both scrollbars present -- no action)
// 4. Column headers: resize drag (border +-3px) or sort toggle (click) // 4. Column headers: resize drag (border +-3px) or sort toggle (click)
// 5. Data rows: item selection with Ctrl/Shift modifiers, double-click, // 5. Data rows: item selection with Ctrl/Shift modifiers, double-click,
// and drag-reorder initiation // and drag-reorder initiation
@ -950,7 +950,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
colX += cw; colX += cw;
} }
// Not on a border check for sort click (disabled when reorderable // Not on a border -- check for sort click (disabled when reorderable
// since sorting conflicts with manual ordering). Clicking a column // since sorting conflicts with manual ordering). Clicking a column
// toggles between ascending/descending; clicking a different column // toggles between ascending/descending; clicking a different column
// starts ascending. // starts ascending.

View file

@ -1,4 +1,4 @@
// widgetOps.c Paint dispatcher and public widget operations // widgetOps.c -- Paint dispatcher and public widget operations
// //
// This file contains two categories of functions: // This file contains two categories of functions:
// 1. The paint dispatcher (widgetPaintOne, widgetPaintOverlays, wgtPaint) // 1. The paint dispatcher (widgetPaintOne, widgetPaintOverlays, wgtPaint)
@ -57,7 +57,7 @@ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
// //
// Recursive paint walker. For each visible widget: // Recursive paint walker. For each visible widget:
// 1. Call the widget's paint function (if any) via vtable. // 1. Call the widget's paint function (if any) via vtable.
// 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion // 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion --
// the widget's paint function already handled its children // the widget's paint function already handled its children
// (e.g. TabControl only paints the active tab's children). // (e.g. TabControl only paints the active tab's children).
// 3. Otherwise, recurse into children (default child painting). // 3. Otherwise, recurse into children (default child painting).
@ -190,7 +190,7 @@ void wgtDestroy(WidgetT *w) {
// ============================================================ // ============================================================
// widgetHashName djb2 hash for widget name lookup // widgetHashName -- djb2 hash for widget name lookup
// ============================================================ // ============================================================
// //
// The djb2 hash function (Dan Bernstein) is used for fast name-based // The djb2 hash function (Dan Bernstein) is used for fast name-based
@ -212,7 +212,7 @@ static uint32_t widgetHashName(const char *s) {
// ============================================================ // ============================================================
// wgtFindByHash recursive search with hash fast-reject // wgtFindByHash -- recursive search with hash fast-reject
// ============================================================ // ============================================================
// //
// Depth-first search by name with hash pre-screening. The hash // Depth-first search by name with hash pre-screening. The hash
@ -297,7 +297,7 @@ AppContextT *wgtGetContext(const WidgetT *w) {
// wgtGetText // wgtGetText
// ============================================================ // ============================================================
// //
// Polymorphic text getter dispatches through the vtable to the // Polymorphic text getter -- dispatches through the vtable to the
// appropriate getText implementation for the widget's type. Returns // appropriate getText implementation for the widget's type. Returns
// an empty string (not NULL) if the widget has no text or no getText // an empty string (not NULL) if the widget has no text or no getText
// handler, so callers don't need NULL checks. // handler, so callers don't need NULL checks.
@ -326,7 +326,7 @@ const char *wgtGetText(const WidgetT *w) {
// The root widget's userData points to the AppContextT, which is // The root widget's userData points to the AppContextT, which is
// the bridge back to the display, font, colors, and blitOps needed // the bridge back to the display, font, colors, and blitOps needed
// for painting. This avoids threading the context through every // for painting. This avoids threading the context through every
// widget function any widget can retrieve it via wgtGetContext(). // widget function -- any widget can retrieve it via wgtGetContext().
WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
WidgetT *root = widgetAlloc(NULL, WidgetVBoxE); WidgetT *root = widgetAlloc(NULL, WidgetVBoxE);
@ -355,7 +355,7 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
// Full invalidation: re-measures the widget tree, manages scrollbars, // Full invalidation: re-measures the widget tree, manages scrollbars,
// re-lays out, repaints, and dirties the window on screen. // re-lays out, repaints, and dirties the window on screen.
// //
// This is the "something structural changed" path use when widget // This is the "something structural changed" path -- use when widget
// sizes may have changed (text changed, children added/removed, // sizes may have changed (text changed, children added/removed,
// visibility toggled). If only visual state changed (cursor blink, // visibility toggled). If only visual state changed (cursor blink,
// selection highlight), use wgtInvalidatePaint() instead to skip // selection highlight), use wgtInvalidatePaint() instead to skip
@ -389,7 +389,7 @@ void wgtInvalidate(WidgetT *w) {
widgetManageScrollbars(w->window, ctx); widgetManageScrollbars(w->window, ctx);
} }
// Dirty the window dvxInvalidateWindow calls onPaint automatically // Dirty the window -- dvxInvalidateWindow calls onPaint automatically
dvxInvalidateWindow(ctx, w->window); dvxInvalidateWindow(ctx, w->window);
} }
@ -398,7 +398,7 @@ void wgtInvalidate(WidgetT *w) {
// wgtInvalidatePaint // wgtInvalidatePaint
// ============================================================ // ============================================================
// //
// Lightweight repaint skips measure/layout/scrollbar management. // Lightweight repaint -- skips measure/layout/scrollbar management.
// Use when only visual state changed (slider value, cursor blink, // Use when only visual state changed (slider value, cursor blink,
// selection highlight, checkbox toggle) but widget sizes are stable. // selection highlight, checkbox toggle) but widget sizes are stable.
@ -419,7 +419,7 @@ void wgtInvalidatePaint(WidgetT *w) {
return; return;
} }
// Dirty the window dvxInvalidateWindow calls onPaint automatically // Dirty the window -- dvxInvalidateWindow calls onPaint automatically
dvxInvalidateWindow(ctx, w->window); dvxInvalidateWindow(ctx, w->window);
} }

View file

@ -1,4 +1,4 @@
// widgetProgressBar.c ProgressBar widget // widgetProgressBar.c -- ProgressBar widget
// //
// A non-interactive fill-bar widget for displaying bounded progress. // A non-interactive fill-bar widget for displaying bounded progress.
// Supports both horizontal and vertical orientations via separate // Supports both horizontal and vertical orientations via separate

View file

@ -1,4 +1,4 @@
// widgetRadio.c RadioGroup and Radio button widgets // widgetRadio.c -- RadioGroup and Radio button widgets
// //
// Two-level architecture: RadioGroupE is an invisible container that // Two-level architecture: RadioGroupE is an invisible container that
// holds the selection state (selectedIdx), while RadioE children are // holds the selection state (selectedIdx), while RadioE children are
@ -130,7 +130,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
} else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) { } else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) {
// Down or Right next radio in group // Down or Right -- next radio in group
if (w->parent && w->parent->type == WidgetRadioGroupE) { if (w->parent && w->parent->type == WidgetRadioGroupE) {
WidgetT *next = NULL; WidgetT *next = NULL;
@ -155,7 +155,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) { } else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) {
// Up or Left previous radio in group // Up or Left -- previous radio in group
if (w->parent && w->parent->type == WidgetRadioGroupE) { if (w->parent && w->parent->type == WidgetRadioGroupE) {
WidgetT *prev = NULL; WidgetT *prev = NULL;
@ -238,7 +238,7 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
} }
} }
// Diamond border upper-left edges get highlight, lower-right get shadow // Diamond border -- upper-left edges get highlight, lower-right get shadow
for (int32_t i = 0; i < mid; i++) { for (int32_t i = 0; i < mid; i++) {
int32_t left = mid - i; int32_t left = mid - i;
int32_t right = mid + i; int32_t right = mid + i;

View file

@ -1,4 +1,4 @@
// widgetScrollPane.c ScrollPane container widget // widgetScrollPane.c -- ScrollPane container widget
// //
// A clipping container that allows its children (laid out as a // A clipping container that allows its children (laid out as a
// vertical box) to overflow and be scrolled into view. This is the // vertical box) to overflow and be scrolled into view. This is the
@ -150,7 +150,7 @@ static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
// ============================================================ // ============================================================
// spCalcNeeds determine scrollbar needs and inner dimensions // spCalcNeeds -- determine scrollbar needs and inner dimensions
// ============================================================ // ============================================================
// Central sizing function called by layout, paint, and mouse handlers. // Central sizing function called by layout, paint, and mouse handlers.
@ -234,7 +234,7 @@ void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) {
widgetCalcMinSizeTree(c, font); widgetCalcMinSizeTree(c, font);
} }
// The scroll pane's own min size is small the whole point is scrolling // The scroll pane's own min size is small -- the whole point is scrolling
w->calcMinW = SP_SB_W * 3 + SP_BORDER * 2; w->calcMinW = SP_SB_W * 3 + SP_BORDER * 2;
w->calcMinH = SP_SB_W * 3 + SP_BORDER * 2; w->calcMinH = SP_SB_W * 3 + SP_BORDER * 2;
} }
@ -424,7 +424,7 @@ void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV); w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV);
w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH); w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH);
wgtInvalidatePaint(w); wgtInvalidate(w);
} }
@ -508,7 +508,7 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
} }
hit->as.scrollPane.scrollPosV = clampInt(hit->as.scrollPane.scrollPosV, 0, maxScrollV); hit->as.scrollPane.scrollPosV = clampInt(hit->as.scrollPane.scrollPosV, 0, maxScrollV);
wgtInvalidatePaint(hit); wgtInvalidate(hit);
return; return;
} }
} }
@ -552,7 +552,7 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
} }
hit->as.scrollPane.scrollPosH = clampInt(hit->as.scrollPane.scrollPosH, 0, maxScrollH); hit->as.scrollPane.scrollPosH = clampInt(hit->as.scrollPane.scrollPosH, 0, maxScrollH);
wgtInvalidatePaint(hit); wgtInvalidate(hit);
return; return;
} }
} }
@ -567,7 +567,7 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
} }
} }
// Click on content area forward to child widgets // Click on content area -- forward to child widgets
// Children are already positioned at scroll-adjusted coordinates // Children are already positioned at scroll-adjusted coordinates
WidgetT *child = NULL; WidgetT *child = NULL;

View file

@ -1,4 +1,4 @@
// widgetScrollbar.c Shared scrollbar painting and hit-testing // widgetScrollbar.c -- Shared scrollbar painting and hit-testing
// //
// These are not widgets themselves -- they are stateless rendering and // These are not widgets themselves -- they are stateless rendering and
// hit-testing utilities shared by ScrollPane, TreeView, TextArea, // hit-testing utilities shared by ScrollPane, TreeView, TextArea,
@ -47,14 +47,14 @@ void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
drawBevel(d, ops, sbX, sbY, sbW, WGT_SB_W, &troughBevel); drawBevel(d, ops, sbX, sbY, sbW, WGT_SB_W, &troughBevel);
// Left arrow button // Left arrow button
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel); drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
// Left arrow triangle // Left arrow triangle
{ {
int32_t cx = sbX + WGT_SB_W / 2; int32_t cx = sbX + WGT_SB_W / 2;
int32_t cy = sbY + WGT_SB_W / 2; int32_t cy = sbY + WGT_SB_W / 2;
uint32_t fg = colors->contentFg; uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
@ -69,7 +69,7 @@ void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
{ {
int32_t cx = rightX + WGT_SB_W / 2; int32_t cx = rightX + WGT_SB_W / 2;
int32_t cy = sbY + WGT_SB_W / 2; int32_t cy = sbY + WGT_SB_W / 2;
uint32_t fg = colors->contentFg; uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
@ -103,14 +103,14 @@ void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
drawBevel(d, ops, sbX, sbY, WGT_SB_W, sbH, &troughBevel); drawBevel(d, ops, sbX, sbY, WGT_SB_W, sbH, &troughBevel);
// Up arrow button // Up arrow button
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel); drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
// Up arrow triangle // Up arrow triangle
{ {
int32_t cx = sbX + WGT_SB_W / 2; int32_t cx = sbX + WGT_SB_W / 2;
int32_t cy = sbY + WGT_SB_W / 2; int32_t cy = sbY + WGT_SB_W / 2;
uint32_t fg = colors->contentFg; uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg);
@ -125,7 +125,7 @@ void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
{ {
int32_t cx = sbX + WGT_SB_W / 2; int32_t cx = sbX + WGT_SB_W / 2;
int32_t cy = downY + WGT_SB_W / 2; int32_t cy = downY + WGT_SB_W / 2;
uint32_t fg = colors->contentFg; uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg);
@ -405,9 +405,12 @@ void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int3
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
// Compute content min size by measuring children // Compute content min size (must match spCalcNeeds in widgetScrollPane.c)
int32_t contentMinW = 0; int32_t contentMinW = 0;
int32_t contentMinH = 0; int32_t contentMinH = 0;
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
int32_t count = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) { if (c->visible) {
@ -420,9 +423,17 @@ void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int3
} }
contentMinH += c->calcMinH; contentMinH += c->calcMinH;
count++;
} }
} }
if (count > 1) {
contentMinH += gap * (count - 1);
}
contentMinW += pad * 2;
contentMinH += pad * 2;
int32_t innerH = w->h - SP_BORDER * 2; int32_t innerH = w->h - SP_BORDER * 2;
int32_t innerW = w->w - SP_BORDER * 2; int32_t innerW = w->w - SP_BORDER * 2;
bool needVSb = (contentMinH > innerH); bool needVSb = (contentMinH > innerH);

View file

@ -1,4 +1,4 @@
// widgetSeparator.c Separator widget (horizontal and vertical) // widgetSeparator.c -- Separator widget (horizontal and vertical)
// //
// A purely decorative widget that draws a 2px etched line (shadow + // A purely decorative widget that draws a 2px etched line (shadow +
// highlight pair) to visually divide groups of widgets. The etched // highlight pair) to visually divide groups of widgets. The etched

View file

@ -1,4 +1,4 @@
// widgetSlider.c Slider (trackbar) widget // widgetSlider.c -- Slider (trackbar) widget
// //
// A continuous-value selector with a draggable thumb on a groove track. // A continuous-value selector with a draggable thumb on a groove track.
// Supports both horizontal and vertical orientation. The value maps // Supports both horizontal and vertical orientation. The value maps
@ -127,9 +127,9 @@ void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
} else { } else {
if (key == (0x4B | 0x100)) { if (key == (0x4B | 0x100) || key == (0x48 | 0x100)) {
w->as.slider.value -= 1; w->as.slider.value -= 1;
} else if (key == (0x4D | 0x100)) { } else if (key == (0x4D | 0x100) || key == (0x50 | 0x100)) {
w->as.slider.value += 1; w->as.slider.value += 1;
} else if (key == (0x49 | 0x100)) { } else if (key == (0x49 | 0x100)) {
w->as.slider.value -= pageStep; w->as.slider.value -= pageStep;
@ -186,11 +186,11 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
mousePos = vy - hit->y; mousePos = vy - hit->y;
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) { if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
// Click on thumb start drag // Click on thumb -- start drag
sDragSlider = hit; sDragSlider = hit;
sDragOffset = mousePos - thumbPos; sDragOffset = mousePos - thumbPos;
} else { } else {
// Click on track jump to position // Click on track -- jump to position
int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange; int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; } if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
@ -208,11 +208,11 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
mousePos = vx - hit->x; mousePos = vx - hit->x;
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) { if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
// Click on thumb start drag // Click on thumb -- start drag
sDragSlider = hit; sDragSlider = hit;
sDragOffset = mousePos - thumbPos; sDragOffset = mousePos - thumbPos;
} else { } else {
// Click on track jump to position // Click on track -- jump to position
int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange; int32_t newVal = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; } if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }

View file

@ -1,4 +1,4 @@
// widgetSpacer.c Spacer widget (invisible stretching element) // widgetSpacer.c -- Spacer widget (invisible stretching element)
// //
// A zero-sized invisible widget with weight=100, used purely for // A zero-sized invisible widget with weight=100, used purely for
// layout control. It absorbs leftover space in box layouts, pushing // layout control. It absorbs leftover space in box layouts, pushing

View file

@ -1,4 +1,4 @@
// widgetSpinner.c Spinner (numeric up/down) widget // widgetSpinner.c -- Spinner (numeric up/down) widget
// //
// A hybrid widget combining a single-line text editor with up/down // A hybrid widget combining a single-line text editor with up/down
// arrow buttons for numeric value entry. The user can either click // arrow buttons for numeric value entry. The user can either click
@ -155,7 +155,7 @@ const char *widgetSpinnerGetText(const WidgetT *w) {
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t step = w->as.spinner.step; int32_t step = w->as.spinner.step;
// Up arrow increment // Up arrow -- increment
if (key == (0x48 | 0x100)) { if (key == (0x48 | 0x100)) {
spinnerCommitEdit(w); spinnerCommitEdit(w);
w->as.spinner.value += step; w->as.spinner.value += step;
@ -169,7 +169,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Down arrow decrement // Down arrow -- decrement
if (key == (0x50 | 0x100)) { if (key == (0x50 | 0x100)) {
spinnerCommitEdit(w); spinnerCommitEdit(w);
w->as.spinner.value -= step; w->as.spinner.value -= step;
@ -183,7 +183,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Page Up increment by step * 10 // Page Up -- increment by step * 10
if (key == (0x49 | 0x100)) { if (key == (0x49 | 0x100)) {
spinnerCommitEdit(w); spinnerCommitEdit(w);
w->as.spinner.value += step * 10; w->as.spinner.value += step * 10;
@ -197,7 +197,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Page Down decrement by step * 10 // Page Down -- decrement by step * 10
if (key == (0x51 | 0x100)) { if (key == (0x51 | 0x100)) {
spinnerCommitEdit(w); spinnerCommitEdit(w);
w->as.spinner.value -= step * 10; w->as.spinner.value -= step * 10;
@ -211,7 +211,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Enter commit edit // Enter -- commit edit
if (key == '\r' || key == '\n') { if (key == '\r' || key == '\n') {
if (w->as.spinner.editing) { if (w->as.spinner.editing) {
spinnerCommitEdit(w); spinnerCommitEdit(w);
@ -226,7 +226,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Escape cancel edit, revert to current value // Escape -- cancel edit, revert to current value
if (key == 27) { if (key == 27) {
if (w->as.spinner.editing) { if (w->as.spinner.editing) {
w->as.spinner.editing = false; w->as.spinner.editing = false;
@ -266,6 +266,32 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.spinner.undoBuf, &w->as.spinner.undoLen, w->as.spinner.undoBuf, &w->as.spinner.undoLen,
&w->as.spinner.undoCursor); &w->as.spinner.undoCursor);
// Validate buffer after paste -- reject non-numeric content.
// Allow optional leading minus and digits only.
bool valid = true;
for (int32_t i = 0; i < w->as.spinner.len; i++) {
char c = w->as.spinner.buf[i];
if (c == '-' && i == 0 && w->as.spinner.minValue < 0) {
continue;
}
if (c < '0' || c > '9') {
valid = false;
break;
}
}
if (!valid) {
// Revert to the undo buffer (pre-paste state)
memcpy(w->as.spinner.buf, w->as.spinner.undoBuf, sizeof(w->as.spinner.buf));
w->as.spinner.len = w->as.spinner.undoLen;
w->as.spinner.cursorPos = w->as.spinner.undoCursor;
w->as.spinner.selStart = 0;
w->as.spinner.selEnd = 0;
}
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
} }

View file

@ -1,4 +1,4 @@
// widgetSplitter.c Splitter (draggable divider between two panes) // widgetSplitter.c -- Splitter (draggable divider between two panes)
// //
// A container that divides its area between exactly two child widgets // A container that divides its area between exactly two child widgets
// with a draggable divider bar between them. The divider position // with a draggable divider bar between them. The divider position
@ -39,7 +39,7 @@ static WidgetT *spSecondChild(WidgetT *w);
// ============================================================ // ============================================================
// spFirstChild get first visible child // spFirstChild -- get first visible child
// ============================================================ // ============================================================
// These helpers skip invisible children so the splitter works // These helpers skip invisible children so the splitter works
@ -58,7 +58,7 @@ static WidgetT *spFirstChild(WidgetT *w) {
// ============================================================ // ============================================================
// spSecondChild get second visible child // spSecondChild -- get second visible child
// ============================================================ // ============================================================
static WidgetT *spSecondChild(WidgetT *w) { static WidgetT *spSecondChild(WidgetT *w) {
@ -79,7 +79,7 @@ static WidgetT *spSecondChild(WidgetT *w) {
// ============================================================ // ============================================================
// widgetSplitterClampPos clamp divider to child minimums // widgetSplitterClampPos -- clamp divider to child minimums
// ============================================================ // ============================================================
void widgetSplitterClampPos(WidgetT *w, int32_t *pos) { void widgetSplitterClampPos(WidgetT *w, int32_t *pos) {
@ -316,14 +316,14 @@ void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
} }
// Draw divider bar raised bevel // Draw divider bar -- raised bevel
BevelStyleT bevel = BEVEL_RAISED(colors, 1); BevelStyleT bevel = BEVEL_RAISED(colors, 1);
if (w->as.splitter.vertical) { if (w->as.splitter.vertical) {
int32_t barX = w->x + pos; int32_t barX = w->x + pos;
drawBevel(d, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel); drawBevel(d, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel);
// Gripper row of embossed 2x2 bumps centered vertically // Gripper -- row of embossed 2x2 bumps centered vertically
int32_t gx = barX + 1; int32_t gx = barX + 1;
int32_t midY = w->y + w->h / 2; int32_t midY = w->y + w->h / 2;
int32_t count = 5; int32_t count = 5;
@ -337,7 +337,7 @@ void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t barY = w->y + pos; int32_t barY = w->y + pos;
drawBevel(d, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel); drawBevel(d, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel);
// Gripper row of embossed 2x2 bumps centered horizontally // Gripper -- row of embossed 2x2 bumps centered horizontally
int32_t gy = barY + 1; int32_t gy = barY + 1;
int32_t midX = w->x + w->w / 2; int32_t midX = w->x + w->w / 2;
int32_t count = 5; int32_t count = 5;

View file

@ -1,4 +1,4 @@
// widgetStatusBar.c StatusBar widget // widgetStatusBar.c -- StatusBar widget
// //
// A horizontal container that draws a sunken border around each visible // A horizontal container that draws a sunken border around each visible
// child to create the classic segmented status bar appearance. Children // child to create the classic segmented status bar appearance. Children

View file

@ -1,4 +1,4 @@
// widgetTabControl.c TabControl and TabPage widgets // widgetTabControl.c -- TabControl and TabPage widgets
// //
// Two-level architecture: TabControlE is the container holding // Two-level architecture: TabControlE is the container holding
// selection state and rendering the tab header strip. TabPageE // selection state and rendering the tab header strip. TabPageE
@ -46,7 +46,7 @@ static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font);
// ============================================================ // ============================================================
// tabClosePopup close any open dropdown/combobox popup // tabClosePopup -- close any open dropdown/combobox popup
// ============================================================ // ============================================================
static void tabClosePopup(void) { static void tabClosePopup(void) {
@ -63,7 +63,7 @@ static void tabClosePopup(void) {
// ============================================================ // ============================================================
// tabEnsureVisible scroll so active tab is visible // tabEnsureVisible -- scroll so active tab is visible
// ============================================================ // ============================================================
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) { static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
@ -119,7 +119,7 @@ static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
// tabHeaderTotalW total width of all tab headers // tabHeaderTotalW -- total width of all tab headers
// ============================================================ // ============================================================
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) { static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) {
@ -136,7 +136,7 @@ static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
// tabNeedScroll do tab headers overflow? // tabNeedScroll -- do tab headers overflow?
// ============================================================ // ============================================================
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font) { static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font) {
@ -469,7 +469,7 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
} }
} }
// Tab headers clip to header area // Tab headers -- clip to header area
int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0); int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0);
int32_t headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w); int32_t headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w);

View file

@ -1,13 +1,13 @@
// widgetTextInput.c TextInput and TextArea widgets // widgetTextInput.c -- TextInput and TextArea widgets
// //
// This file implements three text editing widgets plus shared // This file implements three text editing widgets plus shared
// infrastructure: // infrastructure:
// //
// 1. TextInput single-line text field with scroll, selection, // 1. TextInput -- single-line text field with scroll, selection,
// undo, password masking, and masked input (e.g., phone/SSN). // undo, password masking, and masked input (e.g., phone/SSN).
// 2. TextArea multi-line text editor with row/col cursor, // 2. TextArea -- multi-line text editor with row/col cursor,
// dual-axis scrolling, and full selection/clipboard support. // dual-axis scrolling, and full selection/clipboard support.
// 3. Shared infrastructure clipboard, multi-click detection, // 3. Shared infrastructure -- clipboard, multi-click detection,
// word boundary logic, cross-widget selection clearing, and // word boundary logic, cross-widget selection clearing, and
// the single-line text editing engine (widgetTextEditOnKey). // the single-line text editing engine (widgetTextEditOnKey).
// //
@ -760,7 +760,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)shift; (void)shift;
// Ctrl+A select all // Ctrl+A -- select all
if (key == 1) { if (key == 1) {
w->as.textInput.selStart = 0; w->as.textInput.selStart = 0;
w->as.textInput.selEnd = maskLen; w->as.textInput.selEnd = maskLen;
@ -768,7 +768,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
goto done; goto done;
} }
// Ctrl+C copy formatted text // Ctrl+C -- copy formatted text
if (key == 3) { if (key == 3) {
if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) { if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) {
int32_t selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd; int32_t selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd;
@ -790,7 +790,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Ctrl+V paste valid chars into slots // Ctrl+V -- paste valid chars into slots
if (key == 22) { if (key == 22) {
int32_t clipLen; int32_t clipLen;
const char *clip = clipboardGet(&clipLen); const char *clip = clipboardGet(&clipLen);
@ -835,7 +835,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
goto done; goto done;
} }
// Ctrl+X copy and clear selected slots // Ctrl+X -- copy and clear selected slots
if (key == 24) { if (key == 24) {
if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) { if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) {
int32_t selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd; int32_t selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd;
@ -873,7 +873,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
goto done; goto done;
} }
// Ctrl+Z undo // Ctrl+Z -- undo
if (key == 26 && w->as.textInput.undoBuf) { if (key == 26 && w->as.textInput.undoBuf) {
char tmpBuf[256]; char tmpBuf[256];
int32_t tmpLen = maskLen + 1 < (int32_t)sizeof(tmpBuf) ? maskLen + 1 : (int32_t)sizeof(tmpBuf); int32_t tmpLen = maskLen + 1 < (int32_t)sizeof(tmpBuf) ? maskLen + 1 : (int32_t)sizeof(tmpBuf);
@ -897,7 +897,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
if (key >= 32 && key < 127) { if (key >= 32 && key < 127) {
// Printable character place at current slot if valid // Printable character -- place at current slot if valid
if (*pCur < maskLen && maskIsSlot(mask[*pCur]) && maskCharValid(mask[*pCur], (char)key)) { if (*pCur < maskLen && maskIsSlot(mask[*pCur]) && maskCharValid(mask[*pCur], (char)key)) {
if (w->as.textInput.undoBuf) { if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize); textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
@ -914,7 +914,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == 8) { } else if (key == 8) {
// Backspace clear previous slot // Backspace -- clear previous slot
int32_t prev = maskPrevSlot(mask, *pCur); int32_t prev = maskPrevSlot(mask, *pCur);
if (prev != *pCur) { if (prev != *pCur) {
@ -933,7 +933,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == (0x53 | 0x100)) { } else if (key == (0x53 | 0x100)) {
// Delete clear current slot // Delete -- clear current slot
if (*pCur < maskLen && maskIsSlot(mask[*pCur])) { if (*pCur < maskLen && maskIsSlot(mask[*pCur])) {
if (w->as.textInput.undoBuf) { if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize); textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
@ -949,7 +949,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == (0x4B | 0x100)) { } else if (key == (0x4B | 0x100)) {
// Left arrow move to previous slot // Left arrow -- move to previous slot
int32_t prev = maskPrevSlot(mask, *pCur); int32_t prev = maskPrevSlot(mask, *pCur);
if (shift) { if (shift) {
@ -966,7 +966,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pCur = prev; *pCur = prev;
} }
} else if (key == (0x4D | 0x100)) { } else if (key == (0x4D | 0x100)) {
// Right arrow move to next slot // Right arrow -- move to next slot
int32_t next = maskNextSlot(mask, *pCur); int32_t next = maskNextSlot(mask, *pCur);
if (shift) { if (shift) {
@ -983,7 +983,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pCur = next; *pCur = next;
} }
} else if (key == (0x47 | 0x100)) { } else if (key == (0x47 | 0x100)) {
// Home first slot // Home -- first slot
if (shift) { if (shift) {
if (w->as.textInput.selStart < 0) { if (w->as.textInput.selStart < 0) {
w->as.textInput.selStart = *pCur; w->as.textInput.selStart = *pCur;
@ -998,7 +998,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pCur = maskFirstSlot(mask); *pCur = maskFirstSlot(mask);
} }
} else if (key == (0x4F | 0x100)) { } else if (key == (0x4F | 0x100)) {
// End past last slot // End -- past last slot
int32_t last = maskLen; int32_t last = maskLen;
if (shift) { if (shift) {
@ -1299,7 +1299,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
// Ctrl+A select all // Ctrl+A -- select all
if (key == 1) { if (key == 1) {
*pSA = 0; *pSA = 0;
*pSC = *pLen; *pSC = *pLen;
@ -1310,7 +1310,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Ctrl+C copy // Ctrl+C -- copy
if (key == 3) { if (key == 3) {
if (HAS_SEL()) { if (HAS_SEL()) {
clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
@ -1324,7 +1324,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
goto navigation; goto navigation;
} }
// Ctrl+V paste // Ctrl+V -- paste
if (key == 22) { if (key == 22) {
if (sClipboardLen > 0) { if (sClipboardLen > 0) {
textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize);
@ -1363,7 +1363,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Ctrl+X cut // Ctrl+X -- cut
if (key == 24) { if (key == 24) {
if (HAS_SEL()) { if (HAS_SEL()) {
clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
@ -1390,7 +1390,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Ctrl+Z undo // Ctrl+Z -- undo
if (key == 26) { if (key == 26) {
if (w->as.textArea.undoBuf && w->as.textArea.undoLen >= 0) { if (w->as.textArea.undoBuf && w->as.textArea.undoLen >= 0) {
// Swap current and undo // Swap current and undo
@ -1434,7 +1434,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
return; return;
} }
// Enter insert newline // Enter -- insert newline
if (key == 0x0D) { if (key == 0x0D) {
if (*pLen < bufSize - 1) { if (*pLen < bufSize - 1) {
textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize);
@ -1581,7 +1581,7 @@ navigation:
return; return;
} }
// Ctrl+Left word left // Ctrl+Left -- word left
if (key == (0x73 | 0x100)) { if (key == (0x73 | 0x100)) {
SEL_BEGIN(); SEL_BEGIN();
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
@ -1594,7 +1594,7 @@ navigation:
return; return;
} }
// Ctrl+Right word right // Ctrl+Right -- word right
if (key == (0x74 | 0x100)) { if (key == (0x74 | 0x100)) {
SEL_BEGIN(); SEL_BEGIN();
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
@ -1905,7 +1905,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
return; return;
} }
// Click on text area place cursor // Click on text area -- place cursor
int32_t relX = vx - innerX; int32_t relX = vx - innerX;
int32_t relY = vy - innerY; int32_t relY = vy - innerY;
@ -1978,7 +1978,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// ============================================================ // ============================================================
// widgetTextDragUpdate update selection during mouse drag // widgetTextDragUpdate -- update selection during mouse drag
// ============================================================ // ============================================================
// Called by the event loop on mouse-move while sDragTextSelect is set. // Called by the event loop on mouse-move while sDragTextSelect is set.
@ -2164,7 +2164,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
selHi = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selCursor : w->as.textArea.selAnchor; selHi = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selCursor : w->as.textArea.selAnchor;
} }
// Draw lines compute first visible line offset once, then advance incrementally // Draw lines -- compute first visible line offset once, then advance incrementally
int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD;
int32_t textY = w->y + TEXTAREA_BORDER; int32_t textY = w->y + TEXTAREA_BORDER;
int32_t lineOff = textAreaLineStart(buf, len, w->as.textArea.scrollRow); int32_t lineOff = textAreaLineStart(buf, len, w->as.textArea.scrollRow);
@ -2244,7 +2244,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
} }
} }
} else { } else {
// No selection on this line single run // No selection on this line -- single run
if (drawStart < drawEnd) { if (drawStart < drawEnd) {
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true); drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true);
} }
@ -2559,7 +2559,7 @@ void widgetTextInputSetText(WidgetT *w, const char *text) {
// ============================================================ // ============================================================
// widgetTextEditOnKey shared single-line text editing logic // widgetTextEditOnKey -- shared single-line text editing logic
// ============================================================ // ============================================================
// This is the core single-line text editing engine, parameterized by // This is the core single-line text editing engine, parameterized by
@ -2603,7 +2603,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
} }
} }
// Ctrl+A select all // Ctrl+A -- select all
if (key == 1 && pSelStart && pSelEnd) { if (key == 1 && pSelStart && pSelEnd) {
*pSelStart = 0; *pSelStart = 0;
*pSelEnd = *pLen; *pSelEnd = *pLen;
@ -2611,7 +2611,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
goto adjustScroll; goto adjustScroll;
} }
// Ctrl+C copy // Ctrl+C -- copy
if (key == 3) { if (key == 3) {
if (hasSel) { if (hasSel) {
clipboardCopy(buf + selLo, selHi - selLo); clipboardCopy(buf + selLo, selHi - selLo);
@ -2620,7 +2620,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
return; return;
} }
// Ctrl+V paste // Ctrl+V -- paste
if (key == 22) { if (key == 22) {
if (sClipboardLen > 0) { if (sClipboardLen > 0) {
if (undoBuf) { if (undoBuf) {
@ -2666,7 +2666,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
goto adjustScroll; goto adjustScroll;
} }
// Ctrl+X cut // Ctrl+X -- cut
if (key == 24) { if (key == 24) {
if (hasSel) { if (hasSel) {
clipboardCopy(buf + selLo, selHi - selLo); clipboardCopy(buf + selLo, selHi - selLo);
@ -2685,7 +2685,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
goto adjustScroll; goto adjustScroll;
} }
// Ctrl+Z undo // Ctrl+Z -- undo
if (key == 26 && undoBuf && pUndoLen && pUndoCursor) { if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
// Swap current and undo // Swap current and undo
char tmpBuf[CLIPBOARD_MAX]; char tmpBuf[CLIPBOARD_MAX];
@ -2827,7 +2827,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
} }
} }
} else if (key == (0x73 | 0x100)) { } else if (key == (0x73 | 0x100)) {
// Ctrl+Left word left // Ctrl+Left -- word left
int32_t newPos = wordBoundaryLeft(buf, *pCursor); int32_t newPos = wordBoundaryLeft(buf, *pCursor);
if (shift && pSelStart && pSelEnd) { if (shift && pSelStart && pSelEnd) {
@ -2850,7 +2850,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
*pCursor = newPos; *pCursor = newPos;
} }
} else if (key == (0x74 | 0x100)) { } else if (key == (0x74 | 0x100)) {
// Ctrl+Right word right // Ctrl+Right -- word right
int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor); int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor);
if (shift && pSelStart && pSelEnd) { if (shift && pSelStart && pSelEnd) {

View file

@ -1,7 +1,7 @@
// widgetTimer.c Invisible timer widget // widgetTimer.c -- Invisible timer widget
// //
// Fires onChange callbacks at a configurable interval. Supports both // Fires onChange callbacks at a configurable interval. Supports both
// one-shot and repeating modes. The timer is invisible it takes no // one-shot and repeating modes. The timer is invisible -- it takes no
// space in layout and produces no visual output. // space in layout and produces no visual output.
// //
// Active timers are tracked via a module-level linked list (using the // Active timers are tracked via a module-level linked list (using the
@ -15,7 +15,7 @@
#include "thirdparty/stb_ds.h" #include "thirdparty/stb_ds.h"
// Active timer list avoids walking the widget tree per frame // Active timer list -- avoids walking the widget tree per frame
static WidgetT **sActiveTimers = NULL; static WidgetT **sActiveTimers = NULL;

View file

@ -1,4 +1,4 @@
// widgetToolbar.c Toolbar widget // widgetToolbar.c -- Toolbar widget
// //
// A horizontal container with a raised 1px bevel background, used to // A horizontal container with a raised 1px bevel background, used to
// hold buttons, separators, spacers, and other control widgets in a // hold buttons, separators, spacers, and other control widgets in a

View file

@ -1,4 +1,4 @@
// widgetTreeView.c TreeView and TreeItem widgets // widgetTreeView.c -- TreeView and TreeItem widgets
// //
// A hierarchical list with expand/collapse nodes, scrolling, multi-select, // A hierarchical list with expand/collapse nodes, scrolling, multi-select,
// and drag-reorder support. // and drag-reorder support.
@ -419,7 +419,7 @@ static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
return node; return node;
} }
// No previous sibling go to parent (if it's a tree item) // No previous sibling -- go to parent (if it's a tree item)
if (item->parent && item->parent != treeView && item->parent->type == WidgetTreeItemE) { if (item->parent && item->parent != treeView && item->parent->type == WidgetTreeItemE) {
return item->parent; return item->parent;
} }
@ -496,7 +496,7 @@ static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t
bool needVSb = (totalH > innerH); bool needVSb = (totalH > innerH);
bool needHSb = (totalW > innerW); bool needHSb = (totalW > innerW);
// V scrollbar reduces available width may trigger H scrollbar // V scrollbar reduces available width -- may trigger H scrollbar
if (needVSb) { if (needVSb) {
innerW -= WGT_SB_W; innerW -= WGT_SB_W;
@ -505,7 +505,7 @@ static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t
} }
} }
// H scrollbar reduces available height may trigger V scrollbar // H scrollbar reduces available height -- may trigger V scrollbar
if (needHSb) { if (needHSb) {
innerH -= WGT_SB_W; innerH -= WGT_SB_W;
@ -662,11 +662,48 @@ void wgtTreeItemSetExpanded(WidgetT *w, bool expanded) {
// wgtTreeView // wgtTreeView
// ============================================================ // ============================================================
// Default double-click handler: toggle expand/collapse on the selected
// item if it has children, otherwise fire the item's onDblClick/onClick.
static void treeDefaultDblClick(WidgetT *w) {
WidgetT *sel = w->as.treeView.selectedItem;
if (!sel) {
return;
}
bool hasChildren = false;
for (WidgetT *c = sel->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTreeItemE) {
hasChildren = true;
break;
}
}
if (hasChildren) {
sel->as.treeItem.expanded = !sel->as.treeItem.expanded;
if (sel->onChange) {
sel->onChange(sel);
}
wgtInvalidatePaint(w);
} else {
if (sel->onDblClick) {
sel->onDblClick(sel);
} else if (sel->onClick) {
sel->onClick(sel);
}
}
}
WidgetT *wgtTreeView(WidgetT *parent) { WidgetT *wgtTreeView(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetTreeViewE); WidgetT *w = widgetAlloc(parent, WidgetTreeViewE);
if (w) { if (w) {
w->weight = 100; w->weight = 100;
w->onDblClick = treeDefaultDblClick;
} }
return w; return w;
@ -802,7 +839,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
WidgetT *sel = w->as.treeView.selectedItem; WidgetT *sel = w->as.treeView.selectedItem;
if (key == (0x50 | 0x100)) { if (key == (0x50 | 0x100)) {
// Down arrow next visible item // Down arrow -- next visible item
if (!sel) { if (!sel) {
w->as.treeView.selectedItem = firstVisibleItem(w); w->as.treeView.selectedItem = firstVisibleItem(w);
} else { } else {
@ -813,7 +850,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == (0x48 | 0x100)) { } else if (key == (0x48 | 0x100)) {
// Up arrow previous visible item // Up arrow -- previous visible item
if (!sel) { if (!sel) {
w->as.treeView.selectedItem = firstVisibleItem(w); w->as.treeView.selectedItem = firstVisibleItem(w);
} else { } else {
@ -824,7 +861,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == (0x4D | 0x100)) { } else if (key == (0x4D | 0x100)) {
// Right arrow expand if collapsed, else move to first child // Right arrow -- expand if collapsed, else move to first child
if (sel) { if (sel) {
bool hasChildren = false; bool hasChildren = false;
@ -852,7 +889,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == (0x4B | 0x100)) { } else if (key == (0x4B | 0x100)) {
// Left arrow collapse if expanded, else move to parent // Left arrow -- collapse if expanded, else move to parent
if (sel) { if (sel) {
if (sel->as.treeItem.expanded) { if (sel->as.treeItem.expanded) {
sel->as.treeItem.expanded = false; sel->as.treeItem.expanded = false;
@ -865,7 +902,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == 0x0D) { } else if (key == 0x0D) {
// Enter toggle expand/collapse for parents, onClick for leaves // Enter -- toggle expand/collapse for parents, onClick for leaves
if (sel) { if (sel) {
bool hasChildren = false; bool hasChildren = false;
@ -891,7 +928,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} else if (key == ' ' && multi) { } else if (key == ' ' && multi) {
// Space toggle selection of current item in multi-select // Space -- toggle selection of current item in multi-select
if (sel) { if (sel) {
sel->as.treeItem.selected = !sel->as.treeItem.selected; sel->as.treeItem.selected = !sel->as.treeItem.selected;
w->as.treeView.anchorItem = sel; w->as.treeView.anchorItem = sel;
@ -1112,7 +1149,7 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
} }
} }
// Click in dead corner (both scrollbars present) ignore // Click in dead corner (both scrollbars present) -- ignore
if (needVSb && needHSb) { if (needVSb && needHSb) {
int32_t cornerX = hit->x + hit->w - TREE_BORDER - WGT_SB_W; int32_t cornerX = hit->x + hit->w - TREE_BORDER - WGT_SB_W;
int32_t cornerY = hit->y + hit->h - TREE_BORDER - WGT_SB_W; int32_t cornerY = hit->y + hit->h - TREE_BORDER - WGT_SB_W;
@ -1122,7 +1159,7 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
} }
} }
// Tree item click adjust for scroll offsets // Tree item click -- adjust for scroll offsets
int32_t curY = hit->y + TREE_BORDER - hit->as.treeView.scrollPos; int32_t curY = hit->y + TREE_BORDER - hit->as.treeView.scrollPos;
WidgetT *item = treeItemAtY(hit, vy, &curY, font); WidgetT *item = treeItemAtY(hit, vy, &curY, font);

View file

@ -27,10 +27,10 @@ Requires `lib/libdvx.a` and `lib/libtasks.a` to be built first.
Each iteration of the main loop: Each iteration of the main loop:
1. `dvxUpdate()` process input events, dispatch callbacks, composite dirty rects 1. `dvxUpdate()` -- process input events, dispatch callbacks, composite dirty rects
2. `tsYield()` give CPU time to main-loop app tasks 2. `tsYield()` -- give CPU time to main-loop app tasks
3. `shellReapApps()` clean up apps that terminated this frame 3. `shellReapApps()` -- clean up apps that terminated this frame
4. `desktopUpdate()` notify the desktop app if anything changed 4. `desktopUpdate()` -- notify the desktop app if anything changed
An idle callback (`idleYield`) yields to app tasks during quiet periods when An idle callback (`idleYield`) yields to app tasks during quiet periods when
there are no events or dirty rects to process. there are no events or dirty rects to process.

View file

@ -1,4 +1,4 @@
// shellApp.c DVX Shell application loading, lifecycle, and reaping // shellApp.c -- DVX Shell application loading, lifecycle, and reaping
// //
// Manages DXE app loading via dlopen/dlsym, resource tracking through // Manages DXE app loading via dlopen/dlsym, resource tracking through
// sCurrentAppId, and clean teardown of both callback-only and main-loop apps. // sCurrentAppId, and clean teardown of both callback-only and main-loop apps.
@ -8,9 +8,11 @@
#include "platform/dvxPlatform.h" #include "platform/dvxPlatform.h"
#include <dlfcn.h> #include <dlfcn.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <strings.h>
// ============================================================ // ============================================================
// Module state // Module state
@ -32,7 +34,7 @@ static int32_t allocSlot(void);
static void appTaskWrapper(void *arg); static void appTaskWrapper(void *arg);
static const char *baseName(const char *path); static const char *baseName(const char *path);
static void cleanupTempFile(ShellAppT *app); static void cleanupTempFile(ShellAppT *app);
static int32_t copyFile(AppContextT *ctx, const char *src, const char *dst); static int32_t copyFile(const char *src, const char *dst);
static ShellAppT *findLoadedPath(const char *path); static ShellAppT *findLoadedPath(const char *path);
static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize); static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
void shellAppInit(void); void shellAppInit(void);
@ -69,7 +71,7 @@ static int32_t allocSlot(void) {
// state is set to Terminating so the shell's main loop will reap it. // state is set to Terminating so the shell's main loop will reap it.
// //
// If the app crashes (signal), the signal handler longjmps to main and // If the app crashes (signal), the signal handler longjmps to main and
// this function never completes the task is killed externally via // this function never completes -- the task is killed externally via
// shellForceKillApp + tsKill. // shellForceKillApp + tsKill.
static void appTaskWrapper(void *arg) { static void appTaskWrapper(void *arg) {
ShellAppT *app = (ShellAppT *)arg; ShellAppT *app = (ShellAppT *)arg;
@ -78,7 +80,7 @@ static void appTaskWrapper(void *arg) {
app->entryFn(&app->dxeCtx); app->entryFn(&app->dxeCtx);
sCurrentAppId = 0; sCurrentAppId = 0;
// App returned from its main loop mark for reaping // App returned from its main loop -- mark for reaping
app->state = AppStateTerminatingE; app->state = AppStateTerminatingE;
} }
@ -104,16 +106,18 @@ static void cleanupTempFile(ShellAppT *app) {
// Binary file copy. Returns 0 on success, -1 on failure. // Binary file copy. Returns 0 on success, -1 on failure.
static int32_t copyFile(AppContextT *ctx, const char *src, const char *dst) { static int32_t copyFile(const char *src, const char *dst) {
FILE *in = fopen(src, "rb"); FILE *in = fopen(src, "rb");
if (!in) { if (!in) {
shellLog("copyFile: failed to open source '%s' (errno=%d)", src, errno);
return -1; return -1;
} }
FILE *out = fopen(dst, "wb"); FILE *out = fopen(dst, "wb");
if (!out) { if (!out) {
shellLog("copyFile: failed to create dest '%s' (errno=%d)", dst, errno);
fclose(in); fclose(in);
return -1; return -1;
} }
@ -123,14 +127,12 @@ static int32_t copyFile(AppContextT *ctx, const char *src, const char *dst) {
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
if (fwrite(buf, 1, n, out) != n) { if (fwrite(buf, 1, n, out) != n) {
shellLog("copyFile: write failed '%s' -> '%s' (errno=%d)", src, dst, errno);
fclose(in); fclose(in);
fclose(out); fclose(out);
remove(dst); remove(dst);
return -1; return -1;
} }
// Yield so the UI stays responsive during large copies
dvxUpdate(ctx);
} }
fclose(in); fclose(in);
@ -196,7 +198,25 @@ void shellAppInit(void) {
} }
// Forcible kill — no shutdown hook is called. Used for crashed apps // ============================================================
// shellConfigPath
// ============================================================
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize) {
snprintf(outPath, outSize, "%s/%s", ctx->configDir, filename);
}
// ============================================================
// shellEnsureConfigDir
// ============================================================
int32_t shellEnsureConfigDir(const DxeAppContextT *ctx) {
return platformMkdirRecursive(ctx->configDir);
}
// Forcible kill -- no shutdown hook is called. Used for crashed apps
// (where running more app code would be unsafe) and for "End Task". // (where running more app code would be unsafe) and for "End Task".
// Cleanup order matters: windows first (removes them from the compositor), // Cleanup order matters: windows first (removes them from the compositor),
// then the task (frees the stack), then the DXE handle (unmaps the code). // then the task (frees the stack), then the DXE handle (unmaps the code).
@ -248,14 +268,14 @@ ShellAppT *shellGetApp(int32_t appId) {
// Load a DXE app: dlopen the module, resolve symbols, set up context, launch. // Load a DXE app: dlopen the module, resolve symbols, set up context, launch.
// DXE3 is DJGPP's dynamic linking system similar to dlopen/dlsym on Unix. // DXE3 is DJGPP's dynamic linking system -- similar to dlopen/dlsym on Unix.
// Each .app file is a DXE3 shared object that exports _appDescriptor and // Each .app file is a DXE3 shared object that exports _appDescriptor and
// _appMain (and optionally _appShutdown). The leading underscore is the // _appMain (and optionally _appShutdown). The leading underscore is the
// COFF symbol convention; DJGPP's dlsym expects it. // COFF symbol convention; DJGPP's dlsym expects it.
// //
// Multi-instance support: DXE3's dlopen returns the same handle for the // Multi-instance support: DXE3's dlopen returns the same handle for the
// same path (reference-counted), so two loads of the same .dxe share all // same path (reference-counted), so two loads of the same .dxe share all
// global/static state only one main loop runs and closing one kills both. // global/static state -- only one main loop runs and closing one kills both.
// If the app's descriptor sets multiInstance=true, we copy the .dxe to a // If the app's descriptor sets multiInstance=true, we copy the .dxe to a
// unique temp file before dlopen, giving each instance its own code+data. // unique temp file before dlopen, giving each instance its own code+data.
// If multiInstance=false (the default), a second load of the same path is // If multiInstance=false (the default), a second load of the same path is
@ -271,7 +291,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// Check if this DXE is already loaded. If so, check whether the app // Check if this DXE is already loaded. If so, check whether the app
// allows multiple instances. We read the descriptor from the existing // allows multiple instances. We read the descriptor from the existing
// slot directly no need to dlopen again. // slot directly -- no need to dlopen again.
const char *loadPath = path; const char *loadPath = path;
char tempPath[260] = {0}; char tempPath[260] = {0};
ShellAppT *existing = findLoadedPath(path); ShellAppT *existing = findLoadedPath(path);
@ -291,7 +311,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// an independent code+data image. // an independent code+data image.
makeTempPath(path, id, tempPath, sizeof(tempPath)); makeTempPath(path, id, tempPath, sizeof(tempPath));
if (copyFile(ctx, path, tempPath) != 0) { if (copyFile(path, tempPath) != 0) {
char msg[320]; char msg[320];
snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", baseName(path)); snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", baseName(path));
dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR);
@ -381,6 +401,25 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
app->dxeCtx.appDir[1] = '\0'; app->dxeCtx.appDir[1] = '\0';
} }
// Derive config directory: replace the APPS/ prefix with CONFIG/.
// e.g. "APPS/GAMES/TETRIS" -> "CONFIG/GAMES/TETRIS"
// If the path doesn't start with "apps/" or "APPS/", fall back to
// "CONFIG/<appname>" using the descriptor name.
const char *appDirStr = app->dxeCtx.appDir;
const char *appsPrefix = NULL;
if (strncasecmp(appDirStr, "apps/", 5) == 0 || strncasecmp(appDirStr, "apps\\", 5) == 0) {
appsPrefix = appDirStr + 5;
} else if (strncasecmp(appDirStr, "APPS/", 5) == 0 || strncasecmp(appDirStr, "APPS\\", 5) == 0) {
appsPrefix = appDirStr + 5;
}
if (appsPrefix && appsPrefix[0]) {
snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", appsPrefix);
} else {
snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", desc->name);
}
// Launch. Set sCurrentAppId before any app code runs so that window // Launch. Set sCurrentAppId before any app code runs so that window
// creation wrappers stamp the correct owner. Reset to 0 afterward so // creation wrappers stamp the correct owner. Reset to 0 afterward so
// shell-initiated operations (e.g., message boxes) aren't misattributed. // shell-initiated operations (e.g., message boxes) aren't misattributed.
@ -418,7 +457,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
} }
// Graceful reap called from shellReapApps when an app has reached // Graceful reap -- called from shellReapApps when an app has reached
// the Terminating state. Unlike forceKill, this calls the app's // the Terminating state. Unlike forceKill, this calls the app's
// shutdown hook (if provided) giving it a chance to save state, close // shutdown hook (if provided) giving it a chance to save state, close
// files, etc. The sCurrentAppId is set during the shutdown call so // files, etc. The sCurrentAppId is set during the shutdown call so
@ -464,7 +503,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
// Called every frame from the shell's main loop. Scans for apps that have // Called every frame from the shell's main loop. Scans for apps that have
// entered the Terminating state (either their task returned or their last // entered the Terminating state (either their task returned or their last
// window was closed) and cleans them up. The deferred-reap design avoids // window was closed) and cleans them up. The deferred-reap design avoids
// destroying resources in the middle of a callback chain the app marks // destroying resources in the middle of a callback chain -- the app marks
// itself for termination, and cleanup happens at a safe top-level point. // itself for termination, and cleanup happens at a safe top-level point.
bool shellReapApps(AppContextT *ctx) { bool shellReapApps(AppContextT *ctx) {
bool reaped = false; bool reaped = false;

View file

@ -1,4 +1,4 @@
// shellApp.h DVX Shell application lifecycle types and API // shellApp.h -- DVX Shell application lifecycle types and API
// //
// The shell supports two kinds of DXE apps: // The shell supports two kinds of DXE apps:
// //
@ -61,12 +61,13 @@ typedef struct {
// the shell's GUI context (for creating windows, drawing, etc.) and // the shell's GUI context (for creating windows, drawing, etc.) and
// its own identity. appDir is derived from the .app file path at load // its own identity. appDir is derived from the .app file path at load
// time so the app can find its own resources (icons, data files) via // time so the app can find its own resources (icons, data files) via
// relative paths important because the working directory is shared // relative paths -- important because the working directory is shared
// by all apps and can't be changed per-app in DOS. // by all apps and can't be changed per-app in DOS.
typedef struct { typedef struct {
AppContextT *shellCtx; // the shell's GUI context AppContextT *shellCtx; // the shell's GUI context
int32_t appId; // this app's ID int32_t appId; // this app's ID
char appDir[260]; // directory containing the .app file char appDir[260]; // directory containing the .app file
char configDir[260]; // writable config directory (CONFIG/<apppath>/)
} DxeAppContextT; } DxeAppContextT;
// ============================================================ // ============================================================
@ -76,7 +77,7 @@ typedef struct {
// State machine: Free -> Loaded -> Running -> Terminating -> Free // State machine: Free -> Loaded -> Running -> Terminating -> Free
// LoadedE is transient (only during shellLoadApp before entry is called). // LoadedE is transient (only during shellLoadApp before entry is called).
// TerminatingE means the app's task has exited but cleanup (window // TerminatingE means the app's task has exited but cleanup (window
// destruction, DXE unload) hasn't happened yet the shell's main loop // destruction, DXE unload) hasn't happened yet -- the shell's main loop
// reaps these each frame via shellReapApps(). // reaps these each frame via shellReapApps().
typedef enum { typedef enum {
AppStateFreeE, // slot available AppStateFreeE, // slot available
@ -132,11 +133,11 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path);
// when appMain returns (via appTaskWrapper marking AppStateTerminatingE). // when appMain returns (via appTaskWrapper marking AppStateTerminatingE).
bool shellReapApps(AppContextT *ctx); bool shellReapApps(AppContextT *ctx);
// Gracefully shut down a single app calls shutdownFn if present, // Gracefully shut down a single app -- calls shutdownFn if present,
// destroys windows, kills task, closes DXE handle. // destroys windows, kills task, closes DXE handle.
void shellReapApp(AppContextT *ctx, ShellAppT *app); void shellReapApp(AppContextT *ctx, ShellAppT *app);
// Forcibly kill an app (Task Manager "End Task"). Skips shutdownFn // Forcibly kill an app (Task Manager "End Task"). Skips shutdownFn --
// used when the app is hung or has crashed and cannot be trusted to // used when the app is hung or has crashed and cannot be trusted to
// run its own cleanup code. // run its own cleanup code.
void shellForceKillApp(AppContextT *ctx, ShellAppT *app); void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
@ -157,6 +158,15 @@ int32_t shellRunningAppCount(void);
// Write a printf-style message to DVX.LOG // Write a printf-style message to DVX.LOG
void shellLog(const char *fmt, ...); void shellLog(const char *fmt, ...);
// Ensure an app's config directory exists (creates all parent dirs).
// Returns 0 on success, -1 on failure.
int32_t shellEnsureConfigDir(const DxeAppContextT *ctx);
// Build a full path to a file in the app's config directory.
// e.g. shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))
// -> "CONFIG/PROGMAN/settings.ini"
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);
// ============================================================ // ============================================================
// DXE export table // DXE export table
// ============================================================ // ============================================================

View file

@ -1,4 +1,4 @@
// shellExport.c DXE export table and wrapper functions for DVX Shell // shellExport.c -- DXE export table and wrapper functions for DVX Shell
// //
// Exports all dvx*/wgt*/ts* symbols that DXE apps need. A few functions // Exports all dvx*/wgt*/ts* symbols that DXE apps need. A few functions
// are wrapped for resource tracking (window ownership via appId). // are wrapped for resource tracking (window ownership via appId).
@ -15,7 +15,7 @@
// 1. Wrapped functions: dvxCreateWindow, dvxCreateWindowCentered, // 1. Wrapped functions: dvxCreateWindow, dvxCreateWindowCentered,
// dvxDestroyWindow. These are intercepted to stamp win->appId for // dvxDestroyWindow. These are intercepted to stamp win->appId for
// resource ownership tracking. The DXE sees them under their original // resource ownership tracking. The DXE sees them under their original
// names the app code calls dvxCreateWindow() normally and gets our // names -- the app code calls dvxCreateWindow() normally and gets our
// wrapper transparently. // wrapper transparently.
// //
// 2. Direct exports: all other dvx/wgt/wm/ts functions. These are safe // 2. Direct exports: all other dvx/wgt/wm/ts functions. These are safe
@ -25,7 +25,7 @@
// libc, but DJGPP's DXE3 loader requires explicit re-export of any // libc, but DJGPP's DXE3 loader requires explicit re-export of any
// libc symbols the module references. Without these entries, the DXE // libc symbols the module references. Without these entries, the DXE
// would fail to load with unresolved symbol errors. This is a DXE3 // would fail to load with unresolved symbol errors. This is a DXE3
// design limitation there's no automatic fallback to the host's libc. // design limitation -- there's no automatic fallback to the host's libc.
#include "shellApp.h" #include "shellApp.h"
#include "shellInfo.h" #include "shellInfo.h"
@ -55,12 +55,12 @@
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
// stb headers (no IMPLEMENTATION symbols are in libdvx.a / libtasks.a) // stb headers (no IMPLEMENTATION -- symbols are in libdvx.a / libtasks.a)
#include "thirdparty/stb_image.h" #include "thirdparty/stb_image.h"
#include "thirdparty/stb_image_write.h" #include "thirdparty/stb_image_write.h"
#include "thirdparty/stb_ds.h" #include "thirdparty/stb_ds.h"
// DJGPP stdio internals the stdin/stdout/stderr macros dereference // DJGPP stdio internals -- the stdin/stdout/stderr macros dereference
// these pointers. Without exporting them, any DXE that uses printf // these pointers. Without exporting them, any DXE that uses printf
// or fprintf gets an unresolved symbol. // or fprintf gets an unresolved symbol.
extern FILE __dj_stdin; extern FILE __dj_stdin;
@ -85,13 +85,13 @@ static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *titl
static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win); static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win);
// ============================================================ // ============================================================
// Wrapper: dvxCreateWindow stamps win->appId // Wrapper: dvxCreateWindow -- stamps win->appId
// ============================================================ // ============================================================
// The wrapper calls the real dvxCreateWindow, then tags the result with // The wrapper calls the real dvxCreateWindow, then tags the result with
// sCurrentAppId. This is how the shell knows which app owns which window, // sCurrentAppId. This is how the shell knows which app owns which window,
// enabling per-app window cleanup on crash/termination. The app never // enabling per-app window cleanup on crash/termination. The app never
// sees the difference the wrapper has the same signature and is // sees the difference -- the wrapper has the same signature and is
// exported under the same name as the original function. // exported under the same name as the original function.
static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable); WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable);
@ -105,7 +105,7 @@ static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32
// ============================================================ // ============================================================
// Wrapper: dvxCreateWindowCentered stamps win->appId // Wrapper: dvxCreateWindowCentered -- stamps win->appId
// ============================================================ // ============================================================
static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) { static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) {
@ -120,7 +120,7 @@ static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *titl
// ============================================================ // ============================================================
// Wrapper: dvxDestroyWindow checks for last-window reap // Wrapper: dvxDestroyWindow -- checks for last-window reap
// ============================================================ // ============================================================
// Beyond just destroying the window, this wrapper implements the lifecycle // Beyond just destroying the window, this wrapper implements the lifecycle
@ -162,7 +162,7 @@ static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) {
// ============================================================ // ============================================================
// DXE_EXPORT_TABLE generates a DXE symbol table array. DXE_EXPORT(fn) // DXE_EXPORT_TABLE generates a DXE symbol table array. DXE_EXPORT(fn)
// expands to { "_fn", (void *)fn } the underscore prefix matches COFF // expands to { "_fn", (void *)fn } -- the underscore prefix matches COFF
// symbol naming. For wrapped functions we use raw entries with explicit // symbol naming. For wrapped functions we use raw entries with explicit
// names so the DXE sees "_dvxCreateWindow" but gets our wrapper's address. // names so the DXE sees "_dvxCreateWindow" but gets our wrapper's address.
@ -172,10 +172,11 @@ DXE_EXPORT_TABLE(shellExportTable)
{ "_dvxCreateWindow", (void *)shellWrapCreateWindow }, { "_dvxCreateWindow", (void *)shellWrapCreateWindow },
{ "_dvxDestroyWindow", (void *)shellWrapDestroyWindow }, { "_dvxDestroyWindow", (void *)shellWrapDestroyWindow },
// dvxPlatform.h platform abstraction // dvxPlatform.h -- platform abstraction
DXE_EXPORT(platformLineEnding) DXE_EXPORT(platformLineEnding)
DXE_EXPORT(platformChdir) DXE_EXPORT(platformChdir)
DXE_EXPORT(platformGetMemoryInfo) DXE_EXPORT(platformGetMemoryInfo)
DXE_EXPORT(platformMkdirRecursive)
DXE_EXPORT(platformMouseSetAccel) DXE_EXPORT(platformMouseSetAccel)
DXE_EXPORT(platformMouseWarp) DXE_EXPORT(platformMouseWarp)
DXE_EXPORT(platformPathDirEnd) DXE_EXPORT(platformPathDirEnd)
@ -183,7 +184,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(platformValidateFilename) DXE_EXPORT(platformValidateFilename)
DXE_EXPORT(platformVideoEnumModes) DXE_EXPORT(platformVideoEnumModes)
// dvxPrefs.h preferences // dvxPrefs.h -- preferences
DXE_EXPORT(prefsGetBool) DXE_EXPORT(prefsGetBool)
DXE_EXPORT(prefsGetInt) DXE_EXPORT(prefsGetInt)
DXE_EXPORT(prefsGetString) DXE_EXPORT(prefsGetString)
@ -195,9 +196,10 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(prefsSetInt) DXE_EXPORT(prefsSetInt)
DXE_EXPORT(prefsSetString) DXE_EXPORT(prefsSetString)
// dvxApp.h direct exports // dvxApp.h -- direct exports
DXE_EXPORT(dvxApplyColorScheme) DXE_EXPORT(dvxApplyColorScheme)
DXE_EXPORT(dvxChangeVideoMode) DXE_EXPORT(dvxChangeVideoMode)
DXE_EXPORT(dvxColorLabel)
DXE_EXPORT(dvxColorName) DXE_EXPORT(dvxColorName)
DXE_EXPORT(dvxGetColor) DXE_EXPORT(dvxGetColor)
DXE_EXPORT(dvxInit) DXE_EXPORT(dvxInit)
@ -207,6 +209,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(dvxSetColor) DXE_EXPORT(dvxSetColor)
DXE_EXPORT(dvxSetMouseConfig) DXE_EXPORT(dvxSetMouseConfig)
DXE_EXPORT(dvxSetWallpaper) DXE_EXPORT(dvxSetWallpaper)
DXE_EXPORT(dvxSetWallpaperMode)
DXE_EXPORT(dvxShutdown) DXE_EXPORT(dvxShutdown)
DXE_EXPORT(dvxUpdate) DXE_EXPORT(dvxUpdate)
{ "_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered }, { "_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered },
@ -280,36 +283,37 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wmUpdateContentRect) DXE_EXPORT(wmUpdateContentRect)
DXE_EXPORT(wmReallocContentBuf) DXE_EXPORT(wmReallocContentBuf)
// dvxWidget.h window integration // dvxWidget.h -- window integration
DXE_EXPORT(wgtInitWindow) DXE_EXPORT(wgtInitWindow)
// dvxWidget.h containers // dvxWidget.h -- containers
DXE_EXPORT(wgtVBox) DXE_EXPORT(wgtVBox)
DXE_EXPORT(wgtHBox) DXE_EXPORT(wgtHBox)
DXE_EXPORT(wgtFrame) DXE_EXPORT(wgtFrame)
// dvxWidget.h basic widgets // dvxWidget.h -- basic widgets
DXE_EXPORT(wgtLabel) DXE_EXPORT(wgtLabel)
DXE_EXPORT(wgtLabelSetAlign)
DXE_EXPORT(wgtButton) DXE_EXPORT(wgtButton)
DXE_EXPORT(wgtCheckbox) DXE_EXPORT(wgtCheckbox)
DXE_EXPORT(wgtTextInput) DXE_EXPORT(wgtTextInput)
DXE_EXPORT(wgtPasswordInput) DXE_EXPORT(wgtPasswordInput)
DXE_EXPORT(wgtMaskedInput) DXE_EXPORT(wgtMaskedInput)
// dvxWidget.h radio buttons // dvxWidget.h -- radio buttons
DXE_EXPORT(wgtRadioGroup) DXE_EXPORT(wgtRadioGroup)
DXE_EXPORT(wgtRadio) DXE_EXPORT(wgtRadio)
// dvxWidget.h spacing // dvxWidget.h -- spacing
DXE_EXPORT(wgtSpacer) DXE_EXPORT(wgtSpacer)
DXE_EXPORT(wgtHSeparator) DXE_EXPORT(wgtHSeparator)
DXE_EXPORT(wgtVSeparator) DXE_EXPORT(wgtVSeparator)
// dvxWidget.h complex widgets // dvxWidget.h -- complex widgets
DXE_EXPORT(wgtListBox) DXE_EXPORT(wgtListBox)
DXE_EXPORT(wgtTextArea) DXE_EXPORT(wgtTextArea)
// dvxWidget.h dropdown/combo // dvxWidget.h -- dropdown/combo
DXE_EXPORT(wgtDropdown) DXE_EXPORT(wgtDropdown)
DXE_EXPORT(wgtDropdownSetItems) DXE_EXPORT(wgtDropdownSetItems)
DXE_EXPORT(wgtDropdownGetSelected) DXE_EXPORT(wgtDropdownGetSelected)
@ -319,35 +323,35 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtComboBoxGetSelected) DXE_EXPORT(wgtComboBoxGetSelected)
DXE_EXPORT(wgtComboBoxSetSelected) DXE_EXPORT(wgtComboBoxSetSelected)
// dvxWidget.h progress bar // dvxWidget.h -- progress bar
DXE_EXPORT(wgtProgressBar) DXE_EXPORT(wgtProgressBar)
DXE_EXPORT(wgtProgressBarV) DXE_EXPORT(wgtProgressBarV)
DXE_EXPORT(wgtProgressBarSetValue) DXE_EXPORT(wgtProgressBarSetValue)
DXE_EXPORT(wgtProgressBarGetValue) DXE_EXPORT(wgtProgressBarGetValue)
// dvxWidget.h slider // dvxWidget.h -- slider
DXE_EXPORT(wgtSlider) DXE_EXPORT(wgtSlider)
DXE_EXPORT(wgtSliderSetValue) DXE_EXPORT(wgtSliderSetValue)
DXE_EXPORT(wgtSliderGetValue) DXE_EXPORT(wgtSliderGetValue)
// dvxWidget.h spinner // dvxWidget.h -- spinner
DXE_EXPORT(wgtSpinner) DXE_EXPORT(wgtSpinner)
DXE_EXPORT(wgtSpinnerSetValue) DXE_EXPORT(wgtSpinnerSetValue)
DXE_EXPORT(wgtSpinnerGetValue) DXE_EXPORT(wgtSpinnerGetValue)
DXE_EXPORT(wgtSpinnerSetRange) DXE_EXPORT(wgtSpinnerSetRange)
DXE_EXPORT(wgtSpinnerSetStep) DXE_EXPORT(wgtSpinnerSetStep)
// dvxWidget.h tab control // dvxWidget.h -- tab control
DXE_EXPORT(wgtTabControl) DXE_EXPORT(wgtTabControl)
DXE_EXPORT(wgtTabPage) DXE_EXPORT(wgtTabPage)
DXE_EXPORT(wgtTabControlSetActive) DXE_EXPORT(wgtTabControlSetActive)
DXE_EXPORT(wgtTabControlGetActive) DXE_EXPORT(wgtTabControlGetActive)
// dvxWidget.h status bar / toolbar // dvxWidget.h -- status bar / toolbar
DXE_EXPORT(wgtStatusBar) DXE_EXPORT(wgtStatusBar)
DXE_EXPORT(wgtToolbar) DXE_EXPORT(wgtToolbar)
// dvxWidget.h tree view // dvxWidget.h -- tree view
DXE_EXPORT(wgtTreeView) DXE_EXPORT(wgtTreeView)
DXE_EXPORT(wgtTreeViewGetSelected) DXE_EXPORT(wgtTreeViewGetSelected)
DXE_EXPORT(wgtTreeViewSetSelected) DXE_EXPORT(wgtTreeViewSetSelected)
@ -359,14 +363,14 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtTreeItemIsSelected) DXE_EXPORT(wgtTreeItemIsSelected)
DXE_EXPORT(wgtTreeItemSetSelected) DXE_EXPORT(wgtTreeItemSetSelected)
// dvxWidget.h timer // dvxWidget.h -- timer
DXE_EXPORT(wgtTimer) DXE_EXPORT(wgtTimer)
DXE_EXPORT(wgtTimerIsRunning) DXE_EXPORT(wgtTimerIsRunning)
DXE_EXPORT(wgtTimerSetInterval) DXE_EXPORT(wgtTimerSetInterval)
DXE_EXPORT(wgtTimerStart) DXE_EXPORT(wgtTimerStart)
DXE_EXPORT(wgtTimerStop) DXE_EXPORT(wgtTimerStop)
// dvxWidget.h list view // dvxWidget.h -- list view
DXE_EXPORT(wgtListView) DXE_EXPORT(wgtListView)
DXE_EXPORT(wgtListViewSetColumns) DXE_EXPORT(wgtListViewSetColumns)
DXE_EXPORT(wgtListViewSetData) DXE_EXPORT(wgtListViewSetData)
@ -381,13 +385,13 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtListViewClearSelection) DXE_EXPORT(wgtListViewClearSelection)
DXE_EXPORT(wgtListViewSetReorderable) DXE_EXPORT(wgtListViewSetReorderable)
// dvxWidget.h scroll pane / splitter // dvxWidget.h -- scroll pane / splitter
DXE_EXPORT(wgtScrollPane) DXE_EXPORT(wgtScrollPane)
DXE_EXPORT(wgtSplitter) DXE_EXPORT(wgtSplitter)
DXE_EXPORT(wgtSplitterSetPos) DXE_EXPORT(wgtSplitterSetPos)
DXE_EXPORT(wgtSplitterGetPos) DXE_EXPORT(wgtSplitterGetPos)
// dvxWidget.h image / image button // dvxWidget.h -- image / image button
DXE_EXPORT(wgtImageButton) DXE_EXPORT(wgtImageButton)
DXE_EXPORT(wgtImageButtonFromFile) DXE_EXPORT(wgtImageButtonFromFile)
DXE_EXPORT(wgtImageButtonSetData) DXE_EXPORT(wgtImageButtonSetData)
@ -395,7 +399,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtImageFromFile) DXE_EXPORT(wgtImageFromFile)
DXE_EXPORT(wgtImageSetData) DXE_EXPORT(wgtImageSetData)
// dvxWidget.h canvas // dvxWidget.h -- canvas
DXE_EXPORT(wgtCanvas) DXE_EXPORT(wgtCanvas)
DXE_EXPORT(wgtCanvasClear) DXE_EXPORT(wgtCanvasClear)
DXE_EXPORT(wgtCanvasSetMouseCallback) DXE_EXPORT(wgtCanvasSetMouseCallback)
@ -410,7 +414,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtCanvasSetPixel) DXE_EXPORT(wgtCanvasSetPixel)
DXE_EXPORT(wgtCanvasGetPixel) DXE_EXPORT(wgtCanvasGetPixel)
// dvxWidget.h ANSI terminal // dvxWidget.h -- ANSI terminal
DXE_EXPORT(wgtAnsiTerm) DXE_EXPORT(wgtAnsiTerm)
DXE_EXPORT(wgtAnsiTermWrite) DXE_EXPORT(wgtAnsiTermWrite)
DXE_EXPORT(wgtAnsiTermClear) DXE_EXPORT(wgtAnsiTermClear)
@ -419,7 +423,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtAnsiTermPoll) DXE_EXPORT(wgtAnsiTermPoll)
DXE_EXPORT(wgtAnsiTermRepaint) DXE_EXPORT(wgtAnsiTermRepaint)
// dvxWidget.h operations // dvxWidget.h -- operations
DXE_EXPORT(wgtInvalidate) DXE_EXPORT(wgtInvalidate)
DXE_EXPORT(wgtInvalidatePaint) DXE_EXPORT(wgtInvalidatePaint)
DXE_EXPORT(wgtSetDebugLayout) DXE_EXPORT(wgtSetDebugLayout)
@ -436,7 +440,7 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtFind) DXE_EXPORT(wgtFind)
DXE_EXPORT(wgtDestroy) DXE_EXPORT(wgtDestroy)
// dvxWidget.h list box ops // dvxWidget.h -- list box ops
DXE_EXPORT(wgtListBoxSetItems) DXE_EXPORT(wgtListBoxSetItems)
DXE_EXPORT(wgtListBoxGetSelected) DXE_EXPORT(wgtListBoxGetSelected)
DXE_EXPORT(wgtListBoxSetSelected) DXE_EXPORT(wgtListBoxSetSelected)
@ -447,14 +451,14 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(wgtListBoxClearSelection) DXE_EXPORT(wgtListBoxClearSelection)
DXE_EXPORT(wgtListBoxSetReorderable) DXE_EXPORT(wgtListBoxSetReorderable)
// dvxWidget.h layout // dvxWidget.h -- layout
DXE_EXPORT(wgtResolveSize) DXE_EXPORT(wgtResolveSize)
DXE_EXPORT(wgtLayout) DXE_EXPORT(wgtLayout)
DXE_EXPORT(wgtPaint) DXE_EXPORT(wgtPaint)
// taskswitch.h only yield and query functions are exported. // taskswitch.h -- only yield and query functions are exported.
// tsCreate/tsKill/etc. are NOT exported because apps should not // tsCreate/tsKill/etc. are NOT exported because apps should not
// manipulate the task system directly the shell manages task // manipulate the task system directly -- the shell manages task
// lifecycle through shellLoadApp/shellForceKillApp. // lifecycle through shellLoadApp/shellForceKillApp.
DXE_EXPORT(tsActiveCount) DXE_EXPORT(tsActiveCount)
DXE_EXPORT(tsCreate) DXE_EXPORT(tsCreate)
@ -468,15 +472,17 @@ DXE_EXPORT_TABLE(shellExportTable)
DXE_EXPORT(tsSetPriority) DXE_EXPORT(tsSetPriority)
DXE_EXPORT(tsYield) DXE_EXPORT(tsYield)
// dvxWm.h direct window management // dvxWm.h -- direct window management
DXE_EXPORT(wmRaiseWindow) DXE_EXPORT(wmRaiseWindow)
DXE_EXPORT(wmSetFocus) DXE_EXPORT(wmSetFocus)
DXE_EXPORT(wmRestoreMinimized) DXE_EXPORT(wmRestoreMinimized)
// Shell API // Shell API
DXE_EXPORT(shellLog) DXE_EXPORT(shellConfigPath)
DXE_EXPORT(shellLoadApp) DXE_EXPORT(shellEnsureConfigDir)
DXE_EXPORT(shellGetApp) DXE_EXPORT(shellGetApp)
DXE_EXPORT(shellLoadApp)
DXE_EXPORT(shellLog)
DXE_EXPORT(shellTaskMgrOpen) DXE_EXPORT(shellTaskMgrOpen)
DXE_EXPORT(shellForceKillApp) DXE_EXPORT(shellForceKillApp)
DXE_EXPORT(shellRunningAppCount) DXE_EXPORT(shellRunningAppCount)
@ -711,7 +717,7 @@ DXE_EXPORT_END
// ============================================================ // ============================================================
// dlregsym registers our export table with DJGPP's DXE3 runtime. // dlregsym registers our export table with DJGPP's DXE3 runtime.
// Must be called once before any dlopen subsequent dlopen calls // Must be called once before any dlopen -- subsequent dlopen calls
// will search this table to resolve DXE symbol references. // will search this table to resolve DXE symbol references.
static void shellRegisterExports(void) { static void shellRegisterExports(void) {
dlregsym(shellExportTable); dlregsym(shellExportTable);

View file

@ -1,4 +1,4 @@
// shellInfo.c System information wrapper for DVX Shell // shellInfo.c -- System information wrapper for DVX Shell
// //
// Delegates hardware detection to the platform layer via // Delegates hardware detection to the platform layer via
// platformGetSystemInfo(), then logs the result line-by-line // platformGetSystemInfo(), then logs the result line-by-line
@ -18,7 +18,7 @@
static const char *sCachedInfo = NULL; static const char *sCachedInfo = NULL;
// ============================================================ // ============================================================
// shellGetSystemInfo return the cached info text // shellGetSystemInfo -- return the cached info text
// ============================================================ // ============================================================
const char *shellGetSystemInfo(void) { const char *shellGetSystemInfo(void) {
@ -27,7 +27,7 @@ const char *shellGetSystemInfo(void) {
// ============================================================ // ============================================================
// shellInfoInit gather info and log it // shellInfoInit -- gather info and log it
// ============================================================ // ============================================================
void shellInfoInit(AppContextT *ctx) { void shellInfoInit(AppContextT *ctx) {

View file

@ -1,4 +1,4 @@
// shellInfo.h System information display for DVX Shell // shellInfo.h -- System information display for DVX Shell
// //
// Thin wrapper around platformGetSystemInfo(). Calls the platform // Thin wrapper around platformGetSystemInfo(). Calls the platform
// layer to gather hardware info, logs each line to DVX.LOG, and // layer to gather hardware info, logs each line to DVX.LOG, and

View file

@ -1,4 +1,4 @@
// shellMain.c DVX Shell entry point and main loop // shellMain.c -- DVX Shell entry point and main loop
// //
// Initializes the GUI, task system, DXE export table, and loads // Initializes the GUI, task system, DXE export table, and loads
// the desktop app. Runs the cooperative main loop, yielding to // the desktop app. Runs the cooperative main loop, yielding to
@ -21,7 +21,7 @@
// point in main(). This works because longjmp restores the main task's // point in main(). This works because longjmp restores the main task's
// stack frame regardless of which task was running. tsRecoverToMain() // stack frame regardless of which task was running. tsRecoverToMain()
// then fixes the scheduler's bookkeeping, and the crashed app is killed. // then fixes the scheduler's bookkeeping, and the crashed app is killed.
// This gives the shell Windows 3.1-style fault tolerance one bad app // This gives the shell Windows 3.1-style fault tolerance -- one bad app
// doesn't take down the whole system. // doesn't take down the whole system.
#include "shellApp.h" #include "shellApp.h"
@ -68,7 +68,7 @@ static void logCrash(int sig);
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData); static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
// ============================================================ // ============================================================
// crashHandler catch page faults and other fatal signals // crashHandler -- catch page faults and other fatal signals
// ============================================================ // ============================================================
// Signal handler for fatal exceptions. DJGPP uses System V signal // Signal handler for fatal exceptions. DJGPP uses System V signal
@ -79,7 +79,7 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
// we're on (potentially a crashed app's task stack) and restores the // we're on (potentially a crashed app's task stack) and restores the
// main task's stack frame to the setjmp point in main(). This is safe // main task's stack frame to the setjmp point in main(). This is safe
// because cooperative switching means the main task's stack is always // because cooperative switching means the main task's stack is always
// intact it was cleanly suspended at a yield point. The crashed // intact -- it was cleanly suspended at a yield point. The crashed
// task's stack is abandoned (and later freed by tsKill). // task's stack is abandoned (and later freed by tsKill).
static void crashHandler(int sig) { static void crashHandler(int sig) {
logCrash(sig); logCrash(sig);
@ -93,7 +93,7 @@ static void crashHandler(int sig) {
// ============================================================ // ============================================================
// shellDesktopUpdate notify desktop app of state change // shellDesktopUpdate -- notify desktop app of state change
// ============================================================ // ============================================================
void shellDesktopUpdate(void) { void shellDesktopUpdate(void) {
@ -104,7 +104,7 @@ void shellDesktopUpdate(void) {
// ============================================================ // ============================================================
// ctrlEscHandler system-wide Ctrl+Esc callback // ctrlEscHandler -- system-wide Ctrl+Esc callback
// ============================================================ // ============================================================
static void ctrlEscHandler(void *ctx) { static void ctrlEscHandler(void *ctx) {
@ -113,7 +113,7 @@ static void ctrlEscHandler(void *ctx) {
// ============================================================ // ============================================================
// titleChangeHandler refresh listeners when a window title changes // titleChangeHandler -- refresh listeners when a window title changes
// ============================================================ // ============================================================
static void titleChangeHandler(void *ctx) { static void titleChangeHandler(void *ctx) {
@ -123,12 +123,12 @@ static void titleChangeHandler(void *ctx) {
// ============================================================ // ============================================================
// idleYield called when no dirty rects need compositing // idleYield -- called when no dirty rects need compositing
// ============================================================ // ============================================================
// Registered as sCtx.idleCallback. dvxUpdate calls this when it has // Registered as sCtx.idleCallback. dvxUpdate calls this when it has
// processed all pending events and there are no dirty rects to composite. // processed all pending events and there are no dirty rects to composite.
// Instead of busy-spinning, we yield to app tasks this is where most // Instead of busy-spinning, we yield to app tasks -- this is where most
// of the CPU time for main-loop apps comes from when the UI is idle. // of the CPU time for main-loop apps comes from when the UI is idle.
// The tsActiveCount > 1 check avoids the overhead of a tsYield call // The tsActiveCount > 1 check avoids the overhead of a tsYield call
// (which would do a scheduler scan) when the shell is the only task. // (which would do a scheduler scan) when the shell is the only task.
@ -153,12 +153,12 @@ static void installCrashHandler(void) {
// ============================================================ // ============================================================
// logCrash write exception details to the log // logCrash -- write exception details to the log
// ============================================================ // ============================================================
// Dump as much diagnostic info as possible before longjmp destroys the // Dump as much diagnostic info as possible before longjmp destroys the
// crash context. This runs inside the signal handler, so only // crash context. This runs inside the signal handler, so only
// async-signal-safe functions should be used but since we're in // async-signal-safe functions should be used -- but since we're in
// DJGPP (single-threaded DOS), reentrancy isn't a practical concern // DJGPP (single-threaded DOS), reentrancy isn't a practical concern
// and vfprintf/fflush are safe to call here. // and vfprintf/fflush are safe to call here.
static void logCrash(int sig) { static void logCrash(int sig) {
@ -191,7 +191,7 @@ static void logCrash(int sig) {
// __djgpp_exception_state_ptr is a DJGPP extension that captures the // __djgpp_exception_state_ptr is a DJGPP extension that captures the
// full CPU register state at the point of the exception. This gives // full CPU register state at the point of the exception. This gives
// us the faulting EIP, stack pointer, and all GPRs invaluable for // us the faulting EIP, stack pointer, and all GPRs -- invaluable for
// post-mortem debugging of app crashes from the log file. // post-mortem debugging of app crashes from the log file.
jmp_buf *estate = __djgpp_exception_state_ptr; jmp_buf *estate = __djgpp_exception_state_ptr;
@ -213,7 +213,7 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
// ============================================================ // ============================================================
// shellLog append a line to DVX.LOG // shellLog -- append a line to DVX.LOG
// ============================================================ // ============================================================
void shellLog(const char *fmt, ...) { void shellLog(const char *fmt, ...) {
@ -297,7 +297,7 @@ int main(int argc, char *argv[]) {
int32_t videoW = prefsGetInt("video", "width", 640); int32_t videoW = prefsGetInt("video", "width", 640);
int32_t videoH = prefsGetInt("video", "height", 480); int32_t videoH = prefsGetInt("video", "height", 480);
int32_t videoBpp = prefsGetInt("video", "bpp", 32); int32_t videoBpp = prefsGetInt("video", "bpp", 16);
shellLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); shellLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp);
// Initialize GUI // Initialize GUI
@ -352,12 +352,22 @@ int main(int argc, char *argv[]) {
shellLog("Preferences: loaded custom color scheme"); shellLog("Preferences: loaded custom color scheme");
} }
// Apply saved wallpaper // Apply saved wallpaper mode and image
const char *wpMode = prefsGetString("desktop", "mode", "stretch");
if (strcmp(wpMode, "tile") == 0) {
sCtx.wallpaperMode = WallpaperTileE;
} else if (strcmp(wpMode, "center") == 0) {
sCtx.wallpaperMode = WallpaperCenterE;
} else {
sCtx.wallpaperMode = WallpaperStretchE;
}
const char *wpPath = prefsGetString("desktop", "wallpaper", NULL); const char *wpPath = prefsGetString("desktop", "wallpaper", NULL);
if (wpPath) { if (wpPath) {
if (dvxSetWallpaper(&sCtx, wpPath)) { if (dvxSetWallpaper(&sCtx, wpPath)) {
shellLog("Preferences: loaded wallpaper %s", wpPath); shellLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode);
} else { } else {
shellLog("Preferences: failed to load wallpaper %s", wpPath); shellLog("Preferences: failed to load wallpaper %s", wpPath);
} }
@ -416,7 +426,7 @@ int main(int argc, char *argv[]) {
return 1; return 1;
} }
// Install crash handler after everything is initialized if // Install crash handler after everything is initialized -- if
// initialization itself crashes, we want the default DJGPP behavior // initialization itself crashes, we want the default DJGPP behavior
// (abort with register dump) rather than our recovery path, because // (abort with register dump) rather than our recovery path, because
// the system isn't in a recoverable state yet. // the system isn't in a recoverable state yet.
@ -454,7 +464,7 @@ int main(int argc, char *argv[]) {
shellDesktopUpdate(); shellDesktopUpdate();
} }
// Main loop runs until dvxQuit() sets sCtx.running = false. // Main loop -- runs until dvxQuit() sets sCtx.running = false.
// Two yield points per iteration: one explicit (below) and one via // Two yield points per iteration: one explicit (below) and one via
// the idle callback inside dvxUpdate. The explicit yield here ensures // the idle callback inside dvxUpdate. The explicit yield here ensures
// app tasks get CPU time even during busy frames (lots of repaints). // app tasks get CPU time even during busy frames (lots of repaints).
@ -469,7 +479,7 @@ int main(int argc, char *argv[]) {
} }
// Reap terminated apps and notify desktop if anything changed. // Reap terminated apps and notify desktop if anything changed.
// This is the safe point for cleanup we're at the top of the // This is the safe point for cleanup -- we're at the top of the
// main loop, not inside any callback or compositor operation. // main loop, not inside any callback or compositor operation.
if (shellReapApps(&sCtx)) { if (shellReapApps(&sCtx)) {
shellDesktopUpdate(); shellDesktopUpdate();

View file

@ -1,4 +1,4 @@
// shellTaskMgr.c System Task Manager // shellTaskMgr.c -- System Task Manager
// //
// Shell-level Task Manager window. Accessible via Ctrl+Esc regardless // Shell-level Task Manager window. Accessible via Ctrl+Esc regardless
// of which app is focused or whether the desktop app is running. Lists // of which app is focused or whether the desktop app is running. Lists
@ -271,7 +271,7 @@ void shellTaskMgrOpen(AppContextT *ctx) {
sCtx = ctx; sCtx = ctx;
if (sTmWindow) { if (sTmWindow) {
// Already open raise and focus // Already open -- raise and focus
for (int32_t i = 0; i < ctx->stack.count; i++) { for (int32_t i = 0; i < ctx->stack.count; i++) {
if (ctx->stack.windows[i] == sTmWindow) { if (ctx->stack.windows[i] == sTmWindow) {
wmRaiseWindow(&ctx->stack, &ctx->dirty, i); wmRaiseWindow(&ctx->stack, &ctx->dirty, i);

View file

@ -1,4 +1,4 @@
// shellTaskMgr.h System Task Manager // shellTaskMgr.h -- System Task Manager
// //
// The Task Manager is a shell-level component, not tied to any app. // The Task Manager is a shell-level component, not tied to any app.
// It is always available via Ctrl+Esc and persists even if the desktop // It is always available via Ctrl+Esc and persists even if the desktop

View file

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# mkcd.sh Build DVX and create a CD-ROM ISO image for 86Box # mkcd.sh -- Build DVX and create a CD-ROM ISO image for 86Box
# #
# Usage: ./mkcd.sh # Usage: ./mkcd.sh
# #
@ -22,7 +22,7 @@ make -C "$SCRIPT_DIR" all
# Verify build output exists # Verify build output exists
if [ ! -f "$SCRIPT_DIR/bin/dvx.exe" ]; then if [ ! -f "$SCRIPT_DIR/bin/dvx.exe" ]; then
echo "ERROR: bin/dvx.exe not found build failed?" echo "ERROR: bin/dvx.exe not found -- build failed?"
exit 1 exit 1
fi fi

View file

@ -1,4 +1,4 @@
# Packet Reliable Serial Transport # Packet -- Reliable Serial Transport
Packetized serial transport with HDLC-style framing, CRC-16 error Packetized serial transport with HDLC-style framing, CRC-16 error
detection, and a Go-Back-N sliding window protocol for reliable, detection, and a Go-Back-N sliding window protocol for reliable,
@ -71,7 +71,7 @@ The protocol uses Go-Back-N with a configurable sliding window
### Types ### Types
```c ```c
// Receive callback called for each verified, in-order packet // Receive callback -- called for each verified, in-order packet
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
// Opaque connection handle // Opaque connection handle
@ -106,10 +106,10 @@ PktConnT *pktOpen(int com, int windowSize,
Creates a packetized connection over an already-open COM port. Creates a packetized connection over an already-open COM port.
- `com` RS232 port index (`RS232_COM1`..`RS232_COM4`) - `com` -- RS232 port index (`RS232_COM1`..`RS232_COM4`)
- `windowSize` sliding window size (1-8), 0 for default (4) - `windowSize` -- sliding window size (1-8), 0 for default (4)
- `callback` called from `pktPoll()` for each received packet - `callback` -- called from `pktPoll()` for each received packet
- `callbackCtx` user pointer passed to callback - `callbackCtx` -- user pointer passed to callback
Returns a connection handle, or `NULL` on failure. Returns a connection handle, or `NULL` on failure.
@ -129,8 +129,8 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`. Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`.
- `block = true` waits for window space, polling for ACKs internally - `block = true` -- waits for window space, polling for ACKs internally
- `block = false` returns `PKT_ERR_TX_FULL` if the window is full - `block = false` -- returns `PKT_ERR_TX_FULL` if the window is full
The packet is stored in the retransmit buffer until acknowledged. The packet is stored in the retransmit buffer until acknowledged.

View file

@ -60,7 +60,7 @@
#define RETRANSMIT_TIMEOUT_MS 500 #define RETRANSMIT_TIMEOUT_MS 500
// Receive state machine: three states for HDLC deframing. // Receive state machine: three states for HDLC deframing.
// HUNT: discarding bytes until a flag (0x7E) is seen sync acquisition. // HUNT: discarding bytes until a flag (0x7E) is seen -- sync acquisition.
// ACTIVE: accumulating frame bytes, watching for flag (end of frame) or // ACTIVE: accumulating frame bytes, watching for flag (end of frame) or
// escape (next byte is XOR'd). // escape (next byte is XOR'd).
// ESCAPE: the previous byte was 0x7D; XOR this byte with 0x20 to recover // ESCAPE: the previous byte was 0x7D; XOR this byte with 0x20 to recover
@ -171,7 +171,7 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq);
// Table-driven CRC-16-CCITT. Processing one byte per iteration with a // Table-driven CRC-16-CCITT. Processing one byte per iteration with a
// 256-entry table is ~10x faster than bit-by-bit on a 486. The table // 256-entry table is ~10x faster than bit-by-bit on a 486. The table
// costs 512 bytes of .rodata a worthwhile trade for a function called // costs 512 bytes of .rodata -- a worthwhile trade for a function called
// on every frame received and transmitted. // on every frame received and transmitted.
static uint16_t crcCalc(const uint8_t *data, int len) { static uint16_t crcCalc(const uint8_t *data, int len) {
uint16_t crc = 0xFFFF; uint16_t crc = 0xFFFF;
@ -258,7 +258,7 @@ static void sendRst(PktConnT *conn) {
// Check if a sequence number falls within a window starting at base. // Check if a sequence number falls within a window starting at base.
// Works correctly with 8-bit wrap-around because unsigned subtraction // Works correctly with 8-bit wrap-around because unsigned subtraction
// wraps mod 256 if seq is "ahead" of base by less than size, diff // wraps mod 256 -- if seq is "ahead" of base by less than size, diff
// will be a small positive number. // will be a small positive number.
static int seqInWindow(uint8_t seq, uint8_t base, int size) { static int seqInWindow(uint8_t seq, uint8_t base, int size) {
uint8_t diff = seq - base; uint8_t diff = seq - base;
@ -282,7 +282,7 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq) {
// Handle a complete, de-stuffed frame. CRC is verified first; on failure, // Handle a complete, de-stuffed frame. CRC is verified first; on failure,
// we NAK to request retransmission of the frame we actually expected. // we NAK to request retransmission of the frame we actually expected.
// //
// For DATA frames: Go-Back-N receiver logic only accept if seq matches // For DATA frames: Go-Back-N receiver logic -- only accept if seq matches
// rxExpectSeq (strictly in-order). Out-of-order frames within the window // rxExpectSeq (strictly in-order). Out-of-order frames within the window
// trigger a NAK; duplicates and out-of-window frames are silently dropped. // trigger a NAK; duplicates and out-of-window frames are silently dropped.
// //
@ -306,7 +306,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
calcCrc = crcCalc(frame, len - CRC_SIZE); calcCrc = crcCalc(frame, len - CRC_SIZE);
rxCrc = frame[len - 2] | ((uint16_t)frame[len - 1] << 8); rxCrc = frame[len - 2] | ((uint16_t)frame[len - 1] << 8);
if (calcCrc != rxCrc) { if (calcCrc != rxCrc) {
// CRC mismatch request retransmit of what we expect // CRC mismatch -- request retransmit of what we expect
sendNak(conn, conn->rxExpectSeq); sendNak(conn, conn->rxExpectSeq);
return; return;
} }
@ -330,7 +330,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
conn->rxExpectSeq++; conn->rxExpectSeq++;
sendAck(conn, conn->rxExpectSeq); sendAck(conn, conn->rxExpectSeq);
} else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) { } else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) {
// Out of order but in window NAK the one we want // Out of order but in window -- NAK the one we want
sendNak(conn, conn->rxExpectSeq); sendNak(conn, conn->rxExpectSeq);
} }
// else: duplicate or out of window, silently discard // else: duplicate or out of window, silently discard
@ -361,7 +361,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
} }
case FRAME_RST: case FRAME_RST:
// Remote requested reset clear state and respond with RST // Remote requested reset -- clear state and respond with RST
conn->txNextSeq = 0; conn->txNextSeq = 0;
conn->txAckSeq = 0; conn->txAckSeq = 0;
conn->txCount = 0; conn->txCount = 0;
@ -429,7 +429,7 @@ static void rxProcessByte(PktConnT *conn, uint8_t byte) {
// ======================================================================== // ========================================================================
// Timer-based retransmission for slots that haven't been ACK'd within the // Timer-based retransmission for slots that haven't been ACK'd within the
// timeout. This handles the case where an ACK or NAK was lost without // timeout. This handles the case where an ACK or NAK was lost -- without
// this, the connection would stall forever. Each slot is retransmitted // this, the connection would stall forever. Each slot is retransmitted
// independently and its timer is reset, creating exponential backoff // independently and its timer is reset, creating exponential backoff
// behavior naturally (each retransmit resets the timer). // behavior naturally (each retransmit resets the timer).
@ -504,7 +504,7 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call
} }
// Main polling function must be called frequently (each iteration of the // Main polling function -- must be called frequently (each iteration of the
// app's main loop or event loop). It drains the serial port's RX buffer, // app's main loop or event loop). It drains the serial port's RX buffer,
// feeds bytes through the deframing state machine, and checks for // feeds bytes through the deframing state machine, and checks for
// retransmit timeouts. The callback is invoked synchronously for each // retransmit timeouts. The callback is invoked synchronously for each

View file

@ -16,7 +16,7 @@
// product of a serial link. On a 115200 bps local serial connection, the // product of a serial link. On a 115200 bps local serial connection, the
// round-trip time is negligible, so the window rarely fills. GBN's // round-trip time is negligible, so the window rarely fills. GBN's
// retransmit-all-from-NAK behavior wastes bandwidth on lossy links, but // retransmit-all-from-NAK behavior wastes bandwidth on lossy links, but
// serial links are nearly lossless the CRC check is primarily a safety // serial links are nearly lossless -- the CRC check is primarily a safety
// net for electrical noise, not a routine error recovery mechanism. // net for electrical noise, not a routine error recovery mechanism.
// //
// CRC-16-CCITT: // CRC-16-CCITT:

View file

@ -1,4 +1,4 @@
# SecLink Proxy Linux build # SecLink Proxy -- Linux build
# Compiles the packet, security, and secLink layers against a socket # Compiles the packet, security, and secLink layers against a socket
# shim instead of the DJGPP rs232 driver. # shim instead of the DJGPP rs232 driver.
@ -27,15 +27,15 @@ $(OBJDIR)/sockShim.o: sockShim.c sockShim.h | $(OBJDIR)
$(OBJDIR)/proxy.o: proxy.c sockShim.h | $(OBJDIR) $(OBJDIR)/proxy.o: proxy.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
# Packet layer block real rs232.h, inject socket shim # Packet layer -- block real rs232.h, inject socket shim
$(OBJDIR)/packet.o: ../packet/packet.c sockShim.h | $(OBJDIR) $(OBJDIR)/packet.o: ../packet/packet.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -I. -Istubs/ -include sockShim.h -c -o $@ $< $(CC) $(CFLAGS) -I. -Istubs/ -include sockShim.h -c -o $@ $<
# Security layer stub DOS-specific headers # Security layer -- stub DOS-specific headers
$(OBJDIR)/security.o: ../security/security.c | $(OBJDIR) $(OBJDIR)/security.o: ../security/security.c | $(OBJDIR)
$(CC) $(CFLAGS) -Istubs/ -c -o $@ $< $(CC) $(CFLAGS) -Istubs/ -c -o $@ $<
# SecLink layer block real rs232.h, inject socket shim # SecLink layer -- block real rs232.h, inject socket shim
$(OBJDIR)/secLink.o: ../seclink/secLink.c sockShim.h | $(OBJDIR) $(OBJDIR)/secLink.o: ../seclink/secLink.c sockShim.h | $(OBJDIR)
$(CC) $(CFLAGS) -I. -include sockShim.h -c -o $@ $< $(CC) $(CFLAGS) -I. -include sockShim.h -c -o $@ $<

View file

@ -15,7 +15,7 @@ side is plain telnet over TCP.
TCP:2323 TCP:23 TCP:2323 TCP:23
| | | |
+--- secproxy ----------------------------------+ +--- secproxy ----------------------------------+
secLink ←→ plaintext secLink <-> plaintext
(encrypted, reliable) (encrypted, reliable)
``` ```
@ -59,10 +59,10 @@ secproxy --help # show usage
The main loop uses `poll()` with a 10ms timeout to multiplex between The main loop uses `poll()` with a 10ms timeout to multiplex between
the two TCP connections: the two TCP connections:
- **86Box BBS**: `secLinkPoll()` reads from the 86Box socket via - **86Box -> BBS**: `secLinkPoll()` reads from the 86Box socket via
the socket shim, decrypts incoming packets, and the receive callback the socket shim, decrypts incoming packets, and the receive callback
writes plaintext to the BBS socket. writes plaintext to the BBS socket.
- **BBS 86Box**: `read()` from the BBS socket, then - **BBS -> 86Box**: `read()` from the BBS socket, then
`secLinkSend()` encrypts and sends to 86Box via the socket shim. `secLinkSend()` encrypts and sends to 86Box via the socket shim.
- **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit - **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit
timers on every iteration. timers on every iteration.

View file

@ -1,7 +1,7 @@
// SecLink proxy bridges an 86Box serial connection to a telnet BBS // SecLink proxy -- bridges an 86Box serial connection to a telnet BBS
// //
// Architecture: // Architecture:
// 86Box (DOS terminal) ←→ TCP ←→ proxy ←→ TCP ←→ BBS // 86Box (DOS terminal) <-> TCP <-> proxy <-> TCP <-> BBS
// secLink protocol plain telnet // secLink protocol plain telnet
// //
// The proxy runs on Linux and sits between two TCP connections: // The proxy runs on Linux and sits between two TCP connections:
@ -172,7 +172,7 @@ static int createListenSocket(int port) {
} }
// secLink receive callback called when decrypted data arrives from the DOS // secLink receive callback -- called when decrypted data arrives from the DOS
// terminal. Before the BBS is connected, we watch for an ENTER keypress as // terminal. Before the BBS is connected, we watch for an ENTER keypress as
// a "ready" signal so the user can see the terminal is working before we // a "ready" signal so the user can see the terminal is working before we
// connect to the BBS (which would immediately start sending ANSI art). // connect to the BBS (which would immediately start sending ANSI art).
@ -203,7 +203,7 @@ static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t chann
// On Linux, /dev/urandom provides high-quality entropy without blocking. // On Linux, /dev/urandom provides high-quality entropy without blocking.
// This is vastly better than the PIT-based entropy on DOS one of the // This is vastly better than the PIT-based entropy on DOS -- one of the
// advantages of running the proxy on the host OS. // advantages of running the proxy on the host OS.
static void seedRng(void) { static void seedRng(void) {
uint8_t entropy[32]; uint8_t entropy[32];
@ -246,7 +246,7 @@ static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt) {
// We accept ECHO and SGA (Suppress Go Ahead) because most BBSes require them // We accept ECHO and SGA (Suppress Go Ahead) because most BBSes require them
// for character-at-a-time mode. We accept TTYPE and NAWS for terminal type // for character-at-a-time mode. We accept TTYPE and NAWS for terminal type
// and window size negotiation. Everything else is refused. Subnegotiations // and window size negotiation. Everything else is refused. Subnegotiations
// (SB...SE) are consumed silently we don't actually respond with terminal // (SB...SE) are consumed silently -- we don't actually respond with terminal
// type or window size data, but accepting the option prevents the BBS from // type or window size data, but accepting the option prevents the BBS from
// falling back to line mode. // falling back to line mode.
// Returns the number of clean data bytes written to 'out'. // Returns the number of clean data bytes written to 'out'.
@ -268,7 +268,7 @@ static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
case TS_IAC: case TS_IAC:
switch (b) { switch (b) {
case TEL_IAC: case TEL_IAC:
// Escaped 0xFF emit literal // Escaped 0xFF -- emit literal
out[outLen++] = 0xFF; out[outLen++] = 0xFF;
sTelState = TS_DATA; sTelState = TS_DATA;
break; break;
@ -295,7 +295,7 @@ static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
break; break;
case TS_WILL: case TS_WILL:
// Server offers to do something accept ECHO and SGA, refuse others // Server offers to do something -- accept ECHO and SGA, refuse others
printf(" TEL RX: WILL %d\n", b); printf(" TEL RX: WILL %d\n", b);
if (b == TELOPT_ECHO || b == TELOPT_SGA) { if (b == TELOPT_ECHO || b == TELOPT_SGA) {
telnetRespond(bbsFd, TEL_DO, b); telnetRespond(bbsFd, TEL_DO, b);
@ -312,7 +312,7 @@ static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
break; break;
case TS_DO: case TS_DO:
// Server asks us to do something accept TTYPE and NAWS, refuse others // Server asks us to do something -- accept TTYPE and NAWS, refuse others
printf(" TEL RX: DO %d\n", b); printf(" TEL RX: DO %d\n", b);
if (b == TELOPT_TTYPE || b == TELOPT_NAWS) { if (b == TELOPT_TTYPE || b == TELOPT_NAWS) {
telnetRespond(bbsFd, TEL_WILL, b); telnetRespond(bbsFd, TEL_WILL, b);
@ -329,7 +329,7 @@ static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
break; break;
case TS_SB: case TS_SB:
// Inside subnegotiation skip until IAC SE // Inside subnegotiation -- skip until IAC SE
if (b == TEL_IAC) { if (b == TEL_IAC) {
sTelState = TS_SB_IAC; sTelState = TS_SB_IAC;
} }

View file

@ -1,11 +1,11 @@
// Socket shim rs232-compatible API over TCP sockets // Socket shim -- rs232-compatible API over TCP sockets
// //
// Maps up to 4 "COM ports" to TCP socket file descriptors. Reads use // Maps up to 4 "COM ports" to TCP socket file descriptors. Reads use
// MSG_DONTWAIT (non-blocking) to match rs232Read's non-blocking semantics. // MSG_DONTWAIT (non-blocking) to match rs232Read's non-blocking semantics.
// Writes are blocking (loop until all bytes sent) to match rs232Write's // Writes are blocking (loop until all bytes sent) to match rs232Write's
// guarantee of complete delivery. // guarantee of complete delivery.
// //
// The shim does NOT close sockets in rs232Close socket lifecycle is // The shim does NOT close sockets in rs232Close -- socket lifecycle is
// managed by the proxy's main() function. This avoids double-close bugs // managed by the proxy's main() function. This avoids double-close bugs
// when secLinkClose calls rs232Close but the proxy still needs the fd. // when secLinkClose calls rs232Close but the proxy still needs the fd.
@ -41,7 +41,7 @@ int rs232Close(int com) {
} }
// Serial parameters are ignored TCP handles framing, flow control, and // Serial parameters are ignored -- TCP handles framing, flow control, and
// error correction. We just validate that a socket has been assigned. // error correction. We just validate that a socket has been assigned.
int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) { int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
(void)bps; (void)bps;
@ -79,7 +79,7 @@ int rs232Read(int com, char *data, int len) {
return -1; return -1;
} }
if (n == 0) { if (n == 0) {
// TCP FIN peer closed connection // TCP FIN -- peer closed connection
return -1; return -1;
} }

View file

@ -1,4 +1,4 @@
// Socket shim provides rs232-compatible API backed by TCP sockets // Socket shim -- provides rs232-compatible API backed by TCP sockets
// Used by the Linux proxy to reuse the packet and secLink layers. // Used by the Linux proxy to reuse the packet and secLink layers.
// //
// Design: the packet and secLink layers call rs232Read/rs232Write assuming // Design: the packet and secLink layers call rs232Read/rs232Write assuming

View file

@ -1,4 +1,4 @@
// Stub for DJGPP <go32.h> Linux proxy build // Stub for DJGPP <go32.h> -- Linux proxy build
#ifndef GO32_H_STUB #ifndef GO32_H_STUB
#define GO32_H_STUB #define GO32_H_STUB

View file

@ -1,4 +1,4 @@
// Stub for DJGPP <pc.h> Linux proxy build // Stub for DJGPP <pc.h> -- Linux proxy build
#ifndef PC_H_STUB #ifndef PC_H_STUB
#define PC_H_STUB #define PC_H_STUB

View file

@ -1,4 +1,4 @@
// Stub for DJGPP <sys/farptr.h> Linux proxy build // Stub for DJGPP <sys/farptr.h> -- Linux proxy build
#ifndef FARPTR_H_STUB #ifndef FARPTR_H_STUB
#define FARPTR_H_STUB #define FARPTR_H_STUB

View file

@ -1,4 +1,4 @@
# RS232 Serial Port Library for DJGPP # RS232 -- Serial Port Library for DJGPP
ISR-driven UART communication library supporting up to 4 simultaneous ISR-driven UART communication library supporting up to 4 simultaneous
COM ports with ring buffers and hardware/software flow control. COM ports with ring buffers and hardware/software flow control.
@ -35,10 +35,10 @@ All functions take a COM port index (`int com`) as their first argument:
| Constant | Value | Description | | Constant | Value | Description |
|---------------------|-------|--------------------------------------| |---------------------|-------|--------------------------------------|
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected | | `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
| `RS232_UART_8250` | 1 | 8250 no FIFO, no scratch register | | `RS232_UART_8250` | 1 | 8250 -- no FIFO, no scratch register |
| `RS232_UART_16450` | 2 | 16450 scratch register, no FIFO | | `RS232_UART_16450` | 2 | 16450 -- scratch register, no FIFO |
| `RS232_UART_16550` | 3 | 16550 broken FIFO (unusable) | | `RS232_UART_16550` | 3 | 16550 -- broken FIFO (unusable) |
| `RS232_UART_16550A` | 4 | 16550A working 16-byte FIFO | | `RS232_UART_16550A` | 4 | 16550A -- working 16-byte FIFO |
### Handshaking Modes ### Handshaking Modes
@ -83,13 +83,13 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity,
Opens a COM port. Detects the UART base address from the BIOS data Opens a COM port. Detects the UART base address from the BIOS data
area, auto-detects the IRQ, installs the ISR, and configures the port. area, auto-detects the IRQ, installs the ISR, and configures the port.
- `bps` baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400, - `bps` -- baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400,
3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200) 3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200)
- `dataBits` 5, 6, 7, or 8 - `dataBits` -- 5, 6, 7, or 8
- `parity` `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark), - `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
`'S'` (space) `'S'` (space)
- `stopBits` 1 or 2 - `stopBits` -- 1 or 2
- `handshake` `RS232_HANDSHAKE_*` constant - `handshake` -- `RS232_HANDSHAKE_*` constant
```c ```c
int rs232Close(int com); int rs232Close(int com);
@ -154,9 +154,9 @@ int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
`rs232GetUartType` probes the UART hardware to identify the chip: `rs232GetUartType` probes the UART hardware to identify the chip:
1. **Scratch register test** writes two values to register 7 and 1. **Scratch register test** -- writes two values to register 7 and
reads them back. The 8250 lacks this register, so readback fails. reads them back. The 8250 lacks this register, so readback fails.
2. **FIFO test** enables the FIFO via the FCR, then reads IIR bits 2. **FIFO test** -- enables the FIFO via the FCR, then reads IIR bits
7:6. `0b11` = 16550A (working FIFO), `0b10` = 16550 (broken FIFO), 7:6. `0b11` = 16550A (working FIFO), `0b10` = 16550 (broken FIFO),
`0b00` = 16450 (no FIFO). The original FCR value is restored after `0b00` = 16450 (no FIFO). The original FCR value is restored after
probing. probing.

View file

@ -325,7 +325,7 @@ static void removeIrqHandler(int irq);
// 4. CLI, send EOI to PIC, re-enable COM IRQs, STI before IRET // 4. CLI, send EOI to PIC, re-enable COM IRQs, STI before IRET
// //
// This "mask-then-STI" pattern is standard for slow device ISRs on PC // This "mask-then-STI" pattern is standard for slow device ISRs on PC
// hardware it prevents the same IRQ from re-entering while still allowing // hardware -- it prevents the same IRQ from re-entering while still allowing
// the timer tick and keyboard to function during potentially long UART // the timer tick and keyboard to function during potentially long UART
// processing. // processing.
static void comGeneralIsr(void) { static void comGeneralIsr(void) {
@ -449,7 +449,7 @@ static void comGeneralIsr(void) {
// These functions wrap DJGPP's DPMI interface for installing protected-mode // These functions wrap DJGPP's DPMI interface for installing protected-mode
// interrupt handlers. Under DPMI, the ISR code and data must be locked in // interrupt handlers. Under DPMI, the ISR code and data must be locked in
// physical memory to prevent page faults during interrupt handling a page // physical memory to prevent page faults during interrupt handling -- a page
// fault inside an ISR would be fatal. The IRET wrapper is also allocated // fault inside an ISR would be fatal. The IRET wrapper is also allocated
// by DPMI to handle the real-mode-to-protected-mode transition. // by DPMI to handle the real-mode-to-protected-mode transition.
@ -603,7 +603,7 @@ static int findIrq(int comport) {
// Release this port's IRQ. If another port shares the same IRQ line, // Release this port's IRQ. If another port shares the same IRQ line,
// the handler stays installed only the last user removes it. This is // the handler stays installed -- only the last user removes it. This is
// critical for COM1/COM3 IRQ sharing. // critical for COM1/COM3 IRQ sharing.
static void freeIrq(int comport) { static void freeIrq(int comport) {
Rs232StateT *com = &sComPorts[comport]; Rs232StateT *com = &sComPorts[comport];
@ -998,8 +998,8 @@ int rs232GetTxBuffered(int com) {
// Detect UART type by probing hardware features. The detection sequence: // Detect UART type by probing hardware features. The detection sequence:
// 1. Write/read scratch register 8250 doesn't have one // 1. Write/read scratch register -- 8250 doesn't have one
// 2. Enable FIFO and check IIR bits 7:6 distinguishes 16450/16550/16550A // 2. Enable FIFO and check IIR bits 7:6 -- distinguishes 16450/16550/16550A
// This matters because only 16550A has a reliable 16-byte FIFO; the // This matters because only 16550A has a reliable 16-byte FIFO; the
// original 16550 FIFO is buggy and should not be enabled. // original 16550 FIFO is buggy and should not be enabled.
int rs232GetUartType(int com) { int rs232GetUartType(int com) {
@ -1014,7 +1014,7 @@ int rs232GetUartType(int com) {
return RS232_ERR_NOT_OPEN; return RS232_ERR_NOT_OPEN;
} }
// Test scratch register 8250 lacks it // Test scratch register -- 8250 lacks it
outportb(port->base + UART_SCR, 0xA5); outportb(port->base + UART_SCR, 0xA5);
scratch = inportb(port->base + UART_SCR); scratch = inportb(port->base + UART_SCR);
if (scratch != 0xA5) { if (scratch != 0xA5) {
@ -1026,7 +1026,7 @@ int rs232GetUartType(int com) {
return RS232_UART_8250; return RS232_UART_8250;
} }
// Has scratch register at least 16450. Try enabling FIFO. // Has scratch register -- at least 16450. Try enabling FIFO.
outportb(port->base + UART_FCR, FCR_ENABLE); outportb(port->base + UART_FCR, FCR_ENABLE);
iir = inportb(port->base + UART_IIR); iir = inportb(port->base + UART_IIR);

View file

@ -12,7 +12,7 @@
// pace from application-level processing. // pace from application-level processing.
// //
// Ring buffers are power-of-2 sized (2048 bytes) so head/tail index // Ring buffers are power-of-2 sized (2048 bytes) so head/tail index
// wrapping is a single AND, not a modulo critical for ISR speed. // wrapping is a single AND, not a modulo -- critical for ISR speed.
// //
// Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates entirely within // Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates entirely within
// the ISR using watermark thresholds. When the RX buffer is 80% full, // the ISR using watermark thresholds. When the RX buffer is 80% full,
@ -20,7 +20,7 @@
// prevents buffer overflow without application involvement. // prevents buffer overflow without application involvement.
// //
// The ISR and its data structures are locked in memory via DPMI to // The ISR and its data structures are locked in memory via DPMI to
// prevent page faults during interrupt handling a requirement for // prevent page faults during interrupt handling -- a requirement for
// any ISR running under a DPMI host (DOS extender, Windows, etc.). // any ISR running under a DPMI host (DOS extender, Windows, etc.).
#ifndef RS232_H #ifndef RS232_H

View file

@ -1,12 +1,12 @@
# SecLink Secure Serial Link Library # SecLink -- Secure Serial Link Library
SecLink is a convenience wrapper that ties together three lower-level SecLink is a convenience wrapper that ties together three lower-level
libraries into a single API for reliable, optionally encrypted serial libraries into a single API for reliable, optionally encrypted serial
communication: communication:
- **rs232** ISR-driven UART I/O with ring buffers and flow control - **rs232** -- ISR-driven UART I/O with ring buffers and flow control
- **packet** HDLC-style framing with CRC-16 and sliding window reliability - **packet** -- HDLC-style framing with CRC-16 and sliding window reliability
- **security** 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption - **security** -- 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption
## Architecture ## Architecture
@ -51,7 +51,7 @@ Cleartext packets can be sent immediately after `secLinkOpen()`.
### Types ### Types
```c ```c
// Receive callback called for each incoming packet with plaintext // Receive callback -- called for each incoming packet with plaintext
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel); typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel);
// Opaque connection handle // Opaque connection handle
@ -150,9 +150,9 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel. Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel.
- `channel` logical channel number (0-127) - `channel` -- logical channel number (0-127)
- `encrypt` if `true`, encrypts the payload (requires completed handshake) - `encrypt` -- if `true`, encrypts the payload (requires completed handshake)
- `block` if `true`, waits for transmit window space; if `false`, - `block` -- if `true`, waits for transmit window space; if `false`,
returns `SECLINK_ERR_SEND` when the window is full returns `SECLINK_ERR_SEND` when the window is full
Cleartext packets (`encrypt = false`) can be sent before the handshake. Cleartext packets (`encrypt = false`) can be sent before the handshake.
@ -288,8 +288,8 @@ Link against all four libraries:
SecLink requires these libraries (all in `../lib/`): SecLink requires these libraries (all in `../lib/`):
- `librs232.a` serial port driver - `librs232.a` -- serial port driver
- `libpacket.a` packet framing and reliability - `libpacket.a` -- packet framing and reliability
- `libsecurity.a` DH key exchange and XTEA cipher - `libsecurity.a` -- DH key exchange and XTEA cipher
Target: DJGPP cross-compiler, 486+ CPU. Target: DJGPP cross-compiler, 486+ CPU.

View file

@ -1,14 +1,14 @@
// Secure serial link ties rs232, packet, and security into one API // Secure serial link -- ties rs232, packet, and security into one API
// //
// Handshake protocol: // Handshake protocol:
// 1. Both sides send their DH public key (128 bytes) as a single packet // 1. Both sides send their DH public key (128 bytes) as a single packet
// 2. On receiving the remote key, compute shared secret immediately // 2. On receiving the remote key, compute shared secret immediately
// 3. Derive separate TX/RX cipher keys based on public key ordering // 3. Derive separate TX/RX cipher keys based on public key ordering
// 4. Transition to READY all subsequent encrypted packets use the keys // 4. Transition to READY -- all subsequent encrypted packets use the keys
// //
// The handshake uses the packet layer's reliable delivery, so lost packets // The handshake uses the packet layer's reliable delivery, so lost packets
// are automatically retransmitted. Both sides can send their public key // are automatically retransmitted. Both sides can send their public key
// simultaneously there's no initiator/responder distinction. // simultaneously -- there's no initiator/responder distinction.
// //
// Directionality: the side with the lexicographically lower public key // Directionality: the side with the lexicographically lower public key
// uses master XOR 0xAA for TX and master XOR 0x55 for RX. The other // uses master XOR 0xAA for TX and master XOR 0x55 for RX. The other
@ -79,7 +79,7 @@ static void internalRecv(void *ctx, const uint8_t *data, int len);
// Called when we've received the remote's public key. Computes the DH // Called when we've received the remote's public key. Computes the DH
// shared secret, derives directional cipher keys, and transitions to READY. // shared secret, derives directional cipher keys, and transitions to READY.
// After this, the DH context (containing the private key) is destroyed // After this, the DH context (containing the private key) is destroyed
// immediately forward secrecy principle: even if the long-term state is // immediately -- forward secrecy principle: even if the long-term state is
// compromised later, past session keys can't be recovered. // compromised later, past session keys can't be recovered.
static void completeHandshake(SecLinkT *link) { static void completeHandshake(SecLinkT *link) {
uint8_t masterKey[SEC_XTEA_KEY_SIZE]; uint8_t masterKey[SEC_XTEA_KEY_SIZE];
@ -103,7 +103,7 @@ static void completeHandshake(SecLinkT *link) {
memset(txKey, 0, sizeof(txKey)); memset(txKey, 0, sizeof(txKey));
memset(rxKey, 0, sizeof(rxKey)); memset(rxKey, 0, sizeof(rxKey));
// Destroy DH context private key no longer needed // Destroy DH context -- private key no longer needed
secDhDestroy(link->dh); secDhDestroy(link->dh);
link->dh = 0; link->dh = 0;

View file

@ -1,17 +1,17 @@
// Secure serial link convenience wrapper tying rs232 + packet + security // Secure serial link -- convenience wrapper tying rs232 + packet + security
// //
// This is the top-level API for the serial/networking stack. It composes // This is the top-level API for the serial/networking stack. It composes
// three layers into one: // three layers into one:
// rs232 ISR-driven UART I/O with ring buffers // rs232 -- ISR-driven UART I/O with ring buffers
// packet HDLC framing + CRC-16 + Go-Back-N ARQ (reliable delivery) // packet -- HDLC framing + CRC-16 + Go-Back-N ARQ (reliable delivery)
// security DH key exchange + XTEA-CTR encryption // security -- DH key exchange + XTEA-CTR encryption
// //
// Usage: // Usage:
// 1. secLinkOpen() opens COM port, sets up packet framing // 1. secLinkOpen() -- opens COM port, sets up packet framing
// 2. secLinkHandshake() DH key exchange (blocks until both sides complete) // 2. secLinkHandshake() -- DH key exchange (blocks until both sides complete)
// 3. secLinkSend() send data (optionally encrypted) on a channel // 3. secLinkSend() -- send data (optionally encrypted) on a channel
// 4. secLinkPoll() receive, decrypt if needed, deliver to callback // 4. secLinkPoll() -- receive, decrypt if needed, deliver to callback
// 5. secLinkClose() tear everything down // 5. secLinkClose() -- tear everything down
// //
// Channel multiplexing: // Channel multiplexing:
// Each packet carries a one-byte header: bit 7 = encrypted flag, // Each packet carries a one-byte header: bit 7 = encrypted flag,
@ -46,7 +46,7 @@
// Channel limits // Channel limits
#define SECLINK_MAX_CHANNEL 127 #define SECLINK_MAX_CHANNEL 127
// Receive callback delivers plaintext with channel number // Receive callback -- delivers plaintext with channel number
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel); typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel);
// Opaque handle // Opaque handle

View file

@ -1,4 +1,4 @@
# Security DH Key Exchange and XTEA-CTR Cipher # Security -- DH Key Exchange and XTEA-CTR Cipher
Cryptographic library providing Diffie-Hellman key exchange and XTEA Cryptographic library providing Diffie-Hellman key exchange and XTEA
symmetric encryption, optimized for 486-class DOS hardware running symmetric encryption, optimized for 486-class DOS hardware running
@ -16,8 +16,8 @@ under DJGPP/DPMI.
### XTEA Cipher (CTR Mode) ### XTEA Cipher (CTR Mode)
- 128-bit key, 64-bit block size, 32 rounds - 128-bit key, 64-bit block size, 32 rounds
- CTR mode encrypt and decrypt are the same XOR operation - CTR mode -- encrypt and decrypt are the same XOR operation
- No lookup tables, no key schedule just shifts, adds, and XORs - No lookup tables, no key schedule -- just shifts, adds, and XORs
- Ideal for constrained environments with small key setup cost - Ideal for constrained environments with small key setup cost
### Pseudo-Random Number Generator ### Pseudo-Random Number Generator
@ -148,7 +148,7 @@ starts at zero.
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
``` ```
Encrypts or decrypts `data` in place. CTR mode is symmetric the Encrypts or decrypts `data` in place. CTR mode is symmetric -- the
same operation encrypts and decrypts. same operation encrypts and decrypts.
```c ```c
@ -200,7 +200,7 @@ uint8_t message[] = "Secret message";
secCipherCrypt(cipher, message, sizeof(message)); secCipherCrypt(cipher, message, sizeof(message));
// message is now encrypted // message is now encrypted
// Decrypt (same operation CTR mode is symmetric) // Decrypt (same operation -- CTR mode is symmetric)
// Reset counter first if using the same cipher context // Reset counter first if using the same cipher context
secCipherSetNonce(cipher, 0, 0); secCipherSetNonce(cipher, 0, 0);
secCipherCrypt(cipher, message, sizeof(message)); secCipherCrypt(cipher, message, sizeof(message));
@ -240,8 +240,8 @@ The CIOS (Coarsely Integrated Operand Scanning) variant computes
`a * b * R^-1 mod m` in a single pass with implicit division by the `a * b * R^-1 mod m` in a single pass with implicit division by the
word base. Constants are computed once on first DH use: word base. Constants are computed once on first DH use:
- `R^2 mod p` via 2048 iterations of shift-and-conditional-subtract - `R^2 mod p` -- via 2048 iterations of shift-and-conditional-subtract
- `-p[0]^-1 mod 2^32` via Newton's method (5 iterations) - `-p[0]^-1 mod 2^32` -- via Newton's method (5 iterations)
### Secure Zeroing ### Secure Zeroing

View file

@ -5,7 +5,7 @@
// are 256 bits for fast computation on 486-class hardware. // are 256 bits for fast computation on 486-class hardware.
// //
// XTEA in CTR mode provides symmetric encryption. No lookup tables, // XTEA in CTR mode provides symmetric encryption. No lookup tables,
// no key schedule just shifts, adds, and XORs. // no key schedule -- just shifts, adds, and XORs.
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
@ -393,7 +393,7 @@ static void dhInit(void) {
// Volatile pointer prevents the compiler from optimizing away the zeroing // Volatile pointer prevents the compiler from optimizing away the zeroing
// as a dead store. Critical for clearing key material without volatile, // as a dead store. Critical for clearing key material -- without volatile,
// the compiler sees that ptr is about to be freed and removes the memset. // the compiler sees that ptr is about to be freed and removes the memset.
static void secureZero(void *ptr, int len) { static void secureZero(void *ptr, int len) {
volatile uint8_t *p = (volatile uint8_t *)ptr; volatile uint8_t *p = (volatile uint8_t *)ptr;
@ -406,7 +406,7 @@ static void secureZero(void *ptr, int len) {
// XTEA block cipher: encrypts an 8-byte block in-place. The Feistel network // XTEA block cipher: encrypts an 8-byte block in-place. The Feistel network
// uses 32 rounds (vs TEA's 32 or 64). Each round mixes the halves using // uses 32 rounds (vs TEA's 32 or 64). Each round mixes the halves using
// shifts, adds, and XORs no S-boxes, no lookup tables, no key schedule. // shifts, adds, and XORs -- no S-boxes, no lookup tables, no key schedule.
// The delta constant (golden ratio * 2^32) ensures each round uses a // The delta constant (golden ratio * 2^32) ensures each round uses a
// different effective key, preventing slide attacks. // different effective key, preventing slide attacks.
static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]) { static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]) {
@ -494,7 +494,7 @@ void secRngBytes(uint8_t *buf, int len) {
// and provide ~10 bits of entropy per read (depending on timing jitter). // and provide ~10 bits of entropy per read (depending on timing jitter).
// The BIOS tick at 18.2 Hz adds a few more bits. Two PIT readings with // The BIOS tick at 18.2 Hz adds a few more bits. Two PIT readings with
// the intervening code execution provide some jitter. Total: roughly 20 // the intervening code execution provide some jitter. Total: roughly 20
// bits of real entropy not enough alone, but sufficient to seed the DRBG // bits of real entropy -- not enough alone, but sufficient to seed the DRBG
// when supplemented by user interaction timing. // when supplemented by user interaction timing.
int secRngGatherEntropy(uint8_t *buf, int len) { int secRngGatherEntropy(uint8_t *buf, int len) {
int out = 0; int out = 0;
@ -524,7 +524,7 @@ int secRngGatherEntropy(uint8_t *buf, int len) {
// Initialize the RNG from entropy. The 64-byte discard at the end is // Initialize the RNG from entropy. The 64-byte discard at the end is
// standard DRBG practice it advances the state past any weak initial // standard DRBG practice -- it advances the state past any weak initial
// output that might leak information about the seed material. // output that might leak information about the seed material.
void secRngSeed(const uint8_t *entropy, int len) { void secRngSeed(const uint8_t *entropy, int len) {
memset(&sRng, 0, sizeof(sRng)); memset(&sRng, 0, sizeof(sRng));

View file

@ -7,7 +7,7 @@
// ~20 instructions per round (shifts, adds, XORs). This makes it ideal // ~20 instructions per round (shifts, adds, XORs). This makes it ideal
// for a 486 where cache is tiny (8KB) and AES's 4KB S-boxes would // for a 486 where cache is tiny (8KB) and AES's 4KB S-boxes would
// thrash it. DES is similarly table-heavy and also has a complex key // thrash it. DES is similarly table-heavy and also has a complex key
// schedule. XTEA has no library dependencies the entire cipher fits // schedule. XTEA has no library dependencies -- the entire cipher fits
// in a dozen lines of C. At 32 rounds, XTEA provides 128-bit security // in a dozen lines of C. At 32 rounds, XTEA provides 128-bit security
// with negligible per-byte cost on even the slowest target hardware. // with negligible per-byte cost on even the slowest target hardware.
// //
@ -46,7 +46,7 @@ typedef struct SecDhS SecDhT;
typedef struct SecCipherS SecCipherT; typedef struct SecCipherS SecCipherT;
// RNG seed before generating keys. Hardware entropy is weak (~20 bits); // RNG -- seed before generating keys. Hardware entropy is weak (~20 bits);
// callers should supplement with keyboard timing, mouse jitter, etc. // callers should supplement with keyboard timing, mouse jitter, etc.
int secRngGatherEntropy(uint8_t *buf, int len); int secRngGatherEntropy(uint8_t *buf, int len);
void secRngAddEntropy(const uint8_t *data, int len); void secRngAddEntropy(const uint8_t *data, int len);

View file

@ -113,7 +113,7 @@ int main(void) {
// After all tasks from phases 1-4 have terminated, their slots are free. // After all tasks from phases 1-4 have terminated, their slots are free.
// Creating new tasks should recycle those slots (IDs 1-4) rather than // Creating new tasks should recycle those slots (IDs 1-4) rather than
// growing the array. Each "wave" creates 3 tasks, lets them finish, // growing the array. Each "wave" creates 3 tasks, lets them finish,
// then creates 3 more the IDs should repeat across waves. // then creates 3 more -- the IDs should repeat across waves.
printf("\n--- Phase 5: Slot reuse ---\n"); printf("\n--- Phase 5: Slot reuse ---\n");
printf("[main] active before: %u\n", (unsigned)tsActiveCount()); printf("[main] active before: %u\n", (unsigned)tsActiveCount());

View file

@ -10,7 +10,7 @@
// Why inline asm instead of setjmp/longjmp for context switching: // Why inline asm instead of setjmp/longjmp for context switching:
// setjmp/longjmp only save callee-saved registers and don't give us // setjmp/longjmp only save callee-saved registers and don't give us
// control over the stack pointer in a portable way. We need to set up // control over the stack pointer in a portable way. We need to set up
// a brand-new stack for each task and jump into a trampoline setjmp // a brand-new stack for each task and jump into a trampoline -- setjmp
// can't bootstrap a fresh stack. The asm approach also avoids ABI // can't bootstrap a fresh stack. The asm approach also avoids ABI
// differences in jmp_buf layout across DJGPP versions. // differences in jmp_buf layout across DJGPP versions.
// //
@ -65,10 +65,10 @@ typedef struct {
} TaskContextT; } TaskContextT;
#endif #endif
// Task control block one per task slot. The 'allocated' flag tracks // Task control block -- one per task slot. The 'allocated' flag tracks
// whether the slot is live or recyclable, separate from the state enum, // whether the slot is live or recyclable, separate from the state enum,
// because we need to distinguish "never used" from "terminated and reaped". // because we need to distinguish "never used" from "terminated and reaped".
// The 'isMain' flag protects task 0 from kill/pause destroying the // The 'isMain' flag protects task 0 from kill/pause -- destroying the
// main task would orphan all other tasks with no scheduler to resume them. // main task would orphan all other tasks with no scheduler to resume them.
typedef struct { typedef struct {
char name[TS_NAME_MAX]; char name[TS_NAME_MAX];
@ -167,7 +167,7 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont
// RIP-relative lea captures the resume point address // RIP-relative lea captures the resume point address
"leaq 1f(%%rip), %%rax\n\t" "leaq 1f(%%rip), %%rax\n\t"
"movq %%rax, 56(%%rdi)\n\t" "movq %%rax, 56(%%rdi)\n\t"
// Restore new context once rsp is swapped we're on the other // Restore new context -- once rsp is swapped we're on the other
// task's stack. The jmp completes the switch. // task's stack. The jmp completes the switch.
"movq 0(%%rsi), %%rbx\n\t" "movq 0(%%rsi), %%rbx\n\t"
"movq 8(%%rsi), %%r12\n\t" "movq 8(%%rsi), %%r12\n\t"
@ -219,7 +219,7 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont
// Find a free (terminated or unallocated) slot in the task array. // Find a free (terminated or unallocated) slot in the task array.
// Returns the index, or -1 if no free slot exists. // Returns the index, or -1 if no free slot exists.
// Starts at 1 because slot 0 is always the main task and cannot be reused. // Starts at 1 because slot 0 is always the main task and cannot be reused.
// Linear scan is fine SHELL_MAX_APPS caps the practical limit at ~32 tasks. // Linear scan is fine -- SHELL_MAX_APPS caps the practical limit at ~32 tasks.
static int32_t findFreeSlot(void) { static int32_t findFreeSlot(void) {
ptrdiff_t count = arrlen(tasks); ptrdiff_t count = arrlen(tasks);
for (ptrdiff_t i = 1; i < count; i++) { for (ptrdiff_t i = 1; i < count; i++) {
@ -244,7 +244,7 @@ static int32_t findFreeSlot(void) {
// 4. Scan again after refill // 4. Scan again after refill
// //
// The round-robin scan starts at (currentIdx + 1) and wraps, ensuring // The round-robin scan starts at (currentIdx + 1) and wraps, ensuring
// fairness among tasks with equal priority no task gets picked twice // fairness among tasks with equal priority -- no task gets picked twice
// in a row unless it's the only ready task. // in a row unless it's the only ready task.
// //
// If no ready tasks exist at all (everything paused/terminated), return // If no ready tasks exist at all (everything paused/terminated), return
@ -262,7 +262,7 @@ static uint32_t scheduleNext(void) {
} }
} }
// All credits exhausted start a new epoch by refilling every ready task // All credits exhausted -- start a new epoch by refilling every ready task
bool anyReady = false; bool anyReady = false;
for (uint32_t i = 0; i < count; i++) { for (uint32_t i = 0; i < count; i++) {
if (tasks[i].allocated && tasks[i].state == TaskStateReady) { if (tasks[i].allocated && tasks[i].state == TaskStateReady) {
@ -291,7 +291,7 @@ static uint32_t scheduleNext(void) {
// Entry point for every new task. The first context switch into a new task // Entry point for every new task. The first context switch into a new task
// jumps here (via the EIP/RIP set up in tsCreate). This is a trampoline // jumps here (via the EIP/RIP set up in tsCreate). This is a trampoline
// rather than calling entry directly because we need to call tsExit() when // rather than calling entry directly because we need to call tsExit() when
// the entry function returns if we just set EIP to the entry function, // the entry function returns -- if we just set EIP to the entry function,
// it would return to a garbage address (the dummy 0 on the stack). // it would return to a garbage address (the dummy 0 on the stack).
// The trampoline ensures clean task termination even if the app forgets // The trampoline ensures clean task termination even if the app forgets
// to call tsExit() explicitly. // to call tsExit() explicitly.
@ -364,7 +364,7 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
// Set up initial stack (grows downward, 16-byte aligned). // Set up initial stack (grows downward, 16-byte aligned).
// The ABI requires 16-byte stack alignment at function entry. We align // The ABI requires 16-byte stack alignment at function entry. We align
// the top, then push a dummy return address (0) to simulate a CALL // the top, then push a dummy return address (0) to simulate a CALL
// instruction this keeps the stack aligned for the trampoline. // instruction -- this keeps the stack aligned for the trampoline.
// The dummy address is never used because taskTrampoline calls tsExit() // The dummy address is never used because taskTrampoline calls tsExit()
// which switches away without returning, but it satisfies debuggers // which switches away without returning, but it satisfies debuggers
// and ABI checkers that expect a return address at the bottom of each frame. // and ABI checkers that expect a return address at the bottom of each frame.
@ -401,7 +401,7 @@ uint32_t tsCurrentId(void) {
// Self-termination. Frees resources and switches to the next task. // Self-termination. Frees resources and switches to the next task.
// This function never returns the terminated task's context is abandoned. // This function never returns -- the terminated task's context is abandoned.
// We save to tasks[prev].context even though we'll never restore it because // We save to tasks[prev].context even though we'll never restore it because
// contextSwitch always writes to the save pointer; the data is harmless // contextSwitch always writes to the save pointer; the data is harmless
// and will be overwritten when the slot is recycled. // and will be overwritten when the slot is recycled.
@ -412,7 +412,7 @@ void tsExit(void) {
tasks[currentIdx].state = TaskStateTerminated; tasks[currentIdx].state = TaskStateTerminated;
// Free the stack immediately safe because we're about to switch // Free the stack immediately -- safe because we're about to switch
// away and never return. The context switch itself doesn't touch // away and never return. The context switch itself doesn't touch
// the old stack after swapping ESP/RSP. // the old stack after swapping ESP/RSP.
free(tasks[currentIdx].stack); free(tasks[currentIdx].stack);
@ -465,7 +465,7 @@ TaskStateE tsGetState(uint32_t taskId) {
// Register the calling context as task 0 (main). No stack is allocated // Register the calling context as task 0 (main). No stack is allocated
// because the main task uses the process stack. The main task's context // because the main task uses the process stack. The main task's context
// struct is filled in lazily by contextSwitch on the first tsYield() // struct is filled in lazily by contextSwitch on the first tsYield() --
// until then, the saved EIP/ESP are zero, which is fine because we // until then, the saved EIP/ESP are zero, which is fine because we
// never restore task 0 from a cold start. // never restore task 0 from a cold start.
int32_t tsInit(void) { int32_t tsInit(void) {
@ -492,11 +492,11 @@ int32_t tsInit(void) {
// Forcibly terminate another task. This is safe in a cooperative system // Forcibly terminate another task. This is safe in a cooperative system
// because the target is guaranteed to be suspended at a yield point it // because the target is guaranteed to be suspended at a yield point -- it
// cannot be in the middle of a critical section. The stack is freed and // cannot be in the middle of a critical section. The stack is freed and
// the slot is recycled immediately. // the slot is recycled immediately.
// //
// Cannot kill self (use tsExit instead) killing self would free the // Cannot kill self (use tsExit instead) -- killing self would free the
// stack we're currently executing on. Cannot kill main (task 0) because // stack we're currently executing on. Cannot kill main (task 0) because
// the shell's main loop must always be runnable for crash recovery. // the shell's main loop must always be runnable for crash recovery.
// //
@ -546,7 +546,7 @@ int32_t tsPause(uint32_t taskId) {
tasks[taskId].state = TaskStatePaused; tasks[taskId].state = TaskStatePaused;
// If we paused ourselves, must yield immediately a paused task // If we paused ourselves, must yield immediately -- a paused task
// won't be selected by scheduleNext, so staying on CPU would deadlock. // won't be selected by scheduleNext, so staying on CPU would deadlock.
// If pausing another task, no yield needed; it will simply be skipped // If pausing another task, no yield needed; it will simply be skipped
// the next time the scheduler scans. // the next time the scheduler scans.
@ -574,7 +574,7 @@ int32_t tsPause(uint32_t taskId) {
// app task. This function fixes the bookkeeping so the scheduler treats // app task. This function fixes the bookkeeping so the scheduler treats
// task 0 as the running task again. // task 0 as the running task again.
// //
// The crashed task's slot is NOT freed here its stack is corrupt and // The crashed task's slot is NOT freed here -- its stack is corrupt and
// the caller (shellMain's crash recovery) must call shellForceKillApp // the caller (shellMain's crash recovery) must call shellForceKillApp
// to clean it up properly (destroying windows, closing DXE, etc.). // to clean it up properly (destroying windows, closing DXE, etc.).
void tsRecoverToMain(void) { void tsRecoverToMain(void) {
@ -643,7 +643,7 @@ void tsShutdown(void) {
// The core cooperative yield. Called explicitly by app code (or implicitly // The core cooperative yield. Called explicitly by app code (or implicitly
// via the shell's idle callback and main loop). If no other task is ready, // via the shell's idle callback and main loop). If no other task is ready,
// returns immediately no context switch overhead when running solo. // returns immediately -- no context switch overhead when running solo.
// //
// The state transition: current task moves Running -> Ready (still // The state transition: current task moves Running -> Ready (still
// schedulable), next task moves Ready -> Running. The previous task will // schedulable), next task moves Ready -> Running. The previous task will
@ -660,7 +660,7 @@ void tsYield(void) {
uint32_t prev = currentIdx; uint32_t prev = currentIdx;
// Only transition to Ready if still Running a task that paused itself // Only transition to Ready if still Running -- a task that paused itself
// will already be in Paused state when tsYield is called from tsPause. // will already be in Paused state when tsYield is called from tsPause.
if (tasks[prev].state == TaskStateRunning) { if (tasks[prev].state == TaskStateRunning) {
tasks[prev].state = TaskStateReady; tasks[prev].state = TaskStateReady;

View file

@ -3,7 +3,7 @@
// Cooperative (non-preemptive) multitasking for DOS protected mode (DJGPP/DPMI). // Cooperative (non-preemptive) multitasking for DOS protected mode (DJGPP/DPMI).
// //
// Why cooperative instead of preemptive: // Why cooperative instead of preemptive:
// 1. DOS is single-threaded there is no kernel scheduler to preempt us. // 1. DOS is single-threaded -- there is no kernel scheduler to preempt us.
// 2. DPMI provides no thread or timer-based preemption primitives. // 2. DPMI provides no thread or timer-based preemption primitives.
// 3. The DVX GUI event model is inherently single-threaded: one compositor, // 3. The DVX GUI event model is inherently single-threaded: one compositor,
// one input queue, one window stack. Preemption would require locking // one input queue, one window stack. Preemption would require locking
@ -19,7 +19,7 @@
// //
// This is a weighted fair-share scheduler: a priority-10 task gets 11 // This is a weighted fair-share scheduler: a priority-10 task gets 11
// turns per round while a priority-0 task gets 1, but the low-priority // turns per round while a priority-0 task gets 1, but the low-priority
// task is guaranteed to run eventually no starvation is possible. // task is guaranteed to run eventually -- no starvation is possible.
#ifndef TASKSWITCH_H #ifndef TASKSWITCH_H
#define TASKSWITCH_H #define TASKSWITCH_H
@ -35,20 +35,20 @@
#define TS_ERR_NOMEM (-4) #define TS_ERR_NOMEM (-4)
#define TS_ERR_STATE (-5) #define TS_ERR_STATE (-5)
// 32KB default stack generous for callback-style app tasks that spend // 32KB default stack -- generous for callback-style app tasks that spend
// most of their time in the shell's GUI code. Apps needing deeper recursion // most of their time in the shell's GUI code. Apps needing deeper recursion
// or large stack allocations can request more via AppDescriptorT.stackSize. // or large stack allocations can request more via AppDescriptorT.stackSize.
#define TS_DEFAULT_STACK_SIZE 32768 #define TS_DEFAULT_STACK_SIZE 32768
#define TS_NAME_MAX 32 #define TS_NAME_MAX 32
// Priority levels the credit count is (priority + 1), so LOW gets 1 turn // Priority levels -- the credit count is (priority + 1), so LOW gets 1 turn
// per round, NORMAL gets 6, HIGH gets 11. The shell's main task runs at // per round, NORMAL gets 6, HIGH gets 11. The shell's main task runs at
// HIGH to keep UI responsive; app tasks default to NORMAL. // HIGH to keep UI responsive; app tasks default to NORMAL.
#define TS_PRIORITY_LOW 0 #define TS_PRIORITY_LOW 0
#define TS_PRIORITY_NORMAL 5 #define TS_PRIORITY_NORMAL 5
#define TS_PRIORITY_HIGH 10 #define TS_PRIORITY_HIGH 10
// Task states only Ready tasks participate in scheduling. The Running // Task states -- only Ready tasks participate in scheduling. The Running
// state is cosmetic (marks the currently executing task). Paused tasks // state is cosmetic (marks the currently executing task). Paused tasks
// are skipped until explicitly resumed. Terminated slots are recycled. // are skipped until explicitly resumed. Terminated slots are recycled.
typedef enum { typedef enum {
@ -58,14 +58,14 @@ typedef enum {
TaskStateTerminated = 3 TaskStateTerminated = 3
} TaskStateE; } TaskStateE;
// Task entry function signature the void* arg lets the caller pass // Task entry function signature -- the void* arg lets the caller pass
// arbitrary context (e.g., a ShellAppT* for the DVX shell's app wrapper) // arbitrary context (e.g., a ShellAppT* for the DVX shell's app wrapper)
typedef void (*TaskEntryT)(void *arg); typedef void (*TaskEntryT)(void *arg);
// Initialize the task system. The calling context becomes task 0 (main). // Initialize the task system. The calling context becomes task 0 (main).
// Task 0 is special: it cannot be killed or paused, and the crash recovery // Task 0 is special: it cannot be killed or paused, and the crash recovery
// path (tsRecoverToMain) always returns control here. No separate stack is // path (tsRecoverToMain) always returns control here. No separate stack is
// allocated for task 0 it uses the process stack directly. // allocated for task 0 -- it uses the process stack directly.
int32_t tsInit(void); int32_t tsInit(void);
// Shut down the task system and free all resources. // Shut down the task system and free all resources.
@ -77,7 +77,7 @@ void tsShutdown(void);
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority); int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
// Yield CPU to the next eligible ready task (credit-based round-robin). // Yield CPU to the next eligible ready task (credit-based round-robin).
// This is the sole mechanism for task switching every app must call // This is the sole mechanism for task switching -- every app must call
// this (or a GUI function that calls it) periodically, or it will // this (or a GUI function that calls it) periodically, or it will
// monopolize the CPU. // monopolize the CPU.
void tsYield(void); void tsYield(void);
@ -108,7 +108,7 @@ const char *tsGetName(uint32_t taskId);
// Terminate the calling task. Must not be called from the main task. // Terminate the calling task. Must not be called from the main task.
// The stack is freed immediately and the slot is marked for reuse. // The stack is freed immediately and the slot is marked for reuse.
// Internally performs a context switch to the next ready task this // Internally performs a context switch to the next ready task -- this
// function never returns to the caller. // function never returns to the caller.
void tsExit(void); void tsExit(void);
@ -121,7 +121,7 @@ int32_t tsKill(uint32_t taskId);
// Crash recovery: force scheduler back to main task (id 0). // Crash recovery: force scheduler back to main task (id 0).
// Call after longjmp from a signal handler that fired in a non-main task. // Call after longjmp from a signal handler that fired in a non-main task.
// The crashed task is NOT cleaned up call tsKill() afterward. // The crashed task is NOT cleaned up -- call tsKill() afterward.
// This exists because longjmp unwinds the crashed task's stack but the // This exists because longjmp unwinds the crashed task's stack but the
// scheduler's currentIdx still points to it. We must fix the bookkeeping // scheduler's currentIdx still points to it. We must fix the bookkeeping
// before doing anything else. // before doing anything else.

View file

@ -67,18 +67,18 @@ DOS console shows progress messages during connection setup.
Each iteration: Each iteration:
1. `dvxUpdate()` process mouse, keyboard, paint, and window events 1. `dvxUpdate()` -- process mouse, keyboard, paint, and window events
2. `secLinkPoll()` read serial data, decrypt, deliver to ring buffer 2. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer
3. `wgtAnsiTermPoll()` drain ring buffer into the ANSI parser 3. `wgtAnsiTermPoll()` -- drain ring buffer into the ANSI parser
## Data Flow ## Data Flow
``` ```
BBS → proxy → serial → secLinkPoll() → onRecv() → ring buffer BBS -> proxy -> serial -> secLinkPoll() -> onRecv() -> ring buffer
→ commRead() → wgtAnsiTermWrite() → ANSI parser → screen -> commRead() -> wgtAnsiTermWrite() -> ANSI parser -> screen
Keyboard → widgetAnsiTermOnKey() → commWrite() Keyboard -> widgetAnsiTermOnKey() -> commWrite()
→ secLinkSend() → serial → proxy → BBS -> secLinkSend() -> serial -> proxy -> BBS
``` ```
A 4KB ring buffer bridges the SecLink receive callback (which fires A 4KB ring buffer bridges the SecLink receive callback (which fires
@ -88,7 +88,7 @@ during `secLinkPoll()`) and the terminal widget's comm read interface
## GUI ## GUI
- **Window**: resizable, titled "SecLink Terminal" - **Window**: resizable, titled "SecLink Terminal"
- **Menu bar**: File Quit - **Menu bar**: File -> Quit
- **Terminal**: 80x25 ANSI terminal widget with 1000-line scrollback - **Terminal**: 80x25 ANSI terminal widget with 1000-line scrollback
- **Status bar**: shows COM port, baud rate, and encryption status - **Status bar**: shows COM port, baud rate, and encryption status

View file

@ -1,6 +1,6 @@
// termdemo.c SecLink terminal emulator demo // termdemo.c -- SecLink terminal emulator demo
// //
// A standalone DVX GUI application (NOT a DXE app this has its own main()) // A standalone DVX GUI application (NOT a DXE app -- this has its own main())
// that combines the ANSI terminal widget with the SecLink encrypted serial // that combines the ANSI terminal widget with the SecLink encrypted serial
// stack to create a BBS terminal client. // stack to create a BBS terminal client.
// //
@ -20,8 +20,8 @@
// during its paint cycle. // during its paint cycle.
// //
// Usage: termdemo [com_port] [baud_rate] // Usage: termdemo [com_port] [baud_rate]
// com_port 1-4 (default 1) // com_port -- 1-4 (default 1)
// baud_rate baud rate (default 115200) // baud_rate -- baud rate (default 115200)
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxWidget.h" #include "dvxWidget.h"
@ -71,7 +71,7 @@ static void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel);
// ============================================================ // ============================================================
// commRead drain receive ring buffer for terminal widget // commRead -- drain receive ring buffer for terminal widget
// ============================================================ // ============================================================
static int32_t commRead(void *ctx, uint8_t *buf, int32_t maxLen) { static int32_t commRead(void *ctx, uint8_t *buf, int32_t maxLen) {
@ -88,7 +88,7 @@ static int32_t commRead(void *ctx, uint8_t *buf, int32_t maxLen) {
// ============================================================ // ============================================================
// commWrite send keystrokes via secLink (encrypted, channel 0) // commWrite -- send keystrokes via secLink (encrypted, channel 0)
// ============================================================ // ============================================================
static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len) { static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len) {
@ -100,7 +100,7 @@ static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len) {
// ============================================================ // ============================================================
// idlePoll poll serial link instead of yielding CPU // idlePoll -- poll serial link instead of yielding CPU
// ============================================================ // ============================================================
static void idlePoll(void *ctx) { static void idlePoll(void *ctx) {
@ -110,7 +110,7 @@ static void idlePoll(void *ctx) {
// ============================================================ // ============================================================
// onCloseCb quit when the terminal window is closed // onCloseCb -- quit when the terminal window is closed
// ============================================================ // ============================================================
static void onCloseCb(WindowT *win) { static void onCloseCb(WindowT *win) {
@ -123,7 +123,7 @@ static void onCloseCb(WindowT *win) {
// ============================================================ // ============================================================
// onMenuCb handle menu commands // onMenuCb -- handle menu commands
// ============================================================ // ============================================================
static void onMenuCb(WindowT *win, int32_t menuId) { static void onMenuCb(WindowT *win, int32_t menuId) {
@ -140,7 +140,7 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
// ============================================================ // ============================================================
// onRecv secLink receive callback, fills ring buffer // onRecv -- secLink receive callback, fills ring buffer
// ============================================================ // ============================================================
static void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) { static void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {

Some files were not shown because too many files have changed in this diff Show more