Another deep dive on the code. Needs serious testing.

This commit is contained in:
Scott Duensing 2026-06-24 20:21:45 -05:00
parent 36c6eeaf81
commit aab9facb6c
135 changed files with 9425 additions and 5205 deletions

View file

@ -1235,16 +1235,18 @@ name$ = &quot;Hello&quot; ' String</code></pre>
---------- -------- -----------
1 (highest) ^ Exponentiation
2 - (unary), + (unary) Negation (unary plus is a no-op)
3 * / \ MOD Multiply, float divide, integer divide, modulus
4 + - Addition, subtraction (+ also concatenates strings)
5 &amp; String concatenation
6 = &lt;&gt; &lt; &gt; &lt;= &gt;= Comparison (returns Boolean)
7 NOT Logical/bitwise NOT
8 AND Logical/bitwise AND
9 XOR Logical/bitwise exclusive-or
10 OR Logical/bitwise OR
11 EQV Logical/bitwise equivalence
12 (lowest) IMP Logical/bitwise implication</pre>
3 * / Multiply, float divide
4 \ Integer divide
5 MOD Modulus
6 + - Addition, subtraction (+ also concatenates strings)
7 &amp; String concatenation
8 = &lt;&gt; &lt; &gt; &lt;= &gt;= Comparison (returns Boolean)
9 NOT Logical/bitwise NOT
10 AND Logical/bitwise AND
11 XOR Logical/bitwise exclusive-or
12 OR Logical/bitwise OR
13 EQV Logical/bitwise equivalence
14 (lowest) IMP Logical/bitwise implication</pre>
<h2>Arithmetic Operators</h2>
<pre> Operator Description
-------- -----------
@ -2829,11 +2831,15 @@ End Sub</code></pre>
<pre> Property Type Description
---------- ------- -------------------------------------------
DataSource String Name of the Data control that supplies records.
GridLines Boolean Show or hide grid lines between cells (default: True).</pre>
GridLines Boolean Show or hide grid lines between cells (default: True).
SelectedRow Integer Index of the currently selected row (-1 = no selection).</pre>
<h2>Type-Specific Methods</h2>
<pre> Method Parameters Description
------- ---------- -------------------------------------------
Refresh (none) Reload and redraw the grid from the Data control.</pre>
---------------- ------------------------------- -------------------------------------------
Refresh (none) Reload and redraw the grid from the Data control.
SetColumnHeader Field As String, Header As String Set the displayed header text for a column.
SetColumnVisible Field As String, Visible As Boolean Show or hide a column by field name.
SetColumnWidth Field As String, Width As Integer Set the pixel width of a column by field name.</pre>
<h2>Type-Specific Events</h2>
<pre> Event Parameters Description
-------- ---------- -------------------------------------------

View file

@ -579,11 +579,13 @@ img { max-width: 100%; }
<li><a href="#shell.lifecycle">shellReapApp</a></li>
<li><a href="#shell.lifecycle">shellReapApps</a></li>
<li><a href="#shell.desktop">shellRegisterDesktopUpdate</a></li>
<li><a href="#shell.desktop">shellRegisterIdle</a></li>
<li><a href="#shell.query">shellRunningAppCount</a></li>
<li><a href="#lib.taskmgr">shellTaskMgrOpen</a></li>
<li><a href="#lib.taskmgr">shellTaskMgrRefresh</a></li>
<li><a href="#shell.lifecycle">shellTerminateAllApps</a></li>
<li><a href="#shell.desktop">shellUnregisterDesktopUpdate</a></li>
<li><a href="#shell.desktop">shellUnregisterIdle</a></li>
<li><a href="#widget.slider">Slider</a></li>
<li><a href="#widget.spacer">Spacer</a></li>
<li><a href="#widget.spinner">Spinner</a></li>
@ -783,9 +785,11 @@ img { max-width: 100%; }
<li><a href="#widget.tabcontrol">wgtTabControlSetActive</a></li>
<li><a href="#widget.tabcontrol">wgtTabPage</a></li>
<li><a href="#widget.textinput">wgtTextArea</a></li>
<li><a href="#widget.textinput">wgtTextAreaCursorToEnd</a></li>
<li><a href="#widget.textinput">wgtTextAreaFindNext</a></li>
<li><a href="#widget.textinput">wgtTextAreaGoToLine</a></li>
<li><a href="#widget.textinput">wgtTextAreaReplaceAll</a></li>
<li><a href="#widget.textinput">wgtTextAreaReplaceSelection</a></li>
<li><a href="#widget.textinput">wgtTextAreaSetAutoIndent</a></li>
<li><a href="#widget.textinput">wgtTextAreaSetColorize</a></li>
<li><a href="#widget.textinput">wgtTextAreaSetShowLineNumbers</a></li>
@ -1082,8 +1086,7 @@ img { max-width: 100%; }
CHROME_TITLE_HEIGHT = 20px
CHROME_INNER_BORDER = 2px
CHROME_MENU_HEIGHT = 20px
SCROLLBAR_WIDTH = 16px
CHROME_CLOSE_BTN_SIZE = 16px</code></pre>
SCROLLBAR_WIDTH = 16px</code></pre>
<h2>Hit Test Regions</h2>
<p>wmHitTest() iterates the stack front-to-back and returns a hit-part identifier: HIT_CONTENT, HIT_TITLE, HIT_CLOSE, HIT_RESIZE, HIT_MENU, HIT_VSCROLL, HIT_HSCROLL, HIT_MINIMIZE, HIT_MAXIMIZE. Resize edge detection returns a bitmask of RESIZE_LEFT, RESIZE_RIGHT, RESIZE_TOP, RESIZE_BOTTOM (corners combine two edges).</p>
<h2>Menu System</h2>
@ -1710,8 +1713,7 @@ prefsClose(h);</code></pre>
CHROME_MENU_HEIGHT 20 Menu bar height
CHROME_TOTAL_TOP 26 Total inset from top of frame to content
CHROME_TOTAL_SIDE 6 Total inset from side of frame to content
CHROME_TOTAL_BOTTOM 6 Total inset from bottom of frame to content
CHROME_CLOSE_BTN_SIZE 16 Close button gadget size</pre>
CHROME_TOTAL_BOTTOM 6 Total inset from bottom of frame to content</pre>
<h2>Hit Test Constants</h2>
<pre> Define Value Description
------ ----- -----------
@ -2211,11 +2213,12 @@ prefsClose(h);</code></pre>
win Window to add menu bar to</pre>
<p>Returns: Pointer to the new MenuBarT.</p>
<h3>wmDestroyMenuBar</h3>
<pre><code>void wmDestroyMenuBar(WindowT *win);</code></pre>
<p>Free the menu bar and reclaim the content area.</p>
<pre><code>void wmDestroyMenuBar(WindowT *win, const DisplayT *d);</code></pre>
<p>Free the menu bar and reclaim the content area. The content buffer is reallocated to fit the grown content area.</p>
<pre> Parameter Description
--------- -----------
win Window to remove menu bar from</pre>
win Window to remove menu bar from
d Display context; pass NULL only during window teardown</pre>
<h3>wmAddMenu</h3>
<pre><code>MenuT *wmAddMenu(MenuBarT *bar, const char *label);</code></pre>
<p>Append a dropdown menu to the menu bar. The label supports &amp; accelerator markers (e.g. &quot;&amp;File&quot;).</p>
@ -3875,7 +3878,6 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch</code></pre>
sKeyPressedBtn WidgetT* Button being pressed via keyboard (for release tracking)
sOpenPopup WidgetT* Currently open popup (dropdown/combo list)
sDragWidget WidgetT* Widget being dragged (any drag type)
sPollWidgetCount int32_t Count of widgets with WCLASS_NEEDS_POLL flag set
sPollWidgets WidgetT** Dynamic array of poll widgets (stb_ds)
sCursorBlinkFn fn ptr Callback invoked when the blink flips (repaints focused text)</pre>
<h2>Core Widget Functions</h2>
@ -3928,9 +3930,6 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch</code></pre>
<h3>clipboardGet</h3>
<pre><code>const char *clipboardGet(int32_t *outLen);</code></pre>
<p>Retrieve clipboard text.</p>
<h3>clipboardMaxLen</h3>
<pre><code>int32_t clipboardMaxLen(void);</code></pre>
<p>Return the fixed capacity of the clipboard buffer in bytes.</p>
<h2>Scrollbar Helpers</h2>
<h3>ScrollHitE</h3>
<p>Scrollbar hit-test result.</p>
@ -4464,7 +4463,7 @@ shellConfigPath(ctx, &quot;settings.ini&quot;, path, sizeof(path));
<h2>Shell Event Hooks</h2>
<pre> AppContextT field Handler Purpose
----------------- ------- -------
idleCallback idleYield Yields to cooperative tasks when dvxUpdate has no input events and no dirty rects.
idleCallback shellIdleDispatch Runs every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
onCtrlEsc ctrlEscHandler Invokes shellCtrlEscFn if the Task Manager DXE is loaded.
onF1 f1HelpHandler Launches the help viewer, optionally passing the focused app's helpFile and helpTopic.
onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh.</pre>
@ -4474,6 +4473,12 @@ shellConfigPath(ctx, &quot;settings.ini&quot;, path, sizeof(path));
<h2>shellUnregisterDesktopUpdate</h2>
<pre><code>void shellUnregisterDesktopUpdate(void (*updateFn)(void));</code></pre>
<p>Remove a previously registered callback. Call this before app shutdown to avoid dangling function pointers.</p>
<h2>shellRegisterIdle</h2>
<pre><code>void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);</code></pre>
<p>Register an idle handler invoked every idle frame by the shell main loop, regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook, which the shell calls on both graceful reap and force-kill.</p>
<h2>shellUnregisterIdle</h2>
<pre><code>void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);</code></pre>
<p>Remove a previously registered idle handler. No-op if the (fn, ctx) pair is absent, so a double-unregister is harmless.</p>
<h2>shellDesktopUpdate</h2>
<pre><code>void shellDesktopUpdate(void);</code></pre>
<p>Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh.</p>
@ -5241,13 +5246,8 @@ dvxSqlExec(db, sql);</code></pre>
<pre> Constant Value Description
-------- ----- -----------
PKT_SUCCESS 0 Operation succeeded.
PKT_ERR_INVALID_PORT -1 Invalid COM port index.
PKT_ERR_NOT_OPEN -2 Connection is not open.
PKT_ERR_WOULD_BLOCK -4 Operation would block.
PKT_ERR_OVERFLOW -5 Buffer overflow.
PKT_ERR_INVALID_PARAM -6 Invalid parameter (NULL handle, bad data pointer, length out of range).
PKT_ERR_TX_FULL -7 Transmit window is full (non-blocking send only).
PKT_ERR_NO_DATA -8 No data available.
PKT_ERR_DISCONNECTED -9 Serial port returned a negative read (disconnect/error).</pre>
<h2>PktRecvCallbackT</h2>
<p>Callback type for received packets.</p>
@ -5314,8 +5314,7 @@ dvxSqlExec(db, sql);</code></pre>
-------- ----- -----------
SEC_SUCCESS 0 Operation succeeded.
SEC_ERR_PARAM -1 Invalid parameter.
SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).
SEC_ERR_ALLOC -3 Memory allocation failed.</pre>
SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).</pre>
<h2>RNG Functions</h2>
<p>The RNG uses XTEA-CTR as a DRBG. Hardware entropy from PIT timer jitter is weak (~20 bits); supplement with keyboard timing or mouse jitter before generating DH keys.</p>
<pre><code>void secRngSeed(const uint8_t *entropy, int len);
@ -5349,15 +5348,12 @@ void secDhDestroy(SecDhT *dh);</code></pre>
<h2>XTEA-CTR Cipher</h2>
<p>XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.</p>
<pre><code>SecCipherT *secCipherCreate(const uint8_t *key);
void secCipherSetNonce(SecCipherT *c,
uint32_t nonceLo, uint32_t nonceHi);
void secCipherCrypt(SecCipherT *c,
uint8_t *data, int len);
void secCipherDestroy(SecCipherT *c);</code></pre>
<pre> Function Description
-------- -----------
secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
secCipherSetNonce Set the initial counter value (nonce). Must be unique per session. Both the stored nonce and the running counter are set to (nonceLo, nonceHi).
secCipherCrypt Encrypt or decrypt data in place. The counter increments by one every 8 bytes (one XTEA block).
secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.</pre>
<p>secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len &lt;= 0.</p>
@ -6988,11 +6984,13 @@ WidgetT *page2 = wgtTabPage(tabs, &quot;Advanced&quot;);
void wgtTextAreaSetUseTabChar(w, useChar) When true, insert literal tab characters; when false, insert spaces.
bool wgtTextAreaFindNext(w, needle, caseSens, fwd) Search for the next occurrence. Returns true if found.
int32_t wgtTextAreaReplaceAll(w, needle, repl, caseSens) Replace all occurrences. Returns the number of replacements made.
bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found.
void wgtTextAreaSetLineDecorator(w, fn, ctx) Set a gutter line decorator callback. Returns a color and receives the line number.
int32_t wgtTextAreaGetCursorLine(w) Get the current cursor line number.
void wgtTextAreaSetGutterClickCallback(w, fn) Set a callback for gutter clicks (e.g. for breakpoint toggling).
int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize) Copy the word under the cursor into buf. Returns its length.
void wgtTextAreaSetSyntaxColors(w, colors, count) Set the color palette used by the colorizer.</pre>
void wgtTextAreaSetSyntaxColors(w, colors, count) Set the color palette used by the colorizer.
void wgtTextAreaCursorToEnd(w) Move the cursor to the very end of the buffer (past the last character).</pre>
<h3>API Struct (wgtRegisterApi &quot;textinput&quot;)</h3>
<p>The combined &quot;textinput&quot; API exposes all four constructors plus the TextArea functions. The designer uses per-type APIs registered under the names &quot;textbox&quot; (single-line) and &quot;textarea&quot; (multi-line), each exposing only a create slot.</p>
<h3>Properties (BASIC Interface)</h3>

View file

@ -170,7 +170,7 @@ static void updateTime(void) {
if (!tm) {
snprintf(sState.timeStr, sizeof(sState.timeStr), "--:--:--");
snprintf(sState.dateStr, sizeof(sState.dateStr), "----:--:--");
snprintf(sState.dateStr, sizeof(sState.dateStr), "----------");
sState.lastUpdate = now;
return;
}

View file

@ -76,6 +76,14 @@
#define CP_CONFIRM_H 100
#define CP_CONFIRM_BTN_W 70
#define CP_WPAPER_BTN_W 90
// Cursor-speed slider runs slow..fast (low..high), but the platform
// mickey-to-pixel ratio runs the opposite way (high = slow). The two
// are mirror images across the slider's endpoints, so the conversion
// in both directions is: other = (MIN + MAX) - this. The INI 'speed'
// key and the boot path store/consume the mickey ratio (see shellMain).
#define CP_SPEED_SLIDER_MIN 2
#define CP_SPEED_SLIDER_MAX 32
#define CP_SPEED_INVERT (CP_SPEED_SLIDER_MIN + CP_SPEED_SLIDER_MAX)
#define WPAPER_DIR "CONFIG/WPAPER"
#define THEME_DIR "CONFIG/THEMES"
#define THEME_EXT ".THM"
@ -93,7 +101,6 @@ typedef struct {
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
static PrefsHandleT *sPrefs = NULL;
@ -102,8 +109,8 @@ static PrefsHandleT *sPrefs = NULL;
static uint8_t sSavedColorRgb[ColorCountE][3];
static int32_t sSavedWheelDir;
static int32_t sSavedDblClick;
static int32_t sSavedAccel;
static int32_t sSavedSpeed;
static int32_t sSavedAccelThresh; // accel threshold (dvxSetMouseConfig input)
static int32_t sSavedMickeys; // mickey ratio (dvxSetMouseConfig input)
static int32_t sSavedWheelStep;
static int32_t sSavedVideoW;
static int32_t sSavedVideoH;
@ -144,6 +151,7 @@ static const char **sWpaperLabels = NULL; // stb_ds dynamic array
static const VideoModeInfoT *sVideoModes = NULL; // points into ctx, not owned
static int32_t sVideoCount = 0;
static const char **sVideoLabels = NULL; // stb_ds dynamic array
static char (*sLabelBufs)[48] = NULL; // stb_ds dynamic array of mode labels
static WidgetT *sVideoList = NULL;
static int32_t sVideoConfirmResult = -1;
@ -211,6 +219,31 @@ AppDescriptorT appDescriptor = {
};
// Cleanup hook the shell calls before dlclose: these module-static stb_ds
// arrays (and any still-open prefs handle) would otherwise leak into the
// long-lived shell heap on every open/close, since the DXE unload doesn't
// free tracked blocks.
void appShutdown(void) {
arrfree(sThemeEntries);
sThemeEntries = NULL;
arrfree(sThemeLabels);
sThemeLabels = NULL;
arrfree(sWpaperEntries);
sWpaperEntries = NULL;
arrfree(sWpaperLabels);
sWpaperLabels = NULL;
arrfree(sVideoLabels);
sVideoLabels = NULL;
arrfree(sLabelBufs);
sLabelBufs = NULL;
if (sPrefs) {
prefsClose(sPrefs);
sPrefs = NULL;
}
}
static void applyMouseConfig(void) {
int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
@ -218,12 +251,11 @@ static void applyMouseConfig(void) {
const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop));
int32_t accelVal = mapAccelName(accelName);
// Slider is inverted: low value = slow (high mickeys), high value = fast (low mickeys)
// Slider range 2..32. Mickey ratio = 34 - sliderValue, so:
// Slider is inverted vs the mickey ratio (see CP_SPEED_INVERT):
// slider 2 -> 32 mickeys/8px (slowest)
// slider 8 -> 26 (near default)
// slider 26 -> 8 mickeys/8px (default)
// slider 32 -> 2 mickeys/8px (fastest)
int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr);
int32_t speedVal = CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr);
int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr);
dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep);
@ -445,12 +477,14 @@ static void buildMouseTab(WidgetT *page) {
speedRow->spacing = wgtPixels(CP_SPACING);
wgtLabel(speedRow, "Slow");
sSpeedSldr = wgtSlider(speedRow, 2, 32);
sSpeedSldr = wgtSlider(speedRow, CP_SPEED_SLIDER_MIN, CP_SPEED_SLIDER_MAX);
sSpeedSldr->weight = WGT_WEIGHT_FILL;
sSpeedSldr->onChange = onSpeedSlider;
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
wgtSliderSetValue(sSpeedSldr, speed);
// INI 'speed' holds the mickey ratio (the unit the platform and the
// shell boot path use); convert to the inverted slider position.
int32_t mickeys = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
wgtSliderSetValue(sSpeedSldr, CP_SPEED_INVERT - mickeys);
wgtLabel(speedRow, "Fast ");
sSpeedLbl = wgtLabel(speedRow, "");
sSpeedLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
@ -478,10 +512,9 @@ static void buildVideoTab(WidgetT *page) {
// Build label strings
arrsetlen(sVideoLabels, 0);
static char (*labelBufs)[48] = NULL;
arrfree(labelBufs);
labelBufs = NULL;
arrsetlen(labelBufs, sVideoCount);
arrfree(sLabelBufs);
sLabelBufs = NULL;
arrsetlen(sLabelBufs, sVideoCount);
for (int32_t i = 0; i < sVideoCount; i++) {
const char *depthName;
@ -495,8 +528,8 @@ static void buildVideoTab(WidgetT *page) {
default: depthName = ""; break;
}
snprintf(labelBufs[i], 48, "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName);
arrput(sVideoLabels, (const char *)labelBufs[i]);
snprintf(sLabelBufs[i], sizeof(sLabelBufs[i]), "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName);
arrput(sVideoLabels, (const char *)sLabelBufs[i]);
}
sVideoList = wgtListBox(page);
@ -521,11 +554,11 @@ static void buildVideoTab(WidgetT *page) {
static int32_t mapAccelName(const char *name) {
if (strcmp(name, "off") == 0) return 10000;
if (strcmp(name, "low") == 0) return 100;
if (strcmp(name, "medium") == 0) return 64;
if (strcmp(name, "high") == 0) return 32;
return 64;
if (strcmp(name, "off") == 0) return MOUSE_ACCEL_OFF;
if (strcmp(name, "low") == 0) return MOUSE_ACCEL_LOW;
if (strcmp(name, "medium") == 0) return MOUSE_ACCEL_MEDIUM;
if (strcmp(name, "high") == 0) return MOUSE_ACCEL_HIGH;
return MOUSE_ACCEL_MEDIUM;
}
@ -582,8 +615,8 @@ static void onBrowseTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "\1" },
{ "\1" }
{ "Theme Files (*.thm)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
@ -608,12 +641,12 @@ static void onChooseWallpaper(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "\1" },
{ "\1" }
{ "Images (*.bmp;*.png;*.jpg;*.gif)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, "CONFIG/WPAPER", filters, 2, path, sizeof(path))) {
if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, WPAPER_DIR, filters, 2, path, sizeof(path))) {
if (dvxSetWallpaper(sAc, path)) {
strncpy(sWallpaperPath, path, sizeof(sWallpaperPath) - 1);
sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0';
@ -703,7 +736,9 @@ static void onOk(WidgetT *w) {
prefsSetString(sPrefs, "mouse", "wheel", wheelSel == 1 ? "reversed" : MOUSE_WHEEL_DIR_DEFAULT);
prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
prefsSetInt(sPrefs, "mouse", "speed", wgtSliderGetValue(sSpeedSldr));
// Store the mickey ratio (not the raw slider position) so the boot
// path, which applies this value directly, matches the live setting.
prefsSetInt(sPrefs, "mouse", "speed", CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr));
prefsSetInt(sPrefs, "mouse", "wheelspeed", wgtSliderGetValue(sWheelStepSldr));
// Save colors to INI
@ -759,8 +794,8 @@ static void onSaveTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "\1" },
{ "\1" }
{ "Theme Files (*.thm)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
@ -927,11 +962,9 @@ static void restoreSnapshot(void) {
memcpy(sAc->colorRgb, sSavedColorRgb, sizeof(sSavedColorRgb));
dvxApplyColorScheme(sAc);
// Restore mouse
const char *accelName = mapAccelValue(sSavedAccel);
int32_t accelVal = mapAccelName(accelName);
int32_t speedVal = 34 - sSavedSpeed;
dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal, sSavedWheelStep);
// Restore mouse to the pre-open state captured in saveSnapshot.
// The snapshot already holds the dvxSetMouseConfig inputs directly.
dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, sSavedAccelThresh, sSavedMickeys, sSavedWheelStep);
// Restore video mode if changed
if (sAc->display.width != sSavedVideoW ||
@ -958,8 +991,14 @@ static void saveSnapshot(void) {
memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb));
sSavedWheelDir = sAc->wheelDirection;
sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC);
sSavedAccel = wgtDropdownGetSelected(sAccelDrop);
sSavedSpeed = wgtSliderGetValue(sSpeedSldr);
// Accel and mickey ratio are NOT cached on the AppContext, and the
// mouse-tab widgets do not exist yet when saveSnapshot runs (it is
// called before buildMouseTab). Capture them from prefs, which hold
// the currently-applied values -- reading the NULL widgets here used
// to snapshot garbage (-1 / 0) and corrupt the mouse on Cancel.
const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", MOUSE_ACCEL_DEFAULT);
sSavedAccelThresh = mapAccelName(accelStr);
sSavedMickeys = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
sSavedWheelStep = sAc->wheelStep;
sSavedVideoW = sAc->display.width;
sSavedVideoH = sAc->display.height;
@ -1128,7 +1167,6 @@ static void updateWheelStepLabel(void) {
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
sPrefs = prefsLoad(DVX_INI_PATH);
@ -1138,6 +1176,8 @@ int32_t appMain(DxeAppContextT *ctx) {
sWin = dvxCreateWindow(sAc, "Control Panel", winX, winY, CP_WIN_W, CP_WIN_H, true);
if (!sWin) {
prefsClose(sPrefs);
sPrefs = NULL;
return -1;
}

View file

@ -94,7 +94,17 @@ SYSTEMDIR = ../../../../bin/system
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE
# Run every built harness and fail the build if any returns non-zero.
# test_vm/test_lex/test_quick are inspection harnesses with no pass/fail
# of their own, so they run output-suppressed purely for crash detection;
# test_compiler, test_compact and test_suite each report a failure count
# and return non-zero on failure. make stops at the first one that fails.
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)
$(TEST_VM) > /dev/null
$(TEST_LEX) > /dev/null
$(TEST_QUICK) > /dev/null
$(TEST_COMPILER)
$(TEST_COMPACT)
$(TEST_SUITE)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)

View file

@ -52,4 +52,4 @@ declarations are visible to form event handlers.
make -C src/apps/kpunch/dvxbasic tests # build native test programs
Test programs: `test_compiler`, `test_vm`, `test_lex`, `test_quick`,
`test_compact`.
`test_compact`, `test_suite`.

View file

@ -52,33 +52,36 @@
// ------------------------------------------------------------
static void appendText(const char *path, const char *name, const char *value) {
static int32_t appendText(const char *path, const char *name, const char *value) {
if (!value || !value[0]) {
return;
return 0;
}
dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
return dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
}
static void emitIcon(const char *path, const BasBuildSpecT *spec) {
static int32_t emitIcon(const char *path, const BasBuildSpecT *spec) {
// If a disk path was given, load it and embed. Otherwise use the
// pre-loaded bytes (used by the IDE for its "noicon" fallback).
if (spec->iconPath && spec->iconPath[0]) {
int32_t iconLen = 0;
char *iconData = platformReadFile(spec->iconPath, &iconLen);
int32_t rc = 0;
if (iconData) {
dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
rc = dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
}
return;
return rc;
}
if (spec->iconData && spec->iconSize > 0) {
dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, spec->iconData, (uint32_t)spec->iconSize);
return dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, spec->iconData, (uint32_t)spec->iconSize);
}
return 0;
}
@ -92,47 +95,58 @@ int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
return -1;
}
// Accumulate every append/write result so a disk-full or I/O failure on
// any resource is reported instead of being silently treated as success.
int32_t rc = 0;
// Project metadata. Name is required -- fall back to a generic label
// so the compiled app always has something sensible to display.
const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : "BASIC App";
appendText(outPath, BAS_RES_NAME, projName);
rc |= appendText(outPath, BAS_RES_NAME, projName);
appendText(outPath, BAS_RES_AUTHOR, spec->author);
appendText(outPath, BAS_RES_PUBLISHER, spec->publisher);
appendText(outPath, BAS_RES_VERSION, spec->version);
appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author);
rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher);
rc |= appendText(outPath, BAS_RES_VERSION, spec->version);
rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
// Icon.
emitIcon(outPath, spec);
rc |= emitIcon(outPath, spec);
// Help file name (just the basename -- stub resolves it next to the app).
if (spec->helpFile && spec->helpFile[0]) {
const char *helpBase = platformPathBaseName(spec->helpFile);
appendText(outPath, BAS_RES_HELPFILE, helpBase);
rc |= appendText(outPath, BAS_RES_HELPFILE, helpBase);
}
// Bytecode module.
if (spec->moduleData && spec->moduleLen > 0) {
dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, spec->moduleData, (uint32_t)spec->moduleLen);
rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, spec->moduleData, (uint32_t)spec->moduleLen);
}
// Optional debug info.
if (spec->debugData && spec->debugLen > 0) {
dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, spec->debugData, (uint32_t)spec->debugLen);
rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, spec->debugData, (uint32_t)spec->debugLen);
}
// Form resources. Callers pre-strip / pre-obfuscate as needed; we just
// write them out as FORM0, FORM1, ...
// write them out as FORM0, FORM1, ... Number the OUTPUT densely with
// outIdx rather than the source index i: skipping an empty/NULL form
// by the source index would leave a numbering gap (FORM0, FORM2, ...)
// and the stub's reader stops at the first missing index, silently
// dropping every later form.
int32_t outIdx = 0;
for (int32_t i = 0; i < spec->formCount; i++) {
if (!spec->formData || !spec->formData[i] || !spec->formLens || spec->formLens[i] <= 0) {
continue;
}
char resName[16];
snprintf(resName, sizeof(resName), "FORM%ld", (long)i);
dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]);
snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx);
rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]);
outIdx++;
}
return 0;
return rc;
}

View file

@ -31,6 +31,18 @@
#ifndef BAS_RES_H
#define BAS_RES_H
#include <stdbool.h>
#include <stddef.h> // NULL
#include <stdint.h>
#include <strings.h> // strncasecmp
// dvxSkipWs is declared in platform/dvxPlat.h, which every translation
// unit that includes this header also includes. We forward-declare it
// here (a redundant, compatible declaration is legal C) so the inline
// helper below can use it without pulling the whole platform header into
// basRes.h's include surface.
const char *dvxSkipWs(const char *s);
// ------------------------------------------------------------
// [Project] section of a .dbp file
// ------------------------------------------------------------
@ -76,4 +88,73 @@
// Stub DXE embedded in the IDE app for release builds
#define BAS_RES_STUB "STUB"
// Per-form binary resource name, e.g. FORM0, FORM1, ... Shared by the
// basBuild writer and the basstub reader so the naming cannot drift; %ld is
// the dense form index.
#define BAS_RES_FORM_FMT "FORM%ld"
// ------------------------------------------------------------
// Form resource scan limit
// ------------------------------------------------------------
// Upper bound on FORM0..FORMn resources written by the compiler
// (bascomp/basBuild) and scanned by the stub at load time. Shared
// so the writer and reader cannot drift.
#define BAS_MAX_FORM_RESOURCES 256
// ------------------------------------------------------------
// Begin-Form name extraction (shared by bascomp and basstub)
// ------------------------------------------------------------
// Length of the "Begin Form " keyword prefix scanned for below.
#define BAS_BEGIN_FORM_PREFIX_LEN 11
// Extract the form name from a "Begin Form <name>" line in .frm text.
// Writes a NUL-terminated name into nameBuf (max nameBufSize bytes,
// including the terminator) and returns true if a name was found.
// Shared verbatim by bascomp (concat injection) and basstub (lazy
// frm registration); they previously duplicated this loop with only
// the destination buffer size differing.
static inline bool basExtractFormName(const char *frmText, char *nameBuf, int32_t nameBufSize) {
if (nameBuf == NULL || nameBufSize <= 0) {
return false;
}
nameBuf[0] = '\0';
if (frmText == NULL) {
return false;
}
const char *p = frmText;
while (*p) {
p = dvxSkipWs(p);
if (strncasecmp(p, "Begin Form ", BAS_BEGIN_FORM_PREFIX_LEN) == 0) {
p = dvxSkipWs(p + BAS_BEGIN_FORM_PREFIX_LEN);
int32_t ni = 0;
while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && ni < nameBufSize - 1) {
nameBuf[ni] = *p;
ni++;
p++;
}
nameBuf[ni] = '\0';
return nameBuf[0] != '\0';
}
while (*p && *p != '\n') {
p++;
}
if (*p == '\n') {
p++;
}
}
return false;
}
#endif // BAS_RES_H

View file

@ -32,8 +32,14 @@
#ifndef BAS_EVENTS_H
#define BAS_EVENTS_H
#include <stdbool.h>
// NULL-terminated list of event suffixes. Case-insensitive match.
// Declared extern here, defined once in strip.c.
extern const char *basEventSuffixes[];
// True when suffix case-insensitively equals one of basEventSuffixes.
// Defined once in strip.c (the owner of basEventSuffixes).
bool basEventSuffixMatch(const char *suffix);
#endif // BAS_EVENTS_H

View file

@ -35,7 +35,7 @@
// Function prototypes (alphabetical)
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
bool basAddData(BasCodeGenT *cg, BasValueT val);
void basAddData(BasCodeGenT *cg, BasValueT val);
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
@ -44,6 +44,7 @@ void basCodeGenInit(BasCodeGenT *cg);
int32_t basCodePos(const BasCodeGenT *cg);
void basEmit16(BasCodeGenT *cg, int16_t v);
void basEmit8(BasCodeGenT *cg, uint8_t b);
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len);
void basEmitDouble(BasCodeGenT *cg, double v);
void basEmitFloat(BasCodeGenT *cg, float v);
void basEmitU16(BasCodeGenT *cg, uint16_t v);
@ -58,6 +59,11 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
}
}
if (cg->constCount >= BAS_MAX_CONSTANTS) {
cg->overflow = true;
return 0;
}
uint16_t idx = (uint16_t)cg->constCount;
BasStringT *s = basStringNew(text, len);
arrput(cg->constants, s);
@ -66,11 +72,10 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
}
bool basAddData(BasCodeGenT *cg, BasValueT val) {
void basAddData(BasCodeGenT *cg, BasValueT val) {
BasValueT copy = basValCopy(val);
arrput(cg->dataPool, copy);
cg->dataCount = (int32_t)arrlen(cg->dataPool);
return true;
}
@ -133,6 +138,11 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
mod->dataPool = (BasValueT *)malloc(cg->dataCount * sizeof(BasValueT));
if (!mod->dataPool) {
// Release the constant pool refs taken above before freeing.
for (int32_t i = 0; i < cg->constCount; i++) {
basStringUnref(mod->constants[i]);
}
free(mod->constants);
free(mod->code);
free(mod);
@ -152,10 +162,11 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
if (mod->formVarInfo) {
memcpy(mod->formVarInfo, cg->formVarInfo, cg->formVarInfoCount * sizeof(BasFormVarInfoT));
}
// Keep count in sync with the pointer: on OOM the array stays
// NULL, so leaving count nonzero would make consumers deref NULL.
mod->formVarInfoCount = cg->formVarInfoCount;
}
}
// Copy debug variable info
if (cg->debugVarCount > 0) {
@ -163,10 +174,9 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
if (mod->debugVars) {
memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT));
}
mod->debugVarCount = cg->debugVarCount;
}
}
// Copy UDT type definitions for debugger
if (cg->debugUdtDefCount > 0) {
@ -187,10 +197,10 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
}
}
}
}
mod->debugUdtDefCount = cg->debugUdtDefCount;
}
}
return mod;
}
@ -324,11 +334,7 @@ int32_t basCodePos(const BasCodeGenT *cg) {
void basEmit16(BasCodeGenT *cg, int16_t v) {
uint8_t buf[2];
memcpy(buf, &v, 2);
arrput(cg->code, buf[0]);
arrput(cg->code, buf[1]);
cg->codeLen = (int32_t)arrlen(cg->code);
basEmitBytes(cg, &v, (int32_t)sizeof(v));
}
@ -338,36 +344,29 @@ void basEmit8(BasCodeGenT *cg, uint8_t b) {
}
void basEmitDouble(BasCodeGenT *cg, double v) {
uint8_t buf[sizeof(double)];
memcpy(buf, &v, sizeof(double));
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len) {
const uint8_t *p = (const uint8_t *)data;
for (int32_t i = 0; i < (int32_t)sizeof(double); i++) {
arrput(cg->code, buf[i]);
for (int32_t i = 0; i < len; i++) {
arrput(cg->code, p[i]);
}
cg->codeLen = (int32_t)arrlen(cg->code);
}
void basEmitDouble(BasCodeGenT *cg, double v) {
basEmitBytes(cg, &v, (int32_t)sizeof(v));
}
void basEmitFloat(BasCodeGenT *cg, float v) {
uint8_t buf[sizeof(float)];
memcpy(buf, &v, sizeof(float));
for (int32_t i = 0; i < (int32_t)sizeof(float); i++) {
arrput(cg->code, buf[i]);
}
cg->codeLen = (int32_t)arrlen(cg->code);
basEmitBytes(cg, &v, (int32_t)sizeof(v));
}
void basEmitU16(BasCodeGenT *cg, uint16_t v) {
uint8_t buf[2];
memcpy(buf, &v, 2);
arrput(cg->code, buf[0]);
arrput(cg->code, buf[1]);
cg->codeLen = (int32_t)arrlen(cg->code);
basEmitBytes(cg, &v, (int32_t)sizeof(v));
}

View file

@ -37,6 +37,14 @@
#include <stdint.h>
#include <stdbool.h>
// Constant pool index is emitted as uint16_t; pool cannot exceed this.
#define BAS_MAX_CONSTANTS 0x10000
// Jump offsets are signed 16-bit and call addresses are uint16_t; a module
// larger than this would silently miscompile when those values wrap. This
// conservative bound keeps every offset and address in range.
#define BAS_MAX_CODE_SIZE 32767
// ============================================================
// Code generator state
// ============================================================
@ -56,6 +64,7 @@ typedef struct {
int32_t debugProcCount; // incremented per SUB/FUNCTION for debug var tracking
BasDebugUdtDefT *debugUdtDefs; // stb_ds dynamic array
int32_t debugUdtDefCount;
bool overflow; // set if a pool index would exceed 16 bits
} BasCodeGenT;
// ============================================================
@ -89,8 +98,8 @@ void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
// Add a string to the constant pool. Returns the pool index.
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
// Add a value to the data pool (for DATA statements). Returns true on success.
bool basAddData(BasCodeGenT *cg, BasValueT val);
// Adds a value to the data pool (for DATA statements).
void basAddData(BasCodeGenT *cg, BasValueT val);
// Build a BasModuleT from the generated code. The caller takes
// ownership of the module and must free it with basModuleFree().

View file

@ -28,7 +28,7 @@
//
// Address references:
// - BasProcEntryT::codeAddr (absolute)
// - BasFormVarInfoT::initCodeAddr (absolute, 0 = no init)
// - BasFormVarInfoT::initCodeAddr (absolute, negative = no init)
// - OP_CALL operand (absolute uint16)
// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE operand (relative int16)
// - OP_FOR_NEXT loopTop operand (relative int16)
@ -46,6 +46,8 @@
#include <stdlib.h>
#include <string.h>
#define BAS_OPERAND_U16_MAX 0xFFFF // max absolute address encodable in a uint16 operand
// Function prototypes (alphabetical)
int32_t basCompactBytecode(BasModuleT *mod);
@ -55,7 +57,7 @@ static int32_t opOperandSize(uint8_t op);
static int16_t readI16LE(const uint8_t *p);
static int32_t readI32LE(const uint8_t *p);
static uint16_t readU16LE(const uint8_t *p);
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen);
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t codeLen, int32_t newCodeLen);
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero);
static void writeI16LE(uint8_t *p, int16_t v);
static void writeI32LE(uint8_t *p, int32_t v);
@ -136,7 +138,7 @@ int32_t basCompactBytecode(BasModuleT *mod) {
case OP_CALL: {
uint16_t oldAddr = readU16LE(oldCode + oldPc + 1);
if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, newCodeLen)) {
if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, oldCodeLen, newCodeLen)) {
ok = false;
}
@ -386,6 +388,9 @@ static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
// Opcode operand size table
// ============================================================
// Returns operand byte count (excluding the 1-byte opcode), or -1 if unknown.
// This switch is the single machine-readable operand-size table. The
// trailing comments in opcodes.h are human hints and must be kept in
// sync with this function when adding opcodes.
static int32_t opOperandSize(uint8_t op) {
switch (op) {
// No operand bytes
@ -529,14 +534,20 @@ static uint16_t readU16LE(const uint8_t *p) {
//
// Returns true on success, false if an offset overflows int16 or a
// target doesn't land on a valid instruction in the old code.
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen) {
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t codeLen, int32_t newCodeLen) {
// oldAddr is an arbitrary uint16 operand; remap[] only has codeLen+1
// entries, so a corrupt CALL address would index out of bounds.
if (oldAddr > codeLen) {
return false;
}
int32_t newAddr = remap[oldAddr];
if (newAddr < 0 || newAddr > newCodeLen) {
return false;
}
if (newAddr > 0xFFFF) {
if (newAddr > BAS_OPERAND_U16_MAX) {
return false;
}
@ -573,6 +584,15 @@ static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffse
int32_t newOffset = newTarget - newPcAfter;
// A nonzero ON ERROR handler offset that collapses to 0 would be misread
// by the VM as "disable handler" (0 is the disable sentinel). The
// oldOffset == 0 disable case was already handled above, so a 0 here means
// a real handler collapsed -- abort compaction so the original
// (uncompacted) bytecode with a valid offset is retained.
if (allowZero && newOffset == 0) {
return false;
}
if (newOffset < INT16_MIN || newOffset > INT16_MAX) {
return false;
}

View file

@ -181,7 +181,9 @@ static const KeywordEntryT sKeywords[] = {
// Function prototypes (alphabetical)
static char advance(BasLexerT *lex);
static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c);
static bool atEnd(const BasLexerT *lex);
bool basIsTypeSuffixChar(char c);
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
const char *basLexerKeywordAt(int32_t i);
BasKeywordClassE basLexerKeywordClass(int32_t i);
@ -190,6 +192,7 @@ BasTokenTypeE basLexerNext(BasLexerT *lex);
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
const char *basTokenName(BasTokenTypeE type);
static BasTokenTypeE lookupKeyword(const char *text, int32_t len);
static BasTokenTypeE makeNewlineToken(BasLexerT *lex);
static char peek(const BasLexerT *lex);
static char peekNext(const BasLexerT *lex);
static void setError(BasLexerT *lex, const char *msg);
@ -219,11 +222,23 @@ static char advance(BasLexerT *lex) {
}
static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c) {
if (*idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[(*idx)++] = c;
}
}
static bool atEnd(const BasLexerT *lex) {
return lex->pos >= lex->sourceLen;
}
bool basIsTypeSuffixChar(char c) {
return c == '%' || c == '&' || c == '!' || c == '#' || c == '$';
}
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) {
memset(lex, 0, sizeof(*lex));
lex->source = source;
@ -294,11 +309,7 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
// Newline
if (c == '\n') {
advance(lex);
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
return makeNewlineToken(lex);
}
// Carriage return (handle CR, CRLF)
@ -309,21 +320,13 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
advance(lex);
}
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
return makeNewlineToken(lex);
}
// Comment (apostrophe)
if (c == '\'') {
skipLineComment(lex);
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
return makeNewlineToken(lex);
}
// String literal
@ -521,7 +524,7 @@ static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
// Case-insensitive keyword lookup. Short-circuits on length mismatch
// (via cached keyword length) and on the very first character, both of
// which reject the vast majority of entries before doing a full scan.
char firstUp = (text[0] >= 'a' && text[0] <= 'z') ? (char)(text[0] - 32) : text[0];
char firstUp = upperChar(text[0]);
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
const KeywordEntryT *kw = &sKeywords[i];
@ -548,6 +551,17 @@ static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
}
// Fill the current token as a synthesized newline (used for real newlines,
// CR/CRLF, and the end of apostrophe / REM comments).
static BasTokenTypeE makeNewlineToken(BasLexerT *lex) {
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
}
static char peek(const BasLexerT *lex) {
if (atEnd(lex)) {
return '\0';
@ -646,7 +660,7 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
}
int32_t idx = 0;
int32_t value = 0;
uint64_t value = 0;
for (;;) {
if (atEnd(lex)) {
@ -671,12 +685,16 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
}
advance(lex);
appendTokenChar(lex, &idx, c);
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = c;
value = (value << shift) | (uint64_t)digit;
}
value = (value << shift) | digit;
// No digits after the &H/&O/&B marker is a typo, not the integer 0.
if (idx == 0) {
setError(lex, "Empty hexadecimal/octal/binary literal");
lex->token.type = TOK_ERROR;
return TOK_ERROR;
}
lex->token.text[idx] = '\0';
@ -689,7 +707,7 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
return TOK_LONG_LIT;
}
lex->token.intVal = value;
lex->token.intVal = (int32_t)value;
return TOK_INT_LIT;
}
@ -698,11 +716,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
int32_t idx = 0;
while (!atEnd(lex) && (isalnum((unsigned char)peek(lex)) || peek(lex) == '_')) {
char c = advance(lex);
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = c;
}
appendTokenChar(lex, &idx, advance(lex));
}
lex->token.text[idx] = '\0';
@ -712,9 +726,9 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
if (!atEnd(lex)) {
char c = peek(lex);
if (c == '%' || c == '&' || c == '!' || c == '#' || c == '$') {
if (basIsTypeSuffixChar(c)) {
advance(lex);
lex->token.text[idx++] = c;
appendTokenChar(lex, &idx, c);
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
}
@ -727,7 +741,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
if (baseLen > 0) {
char last = lex->token.text[baseLen - 1];
if (last == '%' || last == '&' || last == '!' || last == '#' || last == '$') {
if (basIsTypeSuffixChar(last)) {
baseLen--;
}
}
@ -743,14 +757,12 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
kwType = lookupKeyword(lex->token.text, baseLen);
}
// REM is a comment -- skip to end of line
if (kwType == TOK_REM) {
// REM is a comment -- skip to end of line. Apply the same acceptance
// gate as below so a suffixed identifier like REM$ stays an identifier
// instead of swallowing the rest of the line as a comment.
if (kwType == TOK_REM && (baseLen == idx || matchedWithSuffix)) {
skipLineComment(lex);
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
return makeNewlineToken(lex);
}
// Accept the keyword if it's a plain keyword (no suffix on source) or
@ -770,42 +782,50 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
// Integer part
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
}
appendTokenChar(lex, &idx, advance(lex));
}
// Decimal part
if (!atEnd(lex) && peek(lex) == '.' && isdigit((unsigned char)peekNext(lex))) {
hasDecimal = true;
lex->token.text[idx++] = advance(lex); // .
appendTokenChar(lex, &idx, advance(lex)); // .
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
}
appendTokenChar(lex, &idx, advance(lex));
}
}
// Exponent
if (!atEnd(lex) && (upperChar(peek(lex)) == 'E' || upperChar(peek(lex)) == 'D')) {
hasExp = true;
lex->token.text[idx++] = advance(lex);
// Exponent. Accept QBASIC/Fortran 'D' (double) as well as 'E', but
// always store 'E' in the buffer so atof() can parse it. Require at
// least one digit after the marker (and optional sign); otherwise the
// marker is not part of the number (e.g. "1E" tokenizes as 1 then E).
char marker = upperChar(peek(lex));
if (!atEnd(lex) && (marker == 'E' || marker == 'D')) {
int32_t savedIdx = idx;
int32_t savedPos = lex->pos;
int32_t savedLine = lex->line;
int32_t savedCol = lex->col;
advance(lex); // consume marker
appendTokenChar(lex, &idx, 'E'); // always store 'E', never 'D'
if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) {
lex->token.text[idx++] = advance(lex);
appendTokenChar(lex, &idx, advance(lex));
}
if (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
hasExp = true;
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
appendTokenChar(lex, &idx, advance(lex));
}
} else {
// Not a valid exponent -- roll back the consumed marker/sign.
idx = savedIdx;
lex->pos = savedPos;
lex->line = savedLine;
lex->col = savedCol;
}
}
@ -828,13 +848,7 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
return TOK_LONG_LIT;
}
if (c == '!') {
advance(lex);
lex->token.dblVal = atof(lex->token.text);
return TOK_FLOAT_LIT;
}
if (c == '#') {
if (c == '!' || c == '#') {
advance(lex);
lex->token.dblVal = atof(lex->token.text);
return TOK_FLOAT_LIT;
@ -863,13 +877,17 @@ static BasTokenTypeE tokenizeString(BasLexerT *lex) {
advance(lex); // skip opening quote
int32_t idx = 0;
bool overflow = false;
while (!atEnd(lex) && peek(lex) != '"' && peek(lex) != '\n' && peek(lex) != '\r') {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
// appendTokenChar silently drops chars once the buffer is full;
// detect that here so over-long literals are diagnosed instead
// of truncated. Keep consuming so the lexer skips the whole
// literal and its closing quote.
if (idx >= BAS_MAX_STRING_LEN) {
overflow = true;
}
appendTokenChar(lex, &idx, advance(lex));
}
if (atEnd(lex) || peek(lex) != '"') {
@ -884,6 +902,13 @@ static BasTokenTypeE tokenizeString(BasLexerT *lex) {
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
if (overflow) {
char buf[BAS_LEX_ERROR_LEN];
snprintf(buf, sizeof(buf), "String literal too long (maximum is %d characters)", (int)BAS_MAX_STRING_LEN);
setError(lex, buf);
return TOK_ERROR;
}
return TOK_STRING_LIT;
}

View file

@ -70,13 +70,6 @@ typedef enum {
TOK_LE, // <=
TOK_GE, // >=
// Type suffixes (attached to identifier)
TOK_SUFFIX_INT, // %
TOK_SUFFIX_LONG, // &
TOK_SUFFIX_SINGLE, // !
TOK_SUFFIX_DOUBLE, // #
TOK_SUFFIX_STRING, // $
// Keywords
TOK_AND,
TOK_APP,
@ -214,6 +207,9 @@ typedef enum {
// ============================================================
#define BAS_MAX_TOKEN_LEN 256
// Maximum user-visible string-literal length. One byte of the token
// buffer is reserved for the NUL terminator.
#define BAS_MAX_STRING_LEN (BAS_MAX_TOKEN_LEN - 1)
#define BAS_LEX_ERROR_LEN 256
typedef struct {
@ -225,7 +221,6 @@ typedef struct {
union {
int32_t intVal;
int64_t longVal;
float fltVal;
double dblVal;
};
@ -265,6 +260,9 @@ BasTokenTypeE basLexerPeek(const BasLexerT *lex);
// Return human-readable name for a token type.
const char *basTokenName(BasTokenTypeE type);
// True when c is a BASIC type-suffix character (% & ! # $).
bool basIsTypeSuffixChar(char c);
// ============================================================
// Keyword iteration
// ============================================================
@ -279,8 +277,9 @@ typedef enum {
BAS_KW_CLASS_LITERAL = 2 // TRUE, FALSE, NOTHING
} BasKeywordClassE;
// Number of keywords in the table (excluding the trailing NULL sentinel
// and any duplicate dollar-suffixed aliases like DIR$/DIR).
// Number of keywords in the table (excluding only the trailing NULL
// sentinel). Dollar-suffixed aliases (DIR$, CURDIR$, etc.) are counted
// as separate entries, consistent with basLexerKeywordAt below.
int32_t basLexerKeywordCount(void);
// Return the text of keyword i (uppercase). i must be in [0, count).

View file

@ -54,7 +54,6 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map);
static int32_t findFormEndPos(const char *text, int32_t len);
static bool isEventSuffix(const char *suffix);
static bool isIdentChar(int c);
static bool isValidIdent(const char *name);
static const char *nameMapAdd(NameMapT *m, const char *name);
@ -327,18 +326,6 @@ static int32_t findFormEndPos(const char *text, int32_t len) {
}
// Check if suffix is a known event name.
static bool isEventSuffix(const char *suffix) {
for (int32_t i = 0; basEventSuffixes[i]; i++) {
if (strcasecmp(suffix, basEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
// ============================================================
// Pass 3: rewrite .frm text with mapped names
// ============================================================
@ -530,6 +517,14 @@ static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *m
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
// KNOWN LIMITATION: the constant pool dedupes a plain string literal and a
// same-text control/form-name reference into ONE entry, so a release build
// that does e.g. MsgBox "Save" while also having a control named "Save"
// will see that literal rewritten to the obfuscated name ("C5"). Fully
// separating name references from literals would give name refs their own
// pool slots and change emitted bytecode for every release build, so it is
// deferred; avoid string literals that exactly match a control/form name
// in release builds.
for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i];
@ -585,7 +580,7 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
const char *suffix = underscore + 1;
if (!isEventSuffix(suffix)) {
if (!basEventSuffixMatch(suffix)) {
continue;
}

File diff suppressed because it is too large Load diff

View file

@ -70,6 +70,12 @@ typedef struct {
} BasCtrlValidatorT;
// Forward jump list (for EXIT FOR / EXIT DO backpatching)
typedef struct {
int32_t *patchAddr; // stb_ds dynamic array of code positions to backpatch
} ExitListT;
typedef struct {
BasLexerT lex;
BasCodeGenT cg;
@ -80,13 +86,24 @@ typedef struct {
int32_t prevLine; // line of the previous token (for error reporting)
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
int32_t optionBase; // default array lower bound (0 or 1)
bool optionCompareText; // true = case-insensitive string comparison
bool optionExplicit; // true = variables must be declared with DIM
uint8_t defType[26]; // default type per letter (A-Z), set by DEFINT etc.
char currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION
// Per-form init block tracking
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
int32_t formInitCodeStart; // code position where init block starts (-1 = none)
// EXIT FOR/DO/SUB/FUNCTION forward-jump backpatch lists
ExitListT exitForList;
ExitListT exitDoList;
ExitListT exitSubList;
ExitListT exitFuncList;
// SELECT CASE keeps its test value on the eval stack for the whole
// construct. selectDepth counts how many are currently live; the
// nearest FOR/DO loop records the depth at its start so EXIT and
// GOTO can emit one OP_POP per SELECT they jump out of.
int32_t selectDepth;
int32_t forSelectBase;
int32_t doSelectBase;
// Optional compile-time CtrlName.Member validator (IDE-only).
const BasCtrlValidatorT *validator;
} BasParserT;

View file

@ -58,9 +58,22 @@ const char *basEventSuffixes[] = {
// Function prototypes (alphabetical)
bool basEventSuffixMatch(const char *suffix);
void basStripModule(BasModuleT *mod);
static bool nameEndsWithEventSuffix(const char *name);
static bool nameInConstantPool(const BasModuleT *mod, const char *name);
static bool procNameTaken(const BasModuleT *mod, int32_t selfIdx, const char *name);
bool basEventSuffixMatch(const char *suffix) {
for (int32_t i = 0; basEventSuffixes[i]; i++) {
if (strcasecmp(suffix, basEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
void basStripModule(BasModuleT *mod) {
@ -104,7 +117,18 @@ void basStripModule(BasModuleT *mod) {
continue;
}
snprintf(proc->name, sizeof(proc->name), "F%ld", (long)nextMangled++);
// Skip any generated name that collides with a kept proc (event
// handler or constant-pool-referenced) or a constant-pool entry, so
// basModuleFindProc can't resolve the wrong procedure at runtime.
char cand[BAS_MAX_PROC_NAME];
snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);
while (procNameTaken(mod, i, cand) || nameInConstantPool(mod, cand)) {
snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);
}
snprintf(proc->name, sizeof(proc->name), "%s", cand);
}
}
@ -116,15 +140,7 @@ static bool nameEndsWithEventSuffix(const char *name) {
return false;
}
const char *suffix = underscore + 1;
for (int32_t i = 0; basEventSuffixes[i]; i++) {
if (strcasecmp(suffix, basEventSuffixes[i]) == 0) {
return true;
}
}
return false;
return basEventSuffixMatch(underscore + 1);
}
@ -139,3 +155,18 @@ static bool nameInConstantPool(const BasModuleT *mod, const char *name) {
return false;
}
static bool procNameTaken(const BasModuleT *mod, int32_t selfIdx, const char *name) {
for (int32_t i = 0; i < mod->procCount; i++) {
if (i == selfIdx) {
continue;
}
if (strcasecmp(mod->procs[i].name, name) == 0) {
return true;
}
}
return false;
}

View file

@ -29,17 +29,26 @@
#include <stdlib.h>
#include <string.h>
// Distance from a lowercase ASCII letter to its uppercase counterpart
// ('a' - 'A'). Used by basAsciiUpper to fold case without touching
// bytes outside a-z.
#define BAS_ASCII_CASE_OFFSET 32
// Function prototypes (alphabetical)
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
int32_t basSymTabAllocSlot(BasSymTabT *tab);
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
void basSymTabEnterLocal(BasSymTabT *tab);
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
void basSymTabFree(BasSymTabT *tab);
void basSymTabInit(BasSymTabT *tab);
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
void basSymTabLeaveLocal(BasSymTabT *tab);
static char basAsciiUpper(char c);
static void basSymbolFree(BasSymbolT *sym);
static bool namesEqual(const char *a, const char *b);
static uint32_t nameHashCI(const char *name);
@ -100,6 +109,11 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
}
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab) {
return tab->nextGlobalIdx++;
}
int32_t basSymTabAllocSlot(BasSymTabT *tab) {
if (tab->inLocalScope) {
return tab->nextLocalIdx++;
@ -176,6 +190,19 @@ BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
}
void basSymTabFree(BasSymTabT *tab) {
// Free every remaining symbol's dynamic arrays and the struct, then
// release the symbol-pointer array itself.
for (int32_t i = 0; i < tab->count; i++) {
basSymbolFree(tab->symbols[i]);
}
arrfree(tab->symbols);
tab->symbols = NULL;
tab->count = 0;
}
void basSymTabInit(BasSymTabT *tab) {
memset(tab, 0, sizeof(*tab));
}
@ -207,9 +234,7 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
for (int32_t i = 0; i < tab->count; i++) {
if (tab->symbols[i]->scope == SCOPE_LOCAL) {
arrfree(tab->symbols[i]->patchAddrs);
arrfree(tab->symbols[i]->fields);
free(tab->symbols[i]);
basSymbolFree(tab->symbols[i]);
} else {
if (i != newCount) {
tab->symbols[newCount] = tab->symbols[i];
@ -226,6 +251,30 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
}
// ============================================================
// Fold a single ASCII byte to upper case. Char in, char out (not int
// via toupper) so bytes >= 0x80 pass through unchanged and the uint8_t
// then uint32_t cast in nameHashCI stays well defined.
// ============================================================
static char basAsciiUpper(char c) {
if (c >= 'a' && c <= 'z') {
c -= BAS_ASCII_CASE_OFFSET;
}
return c;
}
// ============================================================
// Free a single symbol: its dynamic arrays and the struct itself.
// ============================================================
static void basSymbolFree(BasSymbolT *sym) {
arrfree(sym->patchAddrs);
arrfree(sym->fields);
free(sym);
}
// ============================================================
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
// Caller computes hash of search-name once; each entry stores its own
@ -236,10 +285,7 @@ static uint32_t nameHashCI(const char *name) {
uint32_t h = 0x811C9DC5u;
while (*name) {
char c = *name;
if (c >= 'a' && c <= 'z') {
c -= 32;
}
char c = basAsciiUpper(*name);
h ^= (uint32_t)(uint8_t)c;
h *= 0x01000193u;
name++;
@ -254,8 +300,8 @@ static uint32_t nameHashCI(const char *name) {
// ============================================================
static bool namesEqual(const char *a, const char *b) {
while (*a && *b) {
char ca = *a >= 'a' && *a <= 'z' ? *a - 32 : *a;
char cb = *b >= 'a' && *b <= 'z' ? *b - 32 : *b;
char ca = basAsciiUpper(*a);
char cb = basAsciiUpper(*b);
if (ca != cb) {
return false;

View file

@ -57,6 +57,7 @@ typedef enum {
#define BAS_MAX_SYMBOL_NAME 64
#define BAS_MAX_PARAMS 16
#define BAS_MAX_CONST_STR 256 // max CONST string-literal length
// UDT field definition
typedef struct {
@ -66,31 +67,20 @@ typedef struct {
} BasFieldDefT;
typedef struct {
char name[BAS_MAX_SYMBOL_NAME];
// Fields are ordered by alignment (4-byte, then 2-byte, then 1-byte) to
// minimize struct padding on the 32-bit target.
uint32_t nameHash; // FNV-1a over uppercase(name); fast-reject during lookup
BasSymKindE kind;
BasScopeE scope;
uint8_t dataType; // BAS_TYPE_* for variables/functions
int32_t index; // slot index (local or global)
int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL
int32_t localCount; // number of local variables (for SUB/FUNCTION, set on leave)
bool isDefined; // false = forward-declared
bool isArray;
bool isShared;
bool isExtern; // true = external library function (DECLARE LIBRARY)
bool formScopeEnded; // true = form scope ended, invisible to lookups
char formName[BAS_MAX_SYMBOL_NAME]; // form name for SCOPE_FORM vars
int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol
int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length)
uint16_t externLibIdx; // constant pool index for library name (if isExtern)
uint16_t externFuncIdx; // constant pool index for function name (if isExtern)
// For SUB/FUNCTION: parameter info
int32_t paramCount;
int32_t requiredParams; // count of non-optional params
uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter
// Forward-reference backpatch list (code addresses to patch when defined)
int32_t *patchAddrs; // stb_ds dynamic array
@ -101,11 +91,27 @@ typedef struct {
int32_t constInt;
double constDbl;
};
char constStr[256];
// For TYPE_DEF: field definitions
BasFieldDefT *fields; // stb_ds dynamic array
int32_t fieldCount;
uint16_t externLibIdx; // constant pool index for library name (if isExtern)
uint16_t externFuncIdx; // constant pool index for function name (if isExtern)
uint8_t dataType; // BAS_TYPE_* for variables/functions
bool isDefined; // false = forward-declared
bool isArray;
bool isShared;
bool isExtern; // true = external library function (DECLARE LIBRARY)
bool formScopeEnded; // true = form scope ended, invisible to lookups
char name[BAS_MAX_SYMBOL_NAME];
char formName[BAS_MAX_SYMBOL_NAME]; // form name for SCOPE_FORM vars
uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter
char constStr[BAS_MAX_CONST_STR];
} BasSymbolT;
// ============================================================
@ -136,8 +142,11 @@ typedef struct {
void basSymTabInit(BasSymTabT *tab);
// Add a symbol. Returns the symbol pointer, or NULL if the table is full
// or the name already exists in the current scope.
// Free all symbols and the symbol array (called at parser teardown).
void basSymTabFree(BasSymTabT *tab);
// Add a symbol. Returns the symbol pointer, or NULL if the name already
// exists in the current scope or memory allocation fails.
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
// Look up a symbol by name. Searches local scope first, then global.
@ -156,6 +165,11 @@ void basSymTabLeaveLocal(BasSymTabT *tab);
// Allocate the next variable slot (global, local, or form depending on scope).
int32_t basSymTabAllocSlot(BasSymTabT *tab);
// Allocate the next global variable slot unconditionally, ignoring the
// current local/form scope flags. Used when a symbol's final scope is
// SCOPE_GLOBAL but the table is currently inside a SUB/FUNCTION or form.
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
// Enter form scope (called at BEGINFORM). Form-level DIMs create SCOPE_FORM variables.
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);

File diff suppressed because it is too large Load diff

View file

@ -49,8 +49,14 @@ typedef struct BasControlT BasControlT;
// ============================================================
#define BAS_MAX_CTRL_NAME 32
// Form names use the full identifier length (must equal the compiler-side
// BAS_MAX_PROC_NAME / BAS_MAX_SYMBOL_NAME in vm.h); otherwise a 32-63 char
// form name truncates here and form-scope variable binding (keyed by name)
// silently fails to match the module's 63-char formName.
#define BAS_MAX_FORM_NAME 64
#define BAS_MAX_FRM_LINE_LEN 512
#define BAS_MAX_FRM_NESTING 16
#define BAS_MAX_PENDING_UNLOAD 8
// ============================================================
// Menu ID to name mapping for event dispatch
@ -65,7 +71,8 @@ typedef struct {
// Named popup / context menu owned by a form.
typedef struct {
char name[BAS_MAX_CTRL_NAME];
MenuT *menu; // standalone wmCreateMenu allocation, owned
MenuT *menu; // wmCreateMenu root, or a submenu owned by its root
bool ownsMenu; // true only for the root that wmFreeMenu must free
} BasFrmPopupMenuT;
// ============================================================
@ -110,7 +117,7 @@ typedef struct BasControlT {
// ============================================================
typedef struct BasFormT {
char name[BAS_MAX_CTRL_NAME]; // form name (e.g. "Form1")
char name[BAS_MAX_FORM_NAME]; // form name (e.g. "Form1")
WindowT *window; // DVX window
WidgetT *root; // widget root (from wgtInitWindow)
WidgetT *contentBox; // VBox/HBox for user controls
@ -142,6 +149,10 @@ typedef struct BasFormT {
// work the same as for menu-bar items. Freed on form unload.
BasFrmPopupMenuT *popupMenus; // stb_ds array
int32_t popupMenuCount;
// True from the moment Unload is committed (QueryUnload not
// cancelled) until the form is freed. Blocks re-entrant Unload of
// the same form from its own Unload/QueryUnload handlers.
bool unloading;
// Synthetic control entry for the form itself, so that
// FormName.Property works through the same getProp/setProp path.
BasControlT formCtrl;
@ -153,14 +164,14 @@ typedef struct BasFormT {
// Cached .frm source for reload after unload
typedef struct {
char formName[BAS_MAX_CTRL_NAME];
char formName[BAS_MAX_FORM_NAME];
char *frmSource; // malloc'd copy of .frm text
int32_t frmSourceLen;
} BasFrmCacheT;
// Cached compiled form binary (for standalone apps)
typedef struct {
char formName[BAS_MAX_CTRL_NAME];
char formName[BAS_MAX_FORM_NAME];
uint8_t *data; // malloc'd binary data
int32_t dataLen;
} BasCfmCacheT;
@ -188,6 +199,16 @@ typedef struct {
// were seeing when the dialog's event pump raced with other
// queued events.
bool suppressErrorDialog;
// Deferred unload. While eventDepth > 0 a VM event handler is on
// the stack, so Unload must not free the form's vars and controls
// out from under it (the VM dereferences currentFormVars for every
// form-variable access, and fireCtrlEvent writes into the firing
// control after the handler returns). QueryUnload/Unload events
// fire immediately; the frees are queued here and flushed when the
// outermost handler returns (see rtEventLeave).
int32_t eventDepth;
BasFormT *pendingUnload[BAS_MAX_PENDING_UNLOAD];
int32_t pendingUnloadCount;
// Name of the most recent basFormRtFindCtrl lookup. The VM's
// OP_FIND_CTRL opcode discards the name string after the lookup,
// so when a subsequent OP_CALL_METHOD / OP_SET_PROP sees a NULL
@ -219,6 +240,10 @@ void basFormRtRunSimple(BasFormRtT *rt);
// Destroy the form runtime and all forms/controls.
void basFormRtDestroy(BasFormRtT *rt);
// Close all serial/secLink resources and unregister the idle pollers.
// Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook.
void basFormRtSerialShutdown(void);
// Wire up the VM's UI callbacks to this form runtime.
void basFormRtBindVm(BasFormRtT *rt);

View file

@ -34,6 +34,7 @@
#define FRM_MAX_LINE_LEN 512
#define FRM_MAX_TOKEN_LEN 64
#define FRM_MAX_NESTING 16
#define FRM_MAX_VB_VERSION 2.0 // highest VB form VERSION we can import (VB4+ rejected)
// Prototypes (alphabetical)
@ -41,7 +42,7 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
bool frmParseBool(const char *val);
void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
void frmStripQuotes(char *val);
static bool readToken(const char **p, const char *end, char *buf, int32_t bufMax);
static bool readToken(const char **p, char *buf, int32_t bufMax);
typedef enum {
@ -97,13 +98,13 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
}
// "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import)
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
const char *ver = trimmed + 8;
if (strncasecmp(trimmed, "VERSION ", sizeof("VERSION ") - 1) == 0) {
const char *ver = trimmed + sizeof("VERSION ") - 1;
if (strncasecmp(ver, "DVX ", 4) != 0) {
if (strncasecmp(ver, "DVX ", sizeof("DVX ") - 1) != 0) {
double vbVer = atof(ver);
if (vbVer > 2.0) {
if (vbVer > FRM_MAX_VB_VERSION) {
return false; // VB4+ form, not compatible
}
}
@ -112,49 +113,71 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
}
// "Begin TypeName CtrlName"
if (strncasecmp(trimmed, "Begin ", 6) == 0) {
const char *rest = trimmed + 6;
if (strncasecmp(trimmed, "Begin ", sizeof("Begin ") - 1) == 0) {
const char *rest = trimmed + sizeof("Begin ") - 1;
char typeName[FRM_MAX_TOKEN_LEN];
char ctrlName[FRM_MAX_TOKEN_LEN];
BlkTypeE blkType;
bool doPush = false;
readToken(&rest, NULL, typeName, FRM_MAX_TOKEN_LEN);
readToken(&rest, typeName, FRM_MAX_TOKEN_LEN);
rest = dvxSkipWs(rest);
readToken(&rest, NULL, ctrlName, FRM_MAX_TOKEN_LEN);
readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN);
if (typeName[0] == '\0') {
continue;
}
// Classify the block first so overflow is checked once and all
// begin side-effects stay in lockstep with the stack push.
if (strcasecmp(typeName, "Form") == 0) {
blkType = BLK_FORM;
doPush = true;
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
blkType = BLK_MENU;
doPush = true;
} else if (inForm) {
blkType = BLK_CTRL;
doPush = true;
}
if (!doPush) {
continue;
}
// Hard-error on overflow before any callback or state mutation so
// begins and pushes never desync.
if (blockDepth >= FRM_MAX_NESTING) {
return false;
}
switch (blkType) {
case BLK_FORM:
if (cb->onFormBegin && !cb->onFormBegin(cb->userData, ctrlName)) {
return false;
}
inForm = true;
break;
if (blockDepth < FRM_MAX_NESTING) {
blockStack[blockDepth++] = BLK_FORM;
}
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
case BLK_MENU:
if (cb->onMenuBegin) {
cb->onMenuBegin(cb->userData, ctrlName, menuNestDepth);
}
menuNestDepth++;
break;
if (blockDepth < FRM_MAX_NESTING) {
blockStack[blockDepth++] = BLK_MENU;
}
} else if (inForm) {
case BLK_CTRL:
if (cb->onCtrlBegin) {
cb->onCtrlBegin(cb->userData, typeName, ctrlName);
}
if (blockDepth < FRM_MAX_NESTING) {
blockStack[blockDepth++] = BLK_CTRL;
}
break;
}
blockStack[blockDepth++] = blkType;
continue;
}
@ -289,16 +312,12 @@ void frmStripQuotes(char *val) {
// Read a whitespace-delimited token starting at *p. Advances *p past
// the token. If end is non-NULL, reading stops before end.
static bool readToken(const char **p, const char *end, char *buf, int32_t bufMax) {
// the token.
static bool readToken(const char **p, char *buf, int32_t bufMax) {
const char *cur = *p;
int32_t len = 0;
while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n' && len < bufMax - 1) {
if (end && cur >= end) {
break;
}
buf[len++] = *cur++;
}

View file

@ -35,6 +35,7 @@
#include "stb_ds_wrap.h"
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -49,6 +50,10 @@
#define DEFAULT_CTRL_W 100
#define DEFAULT_CTRL_H 30
#define MIN_CTRL_SIZE 8
#define DSGN_INDENT_SPACES 4
#define DSGN_MAX_INDENT_BUF 32
#define DSGN_MAX_NEST_DEPTH 32
#define DSGN_SEL_HANDLE_COUNT 8 // resize handles drawn around a selected control
// ============================================================
@ -71,7 +76,8 @@ static void dsgnLoad_onFormProp(void *userData, const char *key, const c
static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level);
static void dsgnLoad_onMenuEnd(void *userData);
static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value);
static const char *getPropValue(const DsgnControlT *ctrl, const char *name);
static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count);
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
static void rebuildWidgets(DsgnStateT *ds);
@ -88,7 +94,7 @@ typedef struct {
DsgnFormT *form;
DsgnControlT *current;
int32_t curMenuItemIdx;
char parentStack[8][DSGN_MAX_NAME];
char parentStack[BAS_MAX_FRM_NESTING][DSGN_MAX_NAME];
int32_t nestDepth;
bool containerStack[BAS_MAX_FRM_NESTING];
int32_t containerDepth;
@ -144,7 +150,7 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
return;
}
MenuT *menuStack[8];
MenuT *menuStack[DSGN_MENU_STACK_DEPTH];
memset(menuStack, 0, sizeof(menuStack));
for (int32_t i = 0; i < menuCount; i++) {
@ -153,12 +159,22 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
menuStack[0] = wmAddMenu(bar, mi->caption);
} else if (isSep && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
// Only show top-level menus with Visible=True on the bar; popup-only
// menus (Visible=False) become standalone popups at runtime, so keep
// them and their children off the preview bar by leaving parent NULL.
menuStack[0] = mi->visible ? wmAddMenu(bar, mi->caption) : NULL;
// Clear deeper slots so a new top-level menu can't inherit a stale
// submenu pointer from the previous menu (otherwise an invisible
// menu's deep children would attach under the prior visible menu).
for (int32_t d = 1; d < DSGN_MENU_STACK_DEPTH; d++) {
menuStack[d] = NULL;
}
} else if (isSep && mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH && menuStack[mi->level - 1]) {
wmAddMenuSeparator(menuStack[mi->level - 1]);
} else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
} else if (isSubParent && mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH && menuStack[mi->level - 1]) {
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
} else if (mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH && menuStack[mi->level - 1]) {
int32_t id = DSGN_MENU_ID_BASE + i;
if (mi->radioCheck) {
@ -173,6 +189,17 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
}
const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name) {
for (int32_t i = 0; i < ctrl->propCount; i++) {
if (strcasecmp(ctrl->props[i].name, name) == 0) {
return ctrl->props[i].value;
}
}
return NULL;
}
// dsgnCreateFormWindow / dsgnCreateContentBox
// Thin wrappers around formrt functions for backward compatibility.
WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
@ -235,11 +262,13 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
DsgnControlT *pc = ds->form->controls[j];
parent = pc->widget;
const char *layout = getPropValue(pc, "Layout");
const char *layout = dsgnControlGetPropValue(pc, "Layout");
if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
// Check if we already created a content box inside
if (!parent->firstChild || !parent->firstChild->userData ||
// Check if we already created a content box inside.
// pc is non-NULL, so a NULL firstChild->userData also
// fails the != pc test -- no separate NULL check needed.
if (!parent->firstChild ||
parent->firstChild->userData != (void *)pc) {
WidgetT *box = dsgnCreateContentBox(parent, layout);
box->userData = (void *)pc;
@ -263,8 +292,8 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
wgtSetName(w, ctrl->name);
// Set Caption/Text
const char *caption = getPropValue(ctrl, "Caption");
const char *text = getPropValue(ctrl, "Text");
const char *caption = dsgnControlGetPropValue(ctrl, "Caption");
const char *text = dsgnControlGetPropValue(ctrl, "Text");
if (caption) { wgtSetText(w, caption); }
if (text) { wgtSetText(w, text); }
@ -289,6 +318,9 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
w->weight = ctrl->weight;
wgtSetVisible(w, ctrl->visible);
wgtSetEnabled(w, ctrl->enabled);
// Apply interface properties (Alignment, etc.) from FRM data
const char *wgtName = wgtFindByBasName(ctrl->typeName);
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
@ -301,7 +333,7 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
continue;
}
const char *val = getPropValue(ctrl, p->name);
const char *val = dsgnControlGetPropValue(ctrl, p->name);
if (val) {
wgtApplyPropFromString(w, p, val);
@ -345,6 +377,30 @@ void dsgnFree(DsgnStateT *ds) {
}
// Returns true if the widget type registers an interface property with the
// given name. The generic built-in Visible/Enabled designer rows yield to
// such a property so a widget can give those names its own meaning (e.g. the
// Timer's "Enabled" maps to its running state, VB-style) without producing a
// duplicate grid row or a conflicting saved line.
bool dsgnIfaceHasProp(const char *typeName, const char *propName) {
const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface) {
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return true;
}
}
}
}
return false;
}
void dsgnInit(DsgnStateT *ds, AppContextT *ctx) {
memset(ds, 0, sizeof(*ds));
ds->selectedIdx = -1;
@ -414,6 +470,15 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
cbs.onCtrlProp = dsgnLoad_onCtrlProp;
if (!frmParse(source, sourceLen, &cbs)) {
// The callbacks may already have allocated controls/menuItems/code
// before a mid-stream failure; mirror dsgnFree's teardown so a bare
// free(form) doesn't leak them.
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
free(form->controls[i]);
}
arrfree(form->controls);
arrfree(form->menuItems);
free(form->code);
free(form);
return false;
}
@ -441,6 +506,8 @@ static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const cha
}
cp->index = -1;
cp->visible = true;
cp->enabled = true;
snprintf(cp->name, DSGN_MAX_NAME, "%s", name);
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
@ -459,7 +526,10 @@ static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const cha
ctx->containerStack[ctx->containerDepth++] = isCtrl;
}
if (isCtrl && ctx->nestDepth < 7) {
// Guard against parentStack's own bound (now BAS_MAX_FRM_NESTING) so it
// stays in step with containerStack; a smaller limit silently skipped the
// push for deep nesting and desynced nestDepth on the matching CtrlEnd.
if (isCtrl && ctx->nestDepth < BAS_MAX_FRM_NESTING - 1) {
snprintf(ctx->parentStack[ctx->nestDepth], DSGN_MAX_NAME, "%s", name);
ctx->nestDepth++;
}
@ -512,6 +582,10 @@ static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *val
cc->index = atoi(val);
} else if (strcasecmp(key, "HelpTopic") == 0) {
snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val);
} else if (strcasecmp(key, "Visible") == 0 && !dsgnIfaceHasProp(cc->typeName, "Visible")) {
cc->visible = (strcasecmp(val, "False") != 0);
} else if (strcasecmp(key, "Enabled") == 0 && !dsgnIfaceHasProp(cc->typeName, "Enabled")) {
cc->enabled = (strcasecmp(val, "False") != 0);
} else if (strcasecmp(key, "TabIndex") == 0) {
// ignored -- DVX has no tab order
} else {
@ -664,6 +738,13 @@ void dsgnNewForm(DsgnStateT *ds, const char *name) {
dsgnFree(ds);
DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
if (!form) {
ds->form = NULL;
ds->selectedIdx = -1;
return;
}
form->controls = NULL;
form->width = DEFAULT_FORM_W;
form->height = DEFAULT_FORM_H;
@ -689,12 +770,73 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
int32_t count = (int32_t)arrlen(ds->form->controls);
// Delete key -- remove the selected control and any children
if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < count) {
const char *delName = ds->form->controls[ds->selectedIdx]->name;
if (key == KEY_DELETE && ds->selectedIdx >= 0 && ds->selectedIdx < count) {
// Build the full set of names to delete (the selected control and
// every descendant, transitively). parentName references only the
// immediate parent, so a single-level child pass would orphan
// grandchildren -- saveControls then silently drops them because their
// parentName matches no surviving container. The names are wrapped in
// a struct so arrput (which assigns via =) is a legal struct copy; a
// bare char[] element type would not compile.
typedef struct {
char name[DSGN_MAX_NAME];
} DelNameT;
// Remove children first (controls whose parentName matches)
DelNameT *delSet = NULL;
DelNameT seed;
snprintf(seed.name, DSGN_MAX_NAME, "%s", ds->form->controls[ds->selectedIdx]->name);
arrput(delSet, seed);
bool grew = true;
while (grew) {
grew = false;
for (int32_t i = 0; i < count; i++) {
const char *pName = ds->form->controls[i]->parentName;
const char *cName = ds->form->controls[i]->name;
if (pName[0] == '\0') {
continue;
}
bool parentInSet = false;
bool selfInSet = false;
for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) {
if (strcasecmp(delSet[s].name, pName) == 0) {
parentInSet = true;
}
if (strcasecmp(delSet[s].name, cName) == 0) {
selfInSet = true;
}
}
if (parentInSet && !selfInSet) {
DelNameT add;
snprintf(add.name, DSGN_MAX_NAME, "%s", cName);
arrput(delSet, add);
grew = true;
}
}
}
// Delete every flagged control, highest index first so earlier indices
// stay valid, adjusting the selection for each removal below it.
for (int32_t i = count - 1; i >= 0; i--) {
if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i]->parentName, delName) == 0) {
bool remove = false;
for (int32_t s = 0; s < (int32_t)arrlen(delSet); s++) {
if (strcasecmp(delSet[s].name, ds->form->controls[i]->name) == 0) {
remove = true;
break;
}
}
if (remove) {
free(ds->form->controls[i]);
arrdel(ds->form->controls, i);
@ -704,8 +846,7 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
}
}
free(ds->form->controls[ds->selectedIdx]);
arrdel(ds->form->controls, ds->selectedIdx);
arrfree(delSet);
ds->selectedIdx = -1;
ds->form->dirty = true;
@ -829,7 +970,14 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
// Non-pointer tool: place a new control
const char *typeName = ds->activeTool;
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
if (!cp) {
return;
}
cp->index = -1;
cp->visible = true;
cp->enabled = true;
dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME);
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
cp->width = DEFAULT_CTRL_W;
@ -851,6 +999,21 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name);
parentWidget = pc->widget;
const char *layout = dsgnControlGetPropValue(pc, "Layout");
if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
// Mirror dsgnCreateWidgets: non-VBox containers nest
// children inside a tagged content box so the live designer
// layout matches the rebuilt/saved/runtime form.
if (!parentWidget->firstChild || !parentWidget->firstChild->userData ||
parentWidget->firstChild->userData != (void *)pc) {
WidgetT *box = dsgnCreateContentBox(parentWidget, layout);
box->userData = (void *)pc;
}
parentWidget = parentWidget->firstChild;
}
break;
}
}
@ -869,36 +1032,25 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
arrput(ds->form->controls, cp);
ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1;
// Set text AFTER arrput so pointers are stable (heap-allocated, so always stable)
// Set text after the control is in the array; control pointers are heap-allocated and stable
DsgnControlT *stable = ds->form->controls[ds->selectedIdx];
if (stable->widget) {
const char *caption = getPropValue(stable, "Caption");
const char *caption = dsgnControlGetPropValue(stable, "Caption");
wgtSetName(stable->widget, stable->name);
wgtSetText(stable->widget, caption ? caption : stable->name);
}
// Leave the same state a normal selection press leaves (selectedIdx is
// already the new control). The immediate mouse-release then matches the
// REORDERING guard and resets to IDLE, instead of re-arming REORDERING and
// swallowing the user's next click.
ds->activeTool[0] = '\0';
ds->mode = DSGN_IDLE;
ds->mode = DSGN_REORDERING;
ds->dragStartY = y;
ds->form->dirty = true;
}
// Draw grid dots and selection handles over the live widgets.
// Called from the form window's onPaint callback.
void dsgnPaint(DsgnStateT *ds) {
if (!ds->form || !ds->ctx) {
return;
}
// Grid dots and selection handles are drawn directly onto
// the window via the display backbuffer. The live widgets
// handle their own rendering.
// Nothing to do here for now -- the onPaint hook in ideMain
// calls dsgnPaintOverlay with the display pointer.
}
// Draw selection handles on the window's painted surface.
// Called after widgets have painted, using direct display drawing.
void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
@ -943,11 +1095,11 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
int32_t hs = DSGN_HANDLE_SIZE;
// All 8 handles: NW, N, NE, E, SE, S, SW, W
int32_t hx[8] = { cx - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw/2 - hs/2, cx - hs/2, cx - hs/2 };
int32_t hy[8] = { cy - hs/2, cy - hs/2, cy - hs/2, cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch/2 - hs/2 };
int32_t hx[DSGN_SEL_HANDLE_COUNT] = { cx - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw/2 - hs/2, cx - hs/2, cx - hs/2 };
int32_t hy[DSGN_SEL_HANDLE_COUNT] = { cy - hs/2, cy - hs/2, cy - hs/2, cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch/2 - hs/2 };
// E (idx 3), S (idx 5), SE (idx 4) are active (black); rest are inactive (gray)
for (int32_t i = 0; i < 8; i++) {
for (int32_t i = 0; i < DSGN_SEL_HANDLE_COUNT; i++) {
bool active = (i == 3 || i == 4 || i == 5);
rectFill(&cd, ops, hx[i], hy[i], hs, hs, active ? black : gray);
}
@ -961,26 +1113,26 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
int32_t pos = 0;
pos += snprintf(buf + pos, bufSize - pos, "VERSION DVX 1.00\n");
pos += snprintf(buf + pos, bufSize - pos, "Begin Form %s\n", ds->form->name);
pos += snprintf(buf + pos, bufSize - pos, " Caption = \"%s\"\n", ds->form->caption);
pos += snprintf(buf + pos, bufSize - pos, " Layout = %s\n", ds->form->layout);
pos += snprintf(buf + pos, bufSize - pos, " AutoSize = %s\n", ds->form->autoSize ? "True" : "False");
pos += snprintf(buf + pos, bufSize - pos, " Resizable = %s\n", ds->form->resizable ? "True" : "False");
pos += snprintf(buf + pos, bufSize - pos, " Centered = %s\n", ds->form->centered ? "True" : "False");
pos = emitClamped(buf, bufSize, pos, "VERSION DVX 1.00\n");
pos = emitClamped(buf, bufSize, pos, "Begin Form %s\n", ds->form->name);
pos = emitClamped(buf, bufSize, pos, " Caption = \"%s\"\n", ds->form->caption);
pos = emitClamped(buf, bufSize, pos, " Layout = %s\n", ds->form->layout);
pos = emitClamped(buf, bufSize, pos, " AutoSize = %s\n", ds->form->autoSize ? "True" : "False");
pos = emitClamped(buf, bufSize, pos, " Resizable = %s\n", ds->form->resizable ? "True" : "False");
pos = emitClamped(buf, bufSize, pos, " Centered = %s\n", ds->form->centered ? "True" : "False");
if (!ds->form->centered) {
pos += snprintf(buf + pos, bufSize - pos, " Left = %d\n", (int)ds->form->left);
pos += snprintf(buf + pos, bufSize - pos, " Top = %d\n", (int)ds->form->top);
pos = emitClamped(buf, bufSize, pos, " Left = %d\n", (int)ds->form->left);
pos = emitClamped(buf, bufSize, pos, " Top = %d\n", (int)ds->form->top);
}
if (!ds->form->autoSize) {
pos += snprintf(buf + pos, bufSize - pos, " Width = %d\n", (int)ds->form->width);
pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height);
pos = emitClamped(buf, bufSize, pos, " Width = %d\n", (int)ds->form->width);
pos = emitClamped(buf, bufSize, pos, " Height = %d\n", (int)ds->form->height);
}
if (ds->form->helpTopic[0]) {
pos += snprintf(buf + pos, bufSize - pos, " HelpTopic = \"%s\"\n", ds->form->helpTopic);
pos = emitClamped(buf, bufSize, pos, " HelpTopic = \"%s\"\n", ds->form->helpTopic);
}
// Output menu items as nested Begin Menu blocks
@ -995,61 +1147,40 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
while (curLevel > mi->level) {
curLevel--;
for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "End\n");
pos = emitPad(buf, bufSize, pos, (curLevel + 1) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "End\n");
}
// Indent: (level + 1) * 4 spaces (one extra for being inside Form block)
for (int32_t p = 0; p < (mi->level + 1) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Begin Menu %s\n", mi->name);
pos = emitPad(buf, bufSize, pos, (mi->level + 1) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "Begin Menu %s\n", mi->name);
// Caption
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Caption = \"%s\"\n", mi->caption);
pos = emitPad(buf, bufSize, pos, (mi->level + 2) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "Caption = \"%s\"\n", mi->caption);
// Optional properties
if (mi->checked) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n");
pos = emitPad(buf, bufSize, pos, (mi->level + 2) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "Checked = True\n");
}
if (mi->radioCheck) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "RadioCheck = True\n");
pos = emitPad(buf, bufSize, pos, (mi->level + 2) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "RadioCheck = True\n");
}
if (!mi->enabled) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n");
pos = emitPad(buf, bufSize, pos, (mi->level + 2) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "Enabled = False\n");
}
// Visible is only meaningful on top-level menus -- False
// marks it as a popup (not on the menu bar). Never
// serialize the default True.
if (mi->level == 0 && !mi->visible) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Visible = False\n");
pos = emitPad(buf, bufSize, pos, (mi->level + 2) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "Visible = False\n");
}
curLevel = mi->level + 1;
@ -1059,18 +1190,15 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
while (curLevel > 0) {
curLevel--;
for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "End\n");
pos = emitPad(buf, bufSize, pos, (curLevel + 1) * DSGN_INDENT_SPACES);
pos = emitClamped(buf, bufSize, pos, "End\n");
}
}
// Output top-level controls (and recurse into containers)
pos = saveControls(ds->form, buf, bufSize, pos, "", 1);
pos += snprintf(buf + pos, bufSize - pos, "End\n");
pos = emitClamped(buf, bufSize, pos, "End\n");
// Append code section if present
if (ds->form->code && ds->form->code[0]) {
@ -1113,14 +1241,51 @@ const char *dsgnSelectedName(const DsgnStateT *ds) {
}
static const char *getPropValue(const DsgnControlT *ctrl, const char *name) {
for (int32_t i = 0; i < ctrl->propCount; i++) {
if (strcasecmp(ctrl->props[i].name, name) == 0) {
return ctrl->props[i].value;
}
static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
va_list args;
int32_t avail;
int32_t written;
if (!buf || pos < 0 || pos >= bufSize) {
return pos;
}
return NULL;
avail = bufSize - pos;
va_start(args, fmt);
written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
va_end(args);
if (written < 0) {
return pos;
}
if (written >= avail) {
// Truncated: vsnprintf wrote avail-1 chars + NUL. Advance to
// the last writable byte before the NUL so we never exceed bufSize-1.
return bufSize - 1;
}
return pos + written;
}
static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count) {
int32_t i;
if (!buf || pos < 0) {
return pos;
}
for (i = 0; i < count; i++) {
if (pos >= bufSize - 1) {
return pos;
}
buf[pos] = ' ';
pos++;
}
return pos;
}
@ -1184,10 +1349,9 @@ static void rebuildWidgets(DsgnStateT *ds) {
return;
}
// Destroy all existing widget children
// (wgtDestroy not available, so just unlink and let the window own them)
parent->firstChild = NULL;
parent->lastChild = NULL;
// Destroy all existing widget children before recreating them, so
// the old subtrees are freed instead of leaked.
wgtDestroyChildren(parent);
// Clear widget pointers
int32_t count = (int32_t)arrlen(ds->form->controls);
@ -1213,50 +1377,58 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
if (parentName[0] != '\0' && strcasecmp(ctrl->parentName, parentName) != 0) { continue; }
// Indent
char pad[32];
int32_t padLen = indent * 4;
if (padLen > 31) { padLen = 31; }
char pad[DSGN_MAX_INDENT_BUF];
int32_t padLen = indent * DSGN_INDENT_SPACES;
if (padLen > DSGN_MAX_INDENT_BUF - 1) { padLen = DSGN_MAX_INDENT_BUF - 1; }
memset(pad, ' ', padLen);
pad[padLen] = '\0';
pos += snprintf(buf + pos, bufSize - pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
pos = emitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
if (ctrl->index >= 0) {
pos += snprintf(buf + pos, bufSize - pos, "%s Index = %d\n", pad, (int)ctrl->index);
pos = emitClamped(buf, bufSize, pos, "%s Index = %d\n", pad, (int)ctrl->index);
}
const char *caption = getPropValue(ctrl, "Caption");
const char *text = getPropValue(ctrl, "Text");
const char *caption = dsgnControlGetPropValue(ctrl, "Caption");
const char *text = dsgnControlGetPropValue(ctrl, "Text");
if (caption) { pos += snprintf(buf + pos, bufSize - pos, "%s Caption = \"%s\"\n", pad, caption); }
if (text) { pos += snprintf(buf + pos, bufSize - pos, "%s Text = \"%s\"\n", pad, text); }
if (caption) { pos = emitClamped(buf, bufSize, pos, "%s Caption = \"%s\"\n", pad, caption); }
if (text) { pos = emitClamped(buf, bufSize, pos, "%s Text = \"%s\"\n", pad, text); }
pos += snprintf(buf + pos, bufSize - pos, "%s Left = %d\n", pad, (int)ctrl->left);
pos += snprintf(buf + pos, bufSize - pos, "%s Top = %d\n", pad, (int)ctrl->top);
pos += snprintf(buf + pos, bufSize - pos, "%s MinWidth = %d\n", pad, (int)ctrl->width);
pos += snprintf(buf + pos, bufSize - pos, "%s MinHeight = %d\n", pad, (int)ctrl->height);
pos = emitClamped(buf, bufSize, pos, "%s Left = %d\n", pad, (int)ctrl->left);
pos = emitClamped(buf, bufSize, pos, "%s Top = %d\n", pad, (int)ctrl->top);
pos = emitClamped(buf, bufSize, pos, "%s MinWidth = %d\n", pad, (int)ctrl->width);
pos = emitClamped(buf, bufSize, pos, "%s MinHeight = %d\n", pad, (int)ctrl->height);
if (ctrl->maxWidth > 0) {
pos += snprintf(buf + pos, bufSize - pos, "%s MaxWidth = %d\n", pad, (int)ctrl->maxWidth);
pos = emitClamped(buf, bufSize, pos, "%s MaxWidth = %d\n", pad, (int)ctrl->maxWidth);
}
if (ctrl->maxHeight > 0) {
pos += snprintf(buf + pos, bufSize - pos, "%s MaxHeight = %d\n", pad, (int)ctrl->maxHeight);
pos = emitClamped(buf, bufSize, pos, "%s MaxHeight = %d\n", pad, (int)ctrl->maxHeight);
}
if (ctrl->weight > 0) {
pos += snprintf(buf + pos, bufSize - pos, "%s Weight = %d\n", pad, (int)ctrl->weight);
pos = emitClamped(buf, bufSize, pos, "%s Weight = %d\n", pad, (int)ctrl->weight);
}
if (ctrl->helpTopic[0]) {
pos += snprintf(buf + pos, bufSize - pos, "%s HelpTopic = \"%s\"\n", pad, ctrl->helpTopic);
pos = emitClamped(buf, bufSize, pos, "%s HelpTopic = \"%s\"\n", pad, ctrl->helpTopic);
}
if (!ctrl->visible && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
pos = emitClamped(buf, bufSize, pos, "%s Visible = False\n", pad);
}
if (!ctrl->enabled && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
pos = emitClamped(buf, bufSize, pos, "%s Enabled = False\n", pad);
}
for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; }
if (strcasecmp(ctrl->props[j].name, "Text") == 0) { continue; }
pos += snprintf(buf + pos, bufSize - pos, "%s %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value);
pos = emitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value);
}
// Save interface properties (Alignment, etc.) read from the live widget
@ -1296,18 +1468,22 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
char valBuf[DSGN_MAX_TEXT];
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, valBuf);
pos = emitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, p->name, valBuf);
}
}
}
}
// Recursively output children of this container
if (dsgnIsContainer(ctrl->typeName)) {
// Recursively output children of this container. Guard against a
// self-parenting container and runaway cycles (A->B->A) via a depth
// cap so the recursion always terminates.
if (dsgnIsContainer(ctrl->typeName) &&
strcasecmp(ctrl->name, parentName) != 0 &&
indent < DSGN_MAX_NEST_DEPTH) {
pos = saveControls(form, buf, bufSize, pos, ctrl->name, indent + 1);
}
pos += snprintf(buf + pos, bufSize - pos, "%sEnd\n", pad);
pos = emitClamped(buf, bufSize, pos, "%sEnd\n", pad);
}
return pos;

View file

@ -44,9 +44,10 @@
// ============================================================
#define DSGN_MAX_NAME 32
#define DSGN_NAME_FMT "%.31s" // keep in sync with DSGN_MAX_NAME
#define DSGN_MENU_STACK_DEPTH 8 // max menu nesting in designer parent/menu stacks
#define DSGN_MAX_TEXT 256
#define DSGN_MAX_PROPS 32
#define DSGN_GRID_SIZE 8
#define DSGN_HANDLE_SIZE 6
#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items
@ -90,6 +91,8 @@ typedef struct {
int32_t maxHeight; // 0 = no cap (stretch to fill)
int32_t weight; // layout weight (0 = fixed size, >0 = share extra space)
char helpTopic[DSGN_MAX_NAME]; // help topic ID for F1
bool visible; // design-time visibility (default true)
bool enabled; // design-time enabled (default true)
DsgnPropT props[DSGN_MAX_PROPS];
int32_t propCount;
WidgetT *widget; // live widget (created at design time for WYSIWYG)
@ -100,22 +103,23 @@ typedef struct {
// ============================================================
typedef struct {
// Fields are ordered by alignment to minimize struct padding.
char name[DSGN_MAX_NAME];
char caption[DSGN_MAX_TEXT];
int32_t width;
int32_t height;
int32_t left; // initial X position (0 = default)
int32_t top; // initial Y position (0 = default)
DsgnControlT **controls; // stb_ds array of heap-allocated pointers
DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus)
WidgetT *contentBox; // VBox parent for live widgets
char *code; // BASIC code section (malloc'd, after End block)
char layout[DSGN_MAX_NAME]; // "VBox" or "HBox" (contentBox type)
char helpTopic[DSGN_MAX_NAME]; // form-level help topic
bool centered; // true = center on screen, false = use left/top
bool autoSize; // true = dvxFitWindow, false = use width/height
bool resizable; // true = user can resize at runtime
char helpTopic[DSGN_MAX_NAME]; // form-level help topic
DsgnControlT **controls; // stb_ds array of heap-allocated pointers
DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus)
bool dirty;
WidgetT *contentBox; // VBox parent for live widgets
char *code; // BASIC code section (malloc'd, after End block)
} DsgnFormT;
// ============================================================
@ -125,8 +129,6 @@ typedef struct {
// Empty string = pointer (select/move) mode.
// Non-empty = basName of the widget type to place.
#define DSGN_TOOL_NONE "" // pointer mode
// ============================================================
// Grab handle IDs
// ============================================================
@ -175,9 +177,6 @@ typedef struct {
// Initialize designer state.
void dsgnInit(DsgnStateT *ds, AppContextT *ctx);
// Set the canvas overlay widget (for grab handles).
void dsgnSetCanvas(DsgnStateT *ds, WidgetT *canvas);
// Create live widgets for all controls in the form.
// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
@ -192,9 +191,6 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize);
// Create a new blank form.
void dsgnNewForm(DsgnStateT *ds, const char *name);
// Repaint the design surface.
void dsgnPaint(DsgnStateT *ds);
// Draw selection handles over the painted window surface.
void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY);
@ -207,6 +203,9 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key);
// Get the name of the selected control (or form name if nothing selected).
const char *dsgnSelectedName(const DsgnStateT *ds);
// Return a control's property value by name, or NULL if it has no such property.
const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name);
// Get the default event name for a control type.
const char *dsgnDefaultEvent(const char *typeName);
@ -216,6 +215,10 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t
// Check if a control type is a container (can hold children).
bool dsgnIsContainer(const char *typeName);
// True if the widget type registers an interface property of this name. The
// generic Visible/Enabled designer rows defer to such a property.
bool dsgnIfaceHasProp(const char *typeName, const char *propName);
// Free designer resources.
void dsgnFree(DsgnStateT *ds);

File diff suppressed because it is too large Load diff

View file

@ -47,14 +47,14 @@
#define MAX_MENU_LEVEL 5
#define ARROW_STR "-> "
#define MED_MSG_BUF 128 // error-message scratch buffer
// ============================================================
// Dialog state
// ============================================================
typedef struct {
bool done;
bool accepted;
// Fields are ordered by alignment to minimize struct padding.
AppContextT *ctx;
DsgnFormT *form;
@ -62,8 +62,6 @@ typedef struct {
DsgnMenuItemT *items; // stb_ds array
int32_t selectedIdx;
bool nameAutoGen; // true = name was auto-generated, update on caption change
// Widgets
WidgetT *captionInput;
WidgetT *nameInput;
@ -72,6 +70,10 @@ typedef struct {
WidgetT *enabledCb;
WidgetT *popupCb; // Top-level popup (hidden from menu bar)
WidgetT *listBox;
bool done;
bool accepted;
bool nameAutoGen; // true = name was auto-generated, update on caption change
} MnuEdStateT;
static MnuEdStateT sMed;
@ -523,6 +525,11 @@ static void onMoveDown(WidgetT *w) {
// Simple approach: extract our subtree, delete it, insert after next subtree
int32_t subSize = subEnd - idx;
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));
if (!tmp) {
return;
}
memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));
// Delete our subtree
@ -560,10 +567,22 @@ static void onMoveUp(WidgetT *w) {
prevIdx--;
}
// No previous sibling at the same level (e.g. selected item is the first
// child of its parent) -- Move Up would insert the subtree before its
// parent and corrupt nesting, so do nothing.
if (sMed.items[prevIdx].level != sMed.items[idx].level) {
return;
}
// Move our subtree before the previous item's subtree
int32_t subEnd = findSubtreeEnd(idx);
int32_t subSize = subEnd - idx;
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));
if (!tmp) {
return;
}
memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));
// Delete our subtree
@ -645,7 +664,7 @@ static void onOk(WidgetT *w) {
for (int32_t j = i + 1; j < count; j++) {
if (strcasecmp(sMed.items[i].name, sMed.items[j].name) == 0) {
char msg[128];
char msg[MED_MSG_BUF];
snprintf(msg, sizeof(msg), "Duplicate menu name: %s", sMed.items[i].name);
dvxErrorBox(sMed.ctx, "Menu Editor", msg);
sMed.selectedIdx = j;

View file

@ -71,8 +71,13 @@
#define PRJ_WIN_W 180
#define PRJ_WIN_H 300
#define PRJ_COPY_BUF_SIZE 4096 // file-copy chunk
#define PRJ_INI_KEY_LEN 16 // "FileNNN" INI key buffer size
#define PRJ_FILE_KEY_FMT "File%d" // INI key for an indexed module/form path
#define PRJ_COUNT_KEY "Count" // INI key for the file count in a section
#define PPD_WIDTH 380
#define PPD_HEIGHT 410
#define PPD_LABEL_W 96
#define PPD_BTN_W 70
#define PPD_BTN_H 24
@ -123,6 +128,10 @@ static void ppdOnBrowseIcon(WidgetT *w);
static void ppdOnCancel(WidgetT *w);
static void ppdOnClose(WindowT *win);
static void ppdOnOk(WidgetT *w);
static void prjDeriveDir(PrjStateT *prj, const char *dbpPath);
static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm);
static const char *prjMakeRelative(const PrjStateT *prj, const char *path);
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm);
static bool validateIcon(const char *fullPath, bool showErrors);
@ -213,15 +222,9 @@ static void ppdOnBrowseHelp(WidgetT *w) {
}
// Convert to project-relative path
const char *relPath = path;
int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);
const char *relPath = prjMakeRelative(sPpd.prj, path);
if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
relPath = path + dirLen + 1;
}
wgtSetText(sPpd.helpFileInput, relPath);
wgtSetText(sPpd.helpFileInput, relPath ? relPath : path);
}
@ -242,13 +245,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
// The icon must be in the project directory so the relative
// path works when the project is reloaded.
const char *relPath = NULL;
int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);
if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
relPath = path + dirLen + 1;
}
const char *relPath = prjMakeRelative(sPpd.prj, path);
if (!relPath) {
int32_t result = dvxMessageBox(sPpd.ctx, "Copy Icon",
@ -296,16 +293,30 @@ static void ppdOnBrowseIcon(WidgetT *w) {
return;
}
char buf[4096];
char buf[PRJ_COPY_BUF_SIZE];
size_t n;
bool copyOk = true;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
fwrite(buf, 1, n, dst);
if (fwrite(buf, 1, n, dst) != n) {
copyOk = false;
break;
}
}
if (ferror(src)) {
copyOk = false;
}
fclose(src);
fclose(dst);
if (!copyOk) {
remove(destPath);
dvxErrorBox(sPpd.ctx, NULL, "Failed to copy icon into the project directory.");
return;
}
relPath = fname;
}
@ -347,6 +358,70 @@ static void ppdOnOk(WidgetT *w) {
}
// Set prj->projectDir to the directory containing dbpPath, or "." when
// dbpPath has no directory component.
static void prjDeriveDir(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
}
// Load File0, File1, ... entries from an INI section, registering each as a
// project file. The loop ends at the first missing key (no fixed cap).
static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, const char *section, bool isForm) {
for (int32_t i = 0; ; i++) {
char key[PRJ_INI_KEY_LEN];
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)i);
const char *val = prefsGetString(h, section, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, isForm);
}
}
// Return path relative to prj->projectDir when path lives under it, else NULL
// (callers apply their own fallback).
static const char *prjMakeRelative(const PrjStateT *prj, const char *path) {
int32_t dirLen = (int32_t)strlen(prj->projectDir);
if (strncasecmp(path, prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
return path + dirLen + 1;
}
return NULL;
}
// Write the project files whose isForm flag matches into an INI section as
// File0, File1, ... plus a Count entry.
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
int32_t idx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm == isForm) {
char key[PRJ_INI_KEY_LEN];
snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, (int)idx++);
prefsSetString(h, section, key, prj->files[i].path);
}
}
prefsSetInt(h, section, PRJ_COUNT_KEY, idx);
}
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
PrjFileT entry;
memset(&entry, 0, sizeof(entry));
@ -457,15 +532,7 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Derive project directory
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
prjDeriveDir(prj, dbpPath);
// [Project] section
const char *val;
@ -494,31 +561,9 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
val = prefsGetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, NULL);
if (val) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", val); }
// [Modules] section -- File0, File1, ... (no cap; loop exits on missing key)
for (int32_t i = 0; ; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
val = prefsGetString(h, BAS_INI_SECTION_MODULES, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, false);
}
// [Forms] section -- File0, File1, ... (no cap; loop exits on missing key)
for (int32_t i = 0; ; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
val = prefsGetString(h, BAS_INI_SECTION_FORMS, key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, true);
}
// [Modules] and [Forms] sections -- File0, File1, ... (no cap)
prjLoadFileSection(prj, h, BAS_INI_SECTION_MODULES, false);
prjLoadFileSection(prj, h, BAS_INI_SECTION_FORMS, true);
// [Settings] section
val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL);
@ -560,8 +605,8 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
while (*pos) {
pos = dvxSkipWs(pos);
if (strncasecmp(pos, "Begin Form ", 11) == 0) {
const char *np = dvxSkipWs(pos + 11);
if (strncasecmp(pos, "Begin Form ", sizeof("Begin Form ") - 1) == 0) {
const char *np = dvxSkipWs(pos + sizeof("Begin Form ") - 1);
int32_t n = 0;
while (*np && *np != ' ' && *np != '\t' && *np != '\r' && *np != '\n' && n < PRJ_MAX_NAME - 1) {
prj->files[i].formName[n++] = *np++;
@ -622,7 +667,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
return false;
}
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 410, false);
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, PPD_HEIGHT, false);
if (!win) {
return false;
@ -836,9 +881,9 @@ void prjRebuildTree(PrjStateT *prj) {
return;
}
// Clear existing items by removing all children
sTree->firstChild = NULL;
sTree->lastChild = NULL;
// Destroy existing items so the old tree-item widgets are freed
// instead of leaked on every rebuild.
wgtDestroyChildren(sTree);
// Free old labels
if (sLabels) {
@ -930,31 +975,9 @@ bool prjSave(const PrjStateT *prj) {
if (prj->iconPath[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, prj->iconPath); }
if (prj->helpFile[0]) { prefsSetString(h, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, prj->helpFile); }
// [Modules] section
int32_t modIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)modIdx++);
prefsSetString(h, BAS_INI_SECTION_MODULES, key, prj->files[i].path);
}
}
prefsSetInt(h, BAS_INI_SECTION_MODULES, "Count", modIdx);
// [Forms] section
int32_t frmIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)frmIdx++);
prefsSetString(h, BAS_INI_SECTION_FORMS, key, prj->files[i].path);
}
}
prefsSetInt(h, BAS_INI_SECTION_FORMS, "Count", frmIdx);
// [Modules] and [Forms] sections
prjSaveFileSection(h, prj, BAS_INI_SECTION_MODULES, false);
prjSaveFileSection(h, prj, BAS_INI_SECTION_FORMS, true);
// [Settings] section
prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm);
@ -970,15 +993,7 @@ bool prjSaveAs(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Update project directory
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = platformPathDirEnd(prj->projectDir);
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
prjDeriveDir(prj, dbpPath);
return prjSave(prj);
}

View file

@ -45,11 +45,12 @@
// ============================================================
typedef struct {
char path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
bool isForm; // true = .frm, false = .bas
// Fields are ordered by alignment to minimize struct padding.
char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded)
bool modified; // true = buffer has unsaved changes
char path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
char formName[PRJ_MAX_NAME]; // form object name (from "Begin Form <name>")
bool isForm; // true = .frm, false = .bas
bool modified; // true = buffer has unsaved changes
} PrjFileT;
// ============================================================
@ -67,6 +68,12 @@ typedef struct {
// ============================================================
typedef struct {
// Fields are ordered by alignment to minimize struct padding.
PrjFileT *files; // stb_ds dynamic array
PrjSourceMapT *sourceMap; // stb_ds dynamic array
int32_t fileCount;
int32_t sourceMapCount;
int32_t activeFileIdx; // index of file open in editor (-1 = none)
char name[PRJ_MAX_NAME];
char projectPath[DVX_MAX_PATH]; // full path to .dbp file
char projectDir[DVX_MAX_PATH]; // directory containing .dbp
@ -80,12 +87,7 @@ typedef struct {
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
char helpFile[DVX_MAX_PATH]; // relative path to .hlp file
bool optionExplicit; // require DIM before use
PrjFileT *files; // stb_ds dynamic array
int32_t fileCount;
PrjSourceMapT *sourceMap; // stb_ds dynamic array
int32_t sourceMapCount;
bool dirty;
int32_t activeFileIdx; // index of file open in editor (-1 = none)
} PrjStateT;
// ============================================================

View file

@ -48,6 +48,10 @@
#define PRP_WIN_W 220
#define PRP_WIN_H 400
#define PRP_QUERY_BUF 512 // SQL probe scratch buffer
#define PRP_PROMPT_BUF 128 // input-dialog prompt buffer
#define PRP_TITLE_SUFFIX_PAD 16 // pad over DSGN_MAX_TEXT for " [Design]" suffix
#define DSGN_NO_INDEX (-1) // tree label has no "(idx)" / control not in an array
#define PROP_TYPE_STRING WGT_IFACE_STRING
#define PROP_TYPE_INT WGT_IFACE_INT
@ -62,6 +66,21 @@
// No fixed caps: field and table arrays are heap-allocated and sized to the
// actual count reported by SQL.
// ============================================================
// SQL engine function-pointer types (resolved at runtime via dlsym so the
// designer does not hard-link the optional dvxSql* engine). Declared once
// here; the column-name and table-name probes resolve the subset they use.
// ============================================================
typedef int32_t (*SqlOpenFnT)(const char *);
typedef void (*SqlCloseFnT)(int32_t);
typedef int32_t (*SqlQueryFnT)(int32_t, const char *);
typedef int32_t (*SqlFieldCountFnT)(int32_t);
typedef const char *(*SqlFieldNameFnT)(int32_t, int32_t);
typedef bool (*SqlNextFnT)(int32_t);
typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t);
typedef void (*SqlFreeResultFnT)(int32_t);
// ============================================================
// Module state
// ============================================================
@ -73,8 +92,12 @@ static WidgetT *sPropList = NULL;
static AppContextT *sPrpCtx = NULL;
static bool sUpdating = false;
static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings
#define PRP_CELL_COLUMNS 2 // property grid: name column + value column
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types
#define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)")
static char **sCellData = NULL; // stb_ds array of strdup'd strings
static int32_t sCellRows = 0;
// ============================================================
// Prototypes
@ -84,7 +107,7 @@ static void addPropRow(const char *name, const char *value);
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled);
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName);
static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName);
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name);
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
static void freeCellData(void);
static void freeTreeLabels(void);
static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[DSGN_MAX_NAME]);
@ -94,6 +117,8 @@ static void onPropDblClick(WidgetT *w);
static void onPrpClose(WindowT *win);
static void onTreeChange(WidgetT *w);
static void onTreeItemClick(WidgetT *w);
static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex);
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize);
static void resolveDbPath(const char *dbName, char *out, int32_t outSize);
static bool treeOrderMatches(void);
@ -101,7 +126,6 @@ static bool treeOrderMatches(void);
static void addPropRow(const char *name, const char *value) {
arrput(sCellData, strdup(name));
arrput(sCellData, strdup(value ? value : ""));
sCellRows++;
}
@ -140,17 +164,12 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src
}
char itemName[DSGN_MAX_NAME];
int32_t ni = 0;
int32_t itemIndex = DSGN_NO_INDEX;
while (label[ni] && label[ni] != ' ' && ni < DSGN_MAX_NAME - 1) {
itemName[ni] = label[ni];
ni++;
}
itemName[ni] = '\0';
parseTreeLabel(label, itemName, DSGN_MAX_NAME, &itemIndex);
for (int32_t i = 0; i < srcCount; i++) {
if (strcmp(srcArr[i]->name, itemName) == 0) {
if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) {
snprintf(srcArr[i]->parentName, DSGN_MAX_NAME, "%s", parentName);
arrput(*outArr, srcArr[i]);
@ -194,25 +213,43 @@ static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propN
// Walk tree items recursively to find the one matching a control name.
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name) {
// Parse a tree label "name (Type)" or "name(idx) (Type)" into the bare name
// and, when present, the array index used to disambiguate control-array
// members (which all share one name but have distinct ->index).
static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex) {
int32_t ni = 0;
while (label[ni] && label[ni] != ' ' && label[ni] != '(' && ni < outNameSize - 1) {
outName[ni] = label[ni];
ni++;
}
outName[ni] = '\0';
*outIndex = DSGN_NO_INDEX;
if (label[ni] == '(') {
*outIndex = (int32_t)atoi(&label[ni + 1]);
}
}
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData;
if (label) {
// Labels are "Name (Type)" -- match the name portion
int32_t len = 0;
char itemName[DSGN_MAX_NAME];
int32_t itemIndex = DSGN_NO_INDEX;
while (label[len] && label[len] != ' ') {
len++;
}
parseTreeLabel(label, itemName, DSGN_MAX_NAME, &itemIndex);
if ((int32_t)strlen(name) == len && strncmp(label, name, len) == 0) {
if (itemIndex == index && strcmp(itemName, name) == 0) {
return item;
}
}
// Recurse into children (containers)
WidgetT *found = findTreeItemByName(item, name);
WidgetT *found = findTreeItemByName(item, name, index);
if (found) {
return found;
@ -231,7 +268,6 @@ static void freeCellData(void) {
}
arrsetlen(sCellData, 0);
sCellRows = 0;
}
@ -272,14 +308,8 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
continue;
}
for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, "DatabaseName") == 0) {
dbName = ctrl->props[j].value;
} else if (strcasecmp(ctrl->props[j].name, "RecordSource") == 0) {
recSrc = ctrl->props[j].value;
}
}
dbName = dsgnControlGetPropValue(ctrl, "DatabaseName");
recSrc = dsgnControlGetPropValue(ctrl, "RecordSource");
break;
}
@ -288,13 +318,6 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
}
// Resolve SQL functions via dlsym
typedef int32_t (*SqlOpenFnT)(const char *);
typedef void (*SqlCloseFnT)(int32_t);
typedef int32_t (*SqlQueryFnT)(int32_t, const char *);
typedef int32_t (*SqlFieldCountFnT)(int32_t);
typedef const char *(*SqlFieldNameFnT)(int32_t, int32_t);
typedef void (*SqlFreeResultFnT)(int32_t);
SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen");
SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose");
SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery");
@ -316,7 +339,7 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
}
// Query with LIMIT 0 to get column names without fetching rows
char query[512];
char query[PRP_QUERY_BUF];
if (strncasecmp(recSrc, "SELECT ", 7) == 0) {
snprintf(query, sizeof(query), "%s LIMIT 0", recSrc);
@ -432,13 +455,6 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME
return 0;
}
typedef int32_t (*SqlOpenFnT)(const char *);
typedef void (*SqlCloseFnT)(int32_t);
typedef int32_t (*SqlQueryFnT)(int32_t, const char *);
typedef bool (*SqlNextFnT)(int32_t);
typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t);
typedef void (*SqlFreeResultFnT)(int32_t);
SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen");
SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose");
SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery");
@ -507,22 +523,24 @@ static void onPropDblClick(WidgetT *w) {
int32_t row = wgtListViewGetSelected(w);
if (row < 0 || row >= sCellRows) {
int32_t rowCount = (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS;
if (row < 0 || row >= rowCount) {
return;
}
const char *propName = sCellData[row * 2];
const char *curValue = sCellData[row * 2 + 1];
const char *propName = sCellData[row * PRP_CELL_COLUMNS];
const char *curValue = sCellData[row * PRP_CELL_COLUMNS + 1];
// Layout -- select from discovered layout containers
if (strcasecmp(propName, "Layout") == 0) {
// Discover available layout types from loaded widget interfaces.
// A layout container is isContainer with WGT_CREATE_PARENT (no extra args).
const char *layoutNames[32];
const char *layoutNames[PRP_MAX_LAYOUT_NAMES];
int32_t layoutCount = 0;
int32_t ifaceTotal = wgtIfaceCount();
for (int32_t i = 0; i < ifaceTotal && layoutCount < 32; i++) {
for (int32_t i = 0; i < ifaceTotal && layoutCount < PRP_MAX_LAYOUT_NAMES; i++) {
const WgtIfaceT *iface = wgtIfaceAt(i, NULL);
if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT && iface->basName) {
@ -577,10 +595,13 @@ static void onPropDblClick(WidgetT *w) {
if (sDs->formWin && sDs->formWin->widgetRoot) {
WidgetT *root = sDs->formWin->widgetRoot;
root->firstChild = NULL;
root->lastChild = NULL;
wgtDestroyChildren(root);
WidgetT *contentBox = dsgnCreateContentBox(root, layoutField);
// The root content box must use the FORM's layout, not whatever
// layoutField points at (it points at a selected container's
// Layout prop when one is selected). Per-container layouts are
// applied separately inside dsgnCreateWidgets.
WidgetT *contentBox = dsgnCreateContentBox(root, sDs->form->layout);
int32_t cc = (int32_t)arrlen(sDs->form->controls);
@ -647,13 +668,13 @@ static void onPropDblClick(WidgetT *w) {
} else if (propType == PROP_TYPE_DATASOURCE) {
// Show dropdown of Data control names on the form
int32_t formCtrlCount = (int32_t)arrlen(sDs->form->controls);
const char *dataNames[17];
const char *dataNames[PRP_MAX_DATA_NAMES + 1];
int32_t dataCount = 0;
// First entry is "(none)" to clear binding
dataNames[dataCount++] = "(none)";
for (int32_t i = 0; i < formCtrlCount && dataCount < 16; i++) {
for (int32_t i = 0; i < formCtrlCount && dataCount < PRP_MAX_DATA_NAMES; i++) {
if (strcasecmp(sDs->form->controls[i]->typeName, "Data") == 0) {
dataNames[dataCount++] = sDs->form->controls[i]->name;
}
@ -697,69 +718,23 @@ static void onPropDblClick(WidgetT *w) {
if (strcasecmp(selCtrl->typeName, "Data") == 0) {
dataSrc = selCtrl->name;
} else {
for (int32_t i = 0; i < selCtrl->propCount; i++) {
if (strcasecmp(selCtrl->props[i].name, "DataSource") == 0) {
dataSrc = selCtrl->props[i].value;
break;
}
const char *dataSrcProp = dsgnControlGetPropValue(selCtrl, "DataSource");
if (dataSrcProp) {
dataSrc = dataSrcProp;
}
}
}
char (*fieldNames)[DSGN_MAX_NAME] = NULL;
int32_t fieldCount = getDataFieldNames(sDs, dataSrc, &fieldNames);
bool ok = propDropdownOrInput(sPrpCtx, propName, "Select column:", fieldNames, fieldCount, curValue, newValue, sizeof(newValue));
if (fieldCount <= 0) {
// No columns found -- fall back to text input
char prompt[128];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, sizeof(newValue), "%s", curValue);
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) {
free(fieldNames);
if (!ok) {
return;
}
} else {
const char **fieldPtrs = (const char **)calloc((size_t)fieldCount + 1, sizeof(const char *));
if (!fieldPtrs) {
free(fieldNames);
return;
}
fieldPtrs[0] = "(none)";
for (int32_t i = 0; i < fieldCount; i++) {
fieldPtrs[i + 1] = fieldNames[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < fieldCount; i++) {
if (strcasecmp(fieldNames[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, propName, "Select column:", fieldPtrs, fieldCount + 1, defIdx, &chosenIdx)) {
free(fieldPtrs);
free(fieldNames);
return;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, sizeof(newValue), "%s", fieldNames[chosenIdx - 1]);
}
free(fieldPtrs);
}
free(fieldNames);
} else if (propType == PROP_TYPE_RECORDSRC) {
// Show dropdown of table names from the Data control's database
// Find DatabaseName on this control (which is a Data control)
@ -768,72 +743,25 @@ static void onPropDblClick(WidgetT *w) {
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
const char *dbNameProp = dsgnControlGetPropValue(selCtrl, "DatabaseName");
for (int32_t i = 0; i < selCtrl->propCount; i++) {
if (strcasecmp(selCtrl->props[i].name, "DatabaseName") == 0) {
dbName = selCtrl->props[i].value;
break;
}
if (dbNameProp) {
dbName = dbNameProp;
}
}
char (*tableNames)[DSGN_MAX_NAME] = NULL;
int32_t tableCount = getTableNames(dbName, &tableNames);
bool ok = propDropdownOrInput(sPrpCtx, propName, "Select table:", tableNames, tableCount, curValue, newValue, sizeof(newValue));
if (tableCount <= 0) {
// No tables or can't open DB -- fall back to text input
char prompt[128];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, sizeof(newValue), "%s", curValue);
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) {
free(tableNames);
if (!ok) {
return;
}
} else {
const char **tablePtrs = (const char **)calloc((size_t)tableCount + 1, sizeof(const char *));
if (!tablePtrs) {
free(tableNames);
return;
}
tablePtrs[0] = "(none)";
for (int32_t i = 0; i < tableCount; i++) {
tablePtrs[i + 1] = tableNames[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < tableCount; i++) {
if (strcasecmp(tableNames[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, "RecordSource", "Select table:", tablePtrs, tableCount + 1, defIdx, &chosenIdx)) {
free(tablePtrs);
free(tableNames);
return;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, sizeof(newValue), "%s", tableNames[chosenIdx - 1]);
}
free(tablePtrs);
}
free(tableNames);
} else if (propType == PROP_TYPE_INT) {
// Spinner dialog for integers
char prompt[128];
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
int32_t intVal = atoi(curValue);
@ -844,7 +772,7 @@ static void onPropDblClick(WidgetT *w) {
snprintf(newValue, sizeof(newValue), "%d", (int)intVal);
} else {
// Text input for strings
char prompt[128];
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, sizeof(newValue), "%s", curValue);
@ -867,7 +795,7 @@ static void onPropDblClick(WidgetT *w) {
DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->name, oldName) == 0) {
snprintf(c->name, DSGN_MAX_NAME, "%.31s", newValue);
snprintf(c->name, DSGN_MAX_NAME, DSGN_NAME_FMT, newValue);
if (c->widget) {
wgtSetName(c->widget, c->name);
@ -875,6 +803,18 @@ static void onPropDblClick(WidgetT *w) {
}
}
// Children reference their container by parentName; if the
// renamed control was a container, update them too, or they
// would be orphaned -- dropped from the saved .frm and
// reparented to the form node in the tree.
for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->parentName, oldName) == 0) {
snprintf(c->parentName, DSGN_MAX_NAME, DSGN_NAME_FMT, newValue);
}
}
// If this is a Data control, update DataSource and MasterSource
// references on all other controls that pointed to the old name
if (strcasecmp(ctrl->typeName, "Data") == 0) {
@ -923,27 +863,27 @@ static void onPropDblClick(WidgetT *w) {
if (ctrl->widget) {
ctrl->widget->weight = ctrl->weight;
}
} else if (strcasecmp(propName, "Visible") == 0) {
} else if (strcasecmp(propName, "Visible") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
bool val = frmParseBool(newValue);
ctrl->visible = val;
if (ctrl->widget) {
wgtSetVisible(ctrl->widget, val);
}
if (dsgnIsContainer(ctrl->typeName)) {
bool en = ctrl->widget ? ctrl->widget->enabled : true;
cascadeToChildren(sDs, ctrl->name, val, en);
cascadeToChildren(sDs, ctrl->name, val, ctrl->enabled);
}
} else if (strcasecmp(propName, "Enabled") == 0) {
} else if (strcasecmp(propName, "Enabled") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
bool val = frmParseBool(newValue);
ctrl->enabled = val;
if (ctrl->widget) {
wgtSetEnabled(ctrl->widget, val);
}
if (dsgnIsContainer(ctrl->typeName)) {
bool vis = ctrl->widget ? ctrl->widget->visible : true;
cascadeToChildren(sDs, ctrl->name, vis, val);
cascadeToChildren(sDs, ctrl->name, ctrl->visible, val);
}
} else if (strcasecmp(propName, "HelpTopic") == 0) {
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
@ -988,6 +928,20 @@ static void onPropDblClick(WidgetT *w) {
}
} else {
wgtApplyPropFromString(ctrl->widget, p, newValue);
// Keep any ctrl->props[] copy loaded from the
// .frm in sync. saveControls/prpRefresh prefer
// the props[] value over the live widget when an
// entry exists, so a stale copy would revert the
// edit. Only update an existing entry; iface
// props absent from props[] are persisted by
// reading the live widget.
for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, propName) == 0) {
snprintf(ctrl->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
break;
}
}
}
ifaceHandled = true;
@ -1053,7 +1007,7 @@ static void onPropDblClick(WidgetT *w) {
snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue);
if (sDs->formWin) {
char winTitle[280];
char winTitle[DSGN_MAX_TEXT + PRP_TITLE_SUFFIX_PAD];
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption);
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
}
@ -1147,8 +1101,7 @@ static void onTreeChange(WidgetT *w) {
sDs->form->dirty = true;
if (sDs->form->contentBox) {
sDs->form->contentBox->firstChild = NULL;
sDs->form->contentBox->lastChild = NULL;
wgtDestroyChildren(sDs->form->contentBox);
int32_t newCount = (int32_t)arrlen(sDs->form->controls);
@ -1197,21 +1150,16 @@ static void onTreeItemClick(WidgetT *w) {
return;
}
// Extract name from "Name (Type)"
// Extract name and control-array index from "Name(idx) (Type)"
char clickedName[DSGN_MAX_NAME];
int32_t ni = 0;
int32_t clickedIndex = DSGN_NO_INDEX;
while (label[ni] && label[ni] != ' ' && ni < DSGN_MAX_NAME - 1) {
clickedName[ni] = label[ni];
ni++;
}
clickedName[ni] = '\0';
parseTreeLabel(label, clickedName, DSGN_MAX_NAME, &clickedIndex);
int32_t count = (int32_t)arrlen(sDs->form->controls);
for (int32_t i = 0; i < count; i++) {
if (strcmp(sDs->form->controls[i]->name, clickedName) == 0) {
if (strcmp(sDs->form->controls[i]->name, clickedName) == 0 && sDs->form->controls[i]->index == clickedIndex) {
sDs->selectedIdx = i;
if (sDs->formWin) {
@ -1296,8 +1244,7 @@ void prpRebuildTree(DsgnStateT *ds) {
sUpdating = true;
freeTreeLabels();
sTree->firstChild = NULL;
sTree->lastChild = NULL;
wgtDestroyChildren(sTree);
// Form entry at the top
char *formLabel = strdup(ds->form->name);
@ -1320,7 +1267,7 @@ void prpRebuildTree(DsgnStateT *ds) {
for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = ds->form->controls[i];
char buf[128];
char buf[PRP_PROMPT_BUF];
if (ctrl->index >= 0) {
snprintf(buf, sizeof(buf), "%s(%d) (%s)", ctrl->name, (int)ctrl->index, ctrl->typeName);
@ -1378,7 +1325,7 @@ void prpRefresh(DsgnStateT *ds) {
if (ds->selectedIdx < 0) {
target = formItem;
} else if (ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
target = findTreeItemByName(formItem, ds->form->controls[ds->selectedIdx]->name);
target = findTreeItemByName(formItem, ds->form->controls[ds->selectedIdx]->name, ds->form->controls[ds->selectedIdx]->index);
}
if (target) {
@ -1426,8 +1373,14 @@ void prpRefresh(DsgnStateT *ds) {
snprintf(buf, sizeof(buf), "%d", (int)ctrl->weight);
addPropRow("Weight", buf);
addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False");
addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False");
if (!dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
addPropRow("Visible", ctrl->visible ? "True" : "False");
}
if (!dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
addPropRow("Enabled", ctrl->enabled ? "True" : "False");
}
addPropRow("HelpTopic", ctrl->helpTopic);
for (int32_t i = 0; i < ctrl->propCount; i++) {
@ -1502,7 +1455,62 @@ void prpRefresh(DsgnStateT *ds) {
addPropRow("HelpTopic", ds->form->helpTopic);
}
wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows);
wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS);
}
// propDropdownOrInput -- shared editor for the DataField / RecordSource
// properties. When count <= 0 it falls back to a free-text input box;
// otherwise it offers a "(none)"-prefixed dropdown of names, preselecting
// the entry matching curValue. Writes the chosen value (empty for "(none)")
// into newValue. Returns false if the user cancelled.
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize) {
if (count <= 0) {
// No names available -- fall back to text input
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, newValueSize, "%s", curValue);
return dvxInputBox(ctx, "Edit Property", prompt, curValue, newValue, newValueSize);
}
const char **ptrs = (const char **)calloc((size_t)count + 1, sizeof(const char *));
if (!ptrs) {
return false;
}
ptrs[0] = "(none)";
for (int32_t i = 0; i < count; i++) {
ptrs[i + 1] = names[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < count; i++) {
if (strcasecmp(names[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
bool ok = dvxChoiceDialog(ctx, propName, selectPrompt, ptrs, count + 1, defIdx, &chosenIdx);
free(ptrs);
if (!ok) {
return false;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, newValueSize, "%s", names[chosenIdx - 1]);
}
return true;
}
@ -1524,7 +1532,7 @@ static void resolveDbPath(const char *dbName, char *out, int32_t outSize) {
// Check whether the tree order matches the controls array.
// Returns true if they match (no reorder happened).
// Returns true if they match (no reorder AND no reparent happened).
static bool treeOrderMatches(void) {
WidgetT *formItem = sTree->firstChild;
@ -1532,17 +1540,39 @@ static bool treeOrderMatches(void) {
return true;
}
DsgnControlT **newArr = NULL;
int32_t count = (int32_t)arrlen(sDs->form->controls);
// collectTreeOrder rewrites each control's parentName from the tree
// nesting as a side effect, and newArr holds pointers INTO the
// controls array -- so a post-call 'newArr[i]->parentName vs
// controls[i]->parentName' compares each string to itself and never
// detects a reparent. Snapshot the parentNames first and compare
// against that instead; order is detected via pointer identity
// (tree DFS order vs model order).
char (*oldParents)[DSGN_MAX_NAME] = NULL;
if (count > 0) {
oldParents = (char (*)[DSGN_MAX_NAME])malloc((size_t)count * DSGN_MAX_NAME);
if (!oldParents) {
return true;
}
for (int32_t i = 0; i < count; i++) {
snprintf(oldParents[i], DSGN_MAX_NAME, "%s", sDs->form->controls[i]->parentName);
}
}
DsgnControlT **newArr = NULL;
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
bool match = ((int32_t)arrlen(newArr) == count);
if (match) {
for (int32_t i = 0; i < count; i++) {
if (strcmp(newArr[i]->name, sDs->form->controls[i]->name) != 0 ||
strcmp(newArr[i]->parentName, sDs->form->controls[i]->parentName) != 0) {
if (newArr[i] != sDs->form->controls[i] ||
strcmp(sDs->form->controls[i]->parentName, oldParents[i]) != 0) {
match = false;
break;
}
@ -1550,5 +1580,6 @@ static bool treeOrderMatches(void) {
}
arrfree(newArr);
free(oldParents);
return match;
}

View file

@ -126,16 +126,18 @@ Operators listed from highest precedence (evaluated first) to lowest precedence
---------- -------- -----------
1 (highest) ^ Exponentiation
2 - (unary), + (unary) Negation (unary plus is a no-op)
3 * / \ MOD Multiply, float divide, integer divide, modulus
4 + - Addition, subtraction (+ also concatenates strings)
5 & String concatenation
6 = <> < > <= >= Comparison (returns Boolean)
7 NOT Logical/bitwise NOT
8 AND Logical/bitwise AND
9 XOR Logical/bitwise exclusive-or
10 OR Logical/bitwise OR
11 EQV Logical/bitwise equivalence
12 (lowest) IMP Logical/bitwise implication
3 * / Multiply, float divide
4 \ Integer divide
5 MOD Modulus
6 + - Addition, subtraction (+ also concatenates strings)
7 & String concatenation
8 = <> < > <= >= Comparison (returns Boolean)
9 NOT Logical/bitwise NOT
10 AND Logical/bitwise AND
11 XOR Logical/bitwise exclusive-or
12 OR Logical/bitwise OR
13 EQV Logical/bitwise equivalence
14 (lowest) IMP Logical/bitwise implication
.endtable
.h2 Arithmetic Operators

View file

@ -0,0 +1,56 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basErrors.h -- DVX BASIC runtime error code constants
#ifndef DVXBASIC_BASERRORS_H
#define DVXBASIC_BASERRORS_H
// Runtime error numbers reported via runtimeError() and stored in the VM
// errorNumber field. Values are the classic Microsoft/QuickBASIC ERR codes
// and are the single source of truth for these numbers. Ordered by numeric
// value to match the codebase's ABI-ordered constant style (see opcodes.h).
//
// Note: BAS_ERR_SUBSCRIPT_RANGE (9) is overloaded. In addition to genuine
// subscript-out-of-range failures, the VM also uses code 9 for internal
// "invalid variable/field/dimension index" and "outside form context"
// conditions, preserving the historical numeric value at those sites.
#define BAS_ERR_NEXT_WITHOUT_FOR 1
#define BAS_ERR_OUT_OF_DATA 4
#define BAS_ERR_ILLEGAL_FUNC_CALL 5
#define BAS_ERR_OUT_OF_MEMORY 7
#define BAS_ERR_SUBSCRIPT_RANGE 9
#define BAS_ERR_DIV_BY_ZERO 11
#define BAS_ERR_TYPE_MISMATCH 13
#define BAS_ERR_FOR_NESTING 26
#define BAS_ERR_BAD_OPCODE 51
#define BAS_ERR_BAD_FILE_NUM 52
#define BAS_ERR_FILE_NOT_FOUND 53
#define BAS_ERR_BAD_FILE_MODE 54
#define BAS_ERR_FILE_EXISTS 58
#define BAS_ERR_TOO_MANY_FILES 67
#define BAS_ERR_PATH_FILE_ACCESS 75
#define BAS_ERR_PATH_NOT_FOUND 76
#define BAS_ERR_EXTERNAL_NOT_FOUND 453
#endif

View file

@ -56,6 +56,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// Module file header: 4-byte magic + uint16 version. A matched pair
// -- read by basModuleDeserialize, written by basModuleSerialize --
@ -64,6 +65,23 @@
#define BAS_MOD_MAGIC_LEN 4
#define BAS_MOD_VERSION 1
// Fixed header sizes on the wire. BAS_MOD_HEADER_SIZE is magic(4) +
// version(2) + 6 int32 header fields(24) = 30. BAS_DBG_HEADER_SIZE is
// the leading int32 debugVarCount.
#define BAS_MOD_HEADER_SIZE 30
#define BAS_DBG_HEADER_SIZE 4
// Minimum on-wire byte size of one entry in each variable-length table.
// Used by readCount and the inline remaining-bytes count checks so that
// no bare integers appear at the call sites.
#define BAS_CONST_MIN_ENTRY 2
#define BAS_DATA_MIN_ENTRY 2
#define BAS_PROC_MIN_ENTRY 18
#define BAS_FORMVAR_MIN_ENTRY 14
#define BAS_GLOBALINIT_MIN_ENTRY 5
#define BAS_DEBUGVAR_MIN_ENTRY 14
#define BAS_BUF_INIT_CAP 4096
// ============================================================
// Write helpers
// ============================================================
@ -72,12 +90,14 @@ typedef struct {
uint8_t *buf;
int32_t len;
int32_t cap;
bool err;
} BufT;
typedef struct {
const uint8_t *data;
int32_t len;
int32_t pos;
bool err;
} ReaderT;
@ -96,37 +116,49 @@ static void bufWriteU16(BufT *b, uint16_t v);
static void bufWriteU8(BufT *b, uint8_t v);
static double rF64(ReaderT *r);
static int32_t rI32(ReaderT *r);
static bool rOk(const ReaderT *r, int32_t need);
static bool rOk(ReaderT *r, int32_t need);
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
static char *rStr(ReaderT *r);
static void rStrInto(ReaderT *r, char *dst, int32_t cap);
static uint16_t rU16(ReaderT *r);
static uint8_t rU8(ReaderT *r);
void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen) {
if (!mod || !data || dataLen < 4) {
if (!mod || !data || dataLen < BAS_DBG_HEADER_SIZE) {
return;
}
ReaderT r = { data, dataLen, 0 };
ReaderT r = { data, dataLen, 0, false };
// Debug variables
mod->debugVarCount = rI32(&r);
mod->debugVarCount = readCount(&r, BAS_DEBUGVAR_MIN_ENTRY);
if (mod->debugVarCount > 0) {
mod->debugVars = (BasDebugVarT *)calloc(mod->debugVarCount, sizeof(BasDebugVarT));
if (!mod->debugVars) {
mod->debugVarCount = 0;
return;
}
for (int32_t i = 0; i < mod->debugVarCount; i++) {
BasDebugVarT *v = &mod->debugVars[i];
char *name = rStr(&r);
char *formName = rStr(&r);
snprintf(v->name, BAS_MAX_PROC_NAME, "%s", name);
snprintf(v->formName, BAS_MAX_PROC_NAME, "%s", formName);
free(name);
free(formName);
rStrInto(&r, v->name, BAS_MAX_PROC_NAME);
rStrInto(&r, v->formName, BAS_MAX_PROC_NAME);
v->scope = rU8(&r);
v->dataType = rU8(&r);
v->index = rI32(&r);
v->procIndex = rI32(&r);
}
// A truncated debug resource would otherwise leave phantom entries
// (empty names, index 0 aliasing real global slot 0). Drop to
// no-debug-info rather than display garbage.
if (r.err) {
free(mod->debugVars);
mod->debugVars = NULL;
mod->debugVarCount = 0;
}
}
}
@ -153,17 +185,22 @@ uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen) {
bufWriteI32(&b, v->procIndex);
}
if (b.err) {
free(b.buf);
return NULL;
}
*outLen = b.len;
return b.buf;
}
BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
if (!data || dataLen < 30) {
if (!data || dataLen < BAS_MOD_HEADER_SIZE) {
return NULL;
}
ReaderT r = { data, dataLen, 0 };
ReaderT r = { data, dataLen, 0, false };
// Check magic
if (memcmp(data, BAS_MOD_MAGIC, BAS_MOD_MAGIC_LEN) != 0) {
@ -180,6 +217,10 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
if (!mod) {
return NULL;
}
mod->entryPoint = rI32(&r);
mod->globalCount = rI32(&r);
mod->codeLen = rI32(&r);
@ -187,29 +228,67 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
mod->dataCount = rI32(&r);
mod->procCount = rI32(&r);
// Code
if (!rOk(&r, mod->codeLen)) {
free(mod);
// The constCount/dataCount/procCount counts live in the fixed header,
// ahead of the code blob, so their remaining-bytes upper bound cannot
// be checked until the code has been consumed. Reject negatives now.
if (r.err || mod->codeLen < 0 || mod->constCount < 0 || mod->dataCount < 0 || mod->procCount < 0) {
basModuleFree(mod);
return NULL;
}
// Code
if (!rOk(&r, mod->codeLen)) {
basModuleFree(mod);
return NULL;
}
mod->code = (uint8_t *)malloc(mod->codeLen ? mod->codeLen : 1);
if (!mod->code) {
basModuleFree(mod);
return NULL;
}
mod->code = (uint8_t *)malloc(mod->codeLen);
memcpy(mod->code, r.data + r.pos, mod->codeLen);
r.pos += mod->codeLen;
// String constant pool
if (mod->constCount > (r.len - r.pos) / BAS_CONST_MIN_ENTRY) {
basModuleFree(mod);
return NULL;
}
// calloc(0) is permitted to return NULL on a conforming libc, which would
// spuriously reject a valid module that simply has no string constants.
if (mod->constCount > 0) {
mod->constants = (BasStringT **)calloc(mod->constCount, sizeof(BasStringT *));
if (!mod->constants) {
basModuleFree(mod);
return NULL;
}
for (int32_t i = 0; i < mod->constCount; i++) {
char *s = rStr(&r);
mod->constants[i] = basStringNew(s, (int32_t)strlen(s));
mod->constants[i] = basStringNew(s ? s : "", s ? (int32_t)strlen(s) : 0);
free(s);
}
}
// Data pool
if (mod->dataCount > (r.len - r.pos) / BAS_DATA_MIN_ENTRY) {
basModuleFree(mod);
return NULL;
}
if (mod->dataCount > 0) {
mod->dataPool = (BasValueT *)calloc(mod->dataCount, sizeof(BasValueT));
if (!mod->dataPool) {
basModuleFree(mod);
return NULL;
}
for (int32_t i = 0; i < mod->dataCount; i++) {
uint8_t type = rU8(&r);
mod->dataPool[i].type = type;
@ -226,14 +305,14 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
case BAS_TYPE_SINGLE: {
float f;
if (rOk(&r, 4)) {
memcpy(&f, r.data + r.pos, 4);
r.pos += 4;
if (rOk(&r, sizeof(float))) {
memcpy(&f, r.data + r.pos, sizeof(float));
r.pos += sizeof(float);
} else {
f = 0.0f;
}
mod->dataPool[i].dblVal = (double)f;
mod->dataPool[i].sngVal = f;
break;
}
@ -243,26 +322,46 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
case BAS_TYPE_STRING: {
char *s = rStr(&r);
mod->dataPool[i].strVal = basStringNew(s, (int32_t)strlen(s));
mod->dataPool[i].strVal = basStringNew(s ? s : "", s ? (int32_t)strlen(s) : 0);
free(s);
break;
}
case BAS_TYPE_BOOLEAN:
mod->dataPool[i].intVal = rU8(&r) ? -1 : 0;
mod->dataPool[i].boolVal = rU8(&r) ? -1 : 0;
break;
default:
rI32(&r);
break;
// A non-scalar type byte means the stream is
// desynchronized; reject the whole module. Coerce
// this slot to a release-safe INTEGER first.
mod->dataPool[i].type = BAS_TYPE_INTEGER;
mod->dataPool[i].intVal = 0;
basModuleFree(mod);
return NULL;
}
}
if (r.err) {
basModuleFree(mod);
return NULL;
}
}
// Procedure table
if (mod->procCount > (r.len - r.pos) / BAS_PROC_MIN_ENTRY) {
basModuleFree(mod);
return NULL;
}
if (mod->procCount > 0) {
mod->procs = (BasProcEntryT *)calloc(mod->procCount, sizeof(BasProcEntryT));
if (!mod->procs) {
basModuleFree(mod);
return NULL;
}
for (int32_t i = 0; i < mod->procCount; i++) {
BasProcEntryT *p = &mod->procs[i];
p->codeAddr = rI32(&r);
@ -271,27 +370,25 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
p->returnType = rU8(&r);
p->isFunction = rU8(&r) != 0;
char *name = rStr(&r);
snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name);
free(name);
char *formName = rStr(&r);
snprintf(p->formName, BAS_MAX_PROC_NAME, "%s", formName);
free(formName);
rStrInto(&r, p->name, BAS_MAX_PROC_NAME);
rStrInto(&r, p->formName, BAS_MAX_PROC_NAME);
}
}
// Form variable info
mod->formVarInfoCount = rI32(&r);
mod->formVarInfoCount = readCount(&r, BAS_FORMVAR_MIN_ENTRY);
if (mod->formVarInfoCount > 0) {
mod->formVarInfo = (BasFormVarInfoT *)calloc(mod->formVarInfoCount, sizeof(BasFormVarInfoT));
if (!mod->formVarInfo) {
basModuleFree(mod);
return NULL;
}
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
BasFormVarInfoT *fv = &mod->formVarInfo[i];
char *fvName = rStr(&r);
snprintf(fv->formName, BAS_MAX_PROC_NAME, "%s", fvName);
free(fvName);
rStrInto(&r, fv->formName, BAS_MAX_PROC_NAME);
fv->varCount = rI32(&r);
fv->initCodeAddr = rI32(&r);
fv->initCodeLen = rI32(&r);
@ -300,17 +397,30 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
// Global-slot runtime type init table (survives strip). STRING
// globals need this so first-use string ops work correctly.
mod->globalInitCount = rI32(&r);
mod->globalInitCount = readCount(&r, BAS_GLOBALINIT_MIN_ENTRY);
if (mod->globalInitCount > 0) {
mod->globalInits = (BasGlobalInitT *)calloc(mod->globalInitCount, sizeof(BasGlobalInitT));
if (!mod->globalInits) {
basModuleFree(mod);
return NULL;
}
for (int32_t i = 0; i < mod->globalInitCount; i++) {
mod->globalInits[i].index = rI32(&r);
mod->globalInits[i].dataType = rU8(&r);
}
}
// A latched read error means the stream was truncated or corrupt; the
// reader returned benign defaults for everything past the fault, so the
// module would load with zeroed proc/code addresses. Reject it.
if (r.err) {
basModuleFree(mod);
return NULL;
}
return mod;
}
@ -397,13 +507,13 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
break;
case BAS_TYPE_SINGLE: {
float f = (float)v->dblVal;
bufWrite(&b, &f, 4);
float f = v->sngVal;
bufWrite(&b, &f, sizeof(float));
break;
}
case BAS_TYPE_DOUBLE:
bufWrite(&b, &v->dblVal, 8);
bufWrite(&b, &v->dblVal, sizeof(double));
break;
case BAS_TYPE_STRING:
@ -411,7 +521,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
break;
case BAS_TYPE_BOOLEAN:
bufWriteU8(&b, v->intVal ? 1 : 0);
bufWriteU8(&b, v->boolVal ? 1 : 0);
break;
default:
@ -451,36 +561,81 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
bufWriteU8(&b, mod->globalInits[i].dataType);
}
if (b.err) {
free(b.buf);
return NULL;
}
*outLen = b.len;
return b.buf;
}
static void bufGrow(BufT *b, int32_t need) {
while (b->len + need > b->cap) {
if (b->err) {
return;
}
if (need < 0 || b->len > INT32_MAX - need) {
b->err = true;
return;
}
int32_t want = b->len + need;
// cap can be 0 here, doubling 0 never reaches want, so seed it directly to
// escape the otherwise-infinite loop.
if (b->cap < 1) {
b->cap = want;
}
while (b->cap < want) {
if (b->cap > INT32_MAX / 2) {
b->cap = want;
break;
}
b->cap *= 2;
}
b->buf = (uint8_t *)realloc(b->buf, b->cap);
uint8_t *nb = (uint8_t *)realloc(b->buf, b->cap);
if (!nb) {
b->err = true;
return;
}
b->buf = nb;
}
static void bufInit(BufT *b) {
b->cap = 4096;
b->cap = BAS_BUF_INIT_CAP;
b->buf = (uint8_t *)malloc(b->cap);
b->len = 0;
b->err = false;
if (!b->buf) {
b->cap = 0;
b->err = true;
}
}
static void bufWrite(BufT *b, const void *data, int32_t len) {
bufGrow(b, len);
if (b->err) {
return;
}
memcpy(b->buf + b->len, data, len);
b->len += len;
}
static void bufWriteI32(BufT *b, int32_t v) {
bufWrite(b, &v, 4);
bufWrite(b, &v, sizeof(int32_t));
}
@ -495,41 +650,73 @@ static void bufWriteStr(BufT *b, const char *s) {
static void bufWriteU16(BufT *b, uint16_t v) {
bufWrite(b, &v, 2);
bufWrite(b, &v, sizeof(uint16_t));
}
static void bufWriteU8(BufT *b, uint8_t v) {
bufWrite(b, &v, 1);
bufWrite(b, &v, sizeof(uint8_t));
}
static double rF64(ReaderT *r) {
if (!rOk(r, 8)) {
if (!rOk(r, sizeof(double))) {
return 0.0;
}
double v;
memcpy(&v, r->data + r->pos, 8);
r->pos += 8;
memcpy(&v, r->data + r->pos, sizeof(double));
r->pos += sizeof(double);
return v;
}
static int32_t rI32(ReaderT *r) {
if (!rOk(r, 4)) {
if (!rOk(r, sizeof(int32_t))) {
return 0;
}
int32_t v;
memcpy(&v, r->data + r->pos, 4);
r->pos += 4;
memcpy(&v, r->data + r->pos, sizeof(int32_t));
r->pos += sizeof(int32_t);
return v;
}
static bool rOk(const ReaderT *r, int32_t need) {
return r->pos + need <= r->len;
static bool rOk(ReaderT *r, int32_t need) {
// r->len and r->pos are both non-negative and r->pos is within
// [0, len], so r->len - r->pos cannot overflow; comparing against
// need this way avoids the overflow of r->pos + need.
if (need < 0 || r->err || r->pos < 0 || need > r->len - r->pos) {
r->err = true;
return false;
}
return true;
}
// Read an int32 count and validate it is non-negative and that the
// declared minimum per-entry payload still fits in the remaining bytes.
// Uses division, not multiplication, to avoid overflow.
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) {
int32_t count = rI32(r);
if (r->err || count < 0) {
r->err = true;
return 0;
}
if (minBytesPerEntry > 0) {
int32_t remaining = r->len - r->pos;
if (count > remaining / minBytesPerEntry) {
r->err = true;
return 0;
}
}
return count;
}
@ -539,11 +726,23 @@ static char *rStr(ReaderT *r) {
if (len == 0 || !rOk(r, len)) {
char *s = (char *)malloc(1);
if (!s) {
r->err = true;
return NULL;
}
s[0] = '\0';
return s;
}
char *s = (char *)malloc(len + 1);
if (!s) {
r->err = true;
return NULL;
}
memcpy(s, r->data + r->pos, len);
s[len] = '\0';
r->pos += len;
@ -551,20 +750,30 @@ static char *rStr(ReaderT *r) {
}
// Read a length-prefixed string directly into a fixed-size buffer,
// truncating to cap-1 chars. Wraps rStr so the reader position advances
// by the full on-wire string length regardless of truncation.
static void rStrInto(ReaderT *r, char *dst, int32_t cap) {
char *s = rStr(r);
snprintf(dst, cap, "%s", s ? s : "");
free(s);
}
static uint16_t rU16(ReaderT *r) {
if (!rOk(r, 2)) {
if (!rOk(r, sizeof(uint16_t))) {
return 0;
}
uint16_t v;
memcpy(&v, r->data + r->pos, 2);
r->pos += 2;
memcpy(&v, r->data + r->pos, sizeof(uint16_t));
r->pos += sizeof(uint16_t);
return v;
}
static uint8_t rU8(ReaderT *r) {
if (!rOk(r, 1)) {
if (!rOk(r, sizeof(uint8_t))) {
return 0;
}

View file

@ -54,7 +54,7 @@ void basModuleFree(BasModuleT *mod);
// Debug info serialization (separate resource)
// ============================================================
// Serialize debug info (debugVars + formVarInfo) to a malloc'd buffer.
// Serialize debug info (debugVars only) to a malloc'd buffer.
uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen);
// Deserialize debug info and attach to an existing module.

View file

@ -35,6 +35,8 @@
#include <stdlib.h>
#include <string.h>
#define BAS_STRING_IMMORTAL_REFCOUNT 999999 // sentinel refCount; empty string is never freed (see basStringUnref)
// ============================================================
// String system
// ============================================================
@ -44,7 +46,7 @@
static struct {
BasStringT hdr;
char nul;
} sEmptyStringStorage = { { .refCount = 999999, .len = 0, .cap = 1 }, '\0' };
} sEmptyStringStorage = { { .refCount = BAS_STRING_IMMORTAL_REFCOUNT, .len = 0, .cap = 1 }, '\0' };
BasStringT *basEmptyString = &sEmptyStringStorage.hdr;
@ -54,6 +56,8 @@ int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims);
BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType);
BasArrayT *basArrayRef(BasArrayT *arr);
void basArrayUnref(BasArrayT *arr);
static int32_t basClampToRange(double n, int32_t lo, int32_t hi);
static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive);
BasStringT *basStringAlloc(int32_t cap);
int32_t basStringCompare(const BasStringT *a, const BasStringT *b);
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
@ -151,12 +155,12 @@ BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t
arr->elementType = elementType;
arr->dims = dims;
int32_t total = 1;
int64_t total = 1;
for (int32_t d = 0; d < dims; d++) {
arr->lbound[d] = lbounds[d];
arr->ubound[d] = ubounds[d];
int32_t dimSize = ubounds[d] - lbounds[d] + 1;
int64_t dimSize = (int64_t)ubounds[d] - (int64_t)lbounds[d] + 1;
if (dimSize < 1) {
free(arr);
@ -164,19 +168,35 @@ BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t
}
total *= dimSize;
if (total > INT32_MAX) {
free(arr);
return NULL;
}
}
arr->totalElements = total;
arr->elements = (BasValueT *)calloc(total, sizeof(BasValueT));
arr->totalElements = (int32_t)total;
arr->elements = (BasValueT *)calloc(arr->totalElements, sizeof(BasValueT));
if (!arr->elements) {
free(arr);
return NULL;
}
// Initialize all elements to the default for the element type
for (int32_t i = 0; i < total; i++) {
// Initialize all elements to the default for the element type. calloc
// already zeroed everything and BAS_TYPE_INTEGER == 0, so integer arrays
// (the common case) need no init pass at all. Other types need their type
// tag set; STRING elements additionally need a real (empty) string --
// calloc leaves strVal NULL and the VM's string ops dereference it
// unguarded, so an uninitialized string element would crash.
if (elementType != BAS_TYPE_INTEGER) {
for (int32_t i = 0; i < arr->totalElements; i++) {
arr->elements[i].type = elementType;
if (elementType == BAS_TYPE_STRING) {
arr->elements[i].strVal = basStringRef(basEmptyString);
}
}
}
return arr;
@ -205,6 +225,19 @@ void basArrayUnref(BasArrayT *arr) {
}
static int32_t basClampToRange(double n, int32_t lo, int32_t hi) {
if (n <= (double)lo) {
return lo;
}
if (n >= (double)hi) {
return hi;
}
return (int32_t)(n + (n > 0 ? 0.5 : -0.5));
}
BasStringT *basStringAlloc(int32_t cap) {
if (cap < 1) {
cap = 1;
@ -269,12 +302,24 @@ int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b) {
BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b) {
int32_t newLen = a->len + b->len;
int64_t total = (int64_t)a->len + (int64_t)b->len;
if (total > BAS_STRING_MAX_LEN) {
total = BAS_STRING_MAX_LEN;
}
int32_t newLen = (int32_t)total;
int32_t copyA = a->len < newLen ? a->len : newLen;
int32_t copyB = newLen - copyA;
BasStringT *s = basStringAlloc(newLen + 1);
memcpy(s->data, a->data, a->len);
memcpy(s->data + a->len, b->data, b->len);
if (s->cap >= newLen + 1) {
memcpy(s->data, a->data, copyA);
memcpy(s->data + copyA, b->data, copyB);
s->data[newLen] = '\0';
s->len = newLen;
}
return s;
}
@ -312,7 +357,7 @@ BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len) {
return basStringRef(basEmptyString);
}
if (len < 0 || start + len > s->len) {
if (len < 0 || len > s->len - start) {
len = s->len - start;
}
@ -321,7 +366,8 @@ BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len) {
void basStringSystemInit(void) {
sEmptyStringStorage.nul = '\0';
// Nothing to do -- the empty string singleton is statically initialized,
// including its trailing null. Kept for symmetry with the shutdown call.
}
@ -418,34 +464,24 @@ BasValueT basValBool(bool v) {
int32_t basValCompare(BasValueT a, BasValueT b) {
// String comparison
if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
return basStringCompare(a.strVal ? a.strVal : basEmptyString, b.strVal ? b.strVal : basEmptyString);
}
// Numeric comparison
double na = basValToNumber(a);
double nb = basValToNumber(b);
if (na < nb) {
return -1;
}
if (na > nb) {
return 1;
}
return 0;
return basValCompareImpl(a, b, false);
}
int32_t basValCompareCI(BasValueT a, BasValueT b) {
// String comparison (case-insensitive)
return basValCompareImpl(a, b, true);
}
static int32_t basValCompareImpl(BasValueT a, BasValueT b, bool caseInsensitive) {
// String comparison
if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
return basStringCompareCI(a.strVal ? a.strVal : basEmptyString, b.strVal ? b.strVal : basEmptyString);
BasStringT *sa = a.strVal ? a.strVal : basEmptyString;
BasStringT *sb = b.strVal ? b.strVal : basEmptyString;
return caseInsensitive ? basStringCompareCI(sa, sb) : basStringCompare(sa, sb);
}
// Numeric comparison (same as basValCompare)
// Numeric comparison
double na = basValToNumber(a);
double nb = basValToNumber(b);
@ -643,15 +679,18 @@ BasValueT basValToDouble(BasValueT v) {
BasValueT basValToInteger(BasValueT v) {
double n = basValToNumber(v);
// Banker's rounding (round half to even)
int32_t rounded = (int32_t)(n + (n > 0 ? 0.5 : -0.5));
// Round half away from zero, clamping to the INTEGER range first
// so the float-to-int cast is always in range.
int32_t rounded = basClampToRange(n, INT16_MIN, INT16_MAX);
return basValInteger((int16_t)rounded);
}
BasValueT basValToLong(BasValueT v) {
double n = basValToNumber(v);
int32_t rounded = (int32_t)(n + (n > 0 ? 0.5 : -0.5));
// Round half away from zero, clamping to the LONG range first
// so the float-to-int cast is always in range.
int32_t rounded = basClampToRange(n, INT32_MIN, INT32_MAX);
return basValLong(rounded);
}
@ -693,6 +732,13 @@ BasValueT basValToSingle(BasValueT v) {
BasValueT basValToString(BasValueT v) {
if (v.type == BAS_TYPE_STRING) {
// Normalize a NULL strVal to the empty string so callers can
// dereference the result unconditionally (basValCopy would
// otherwise propagate the NULL).
if (!v.strVal) {
return basValString(NULL);
}
return basValCopy(v);
}

View file

@ -46,6 +46,9 @@ typedef struct {
char data[]; // flexible array member, null-terminated
} BasStringT;
// Maximum string length in bytes (one byte reserved for the null terminator).
#define BAS_STRING_MAX_LEN (INT32_MAX - 1)
// Allocate a new string from a C string. refCount starts at 1.
BasStringT *basStringNew(const char *text, int32_t len);

File diff suppressed because it is too large Load diff

View file

@ -52,8 +52,10 @@
#define BAS_VM_MAX_GLOBALS 512 // global variable slots
#define BAS_VM_MAX_LOCALS 64 // locals per stack frame
#define BAS_VM_MAX_FOR_DEPTH 32 // nested FOR loops
#define BAS_VM_MAX_FILES 16 // open file channels
#define BAS_VM_MAX_FILES 16 // file channel array size; index 0 unused, so 15 usable channels (1..15)
#define BAS_VM_RANDOM_RECORD_SIZE 128 // fixed RANDOM-mode record length (bytes)
#define BAS_VM_DEFAULT_STEP_SLICE 10000 // bytecode steps per yield
#define BAS_VM_MAX_CALL_ARGS 16 // max args to OP_CALL_METHOD / OP_CALL_EXTERN
#define BAS_RGB_COMPONENT_MAX 255 // RGB(r,g,b): max per-channel value
#define BAS_PRINT_SPC_MAX 255 // PRINT SPC()/TAB(): max column count
@ -386,6 +388,7 @@ typedef struct {
bool running;
bool ended; // END statement executed -- program should terminate
bool yielded;
bool badOperand; // operand read ran past code[] -- raise BAS_VM_BAD_OPCODE next step
int32_t stepLimit; // max steps per basVmRun (0 = unlimited)
int32_t stepCount; // steps executed in last basVmRun
int32_t currentLine; // source line from last OP_LINE (debugger)
@ -403,11 +406,20 @@ typedef struct {
// Evaluation stack
BasValueT stack[BAS_VM_STACK_SIZE];
int32_t sp; // stack pointer (index of next free slot)
int32_t stmtSp; // eval-stack depth snapshot at last OP_LINE (statement boundary)
// Call stack
BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE];
int32_t callDepth;
// Out-argument copy-back for basVmCallSubWithArgsOut. OP_RET/OP_RET_VAL
// copy the returning frame's locals here before releasing them, so the
// caller reads valid values instead of already-released refs. Not
// reentrant: only one out-arg call may be in flight at a time.
BasValueT *outArgs; // caller-provided out-arg buffer (NULL = none)
int32_t outArgCount; // number of out-args to copy back
int32_t outArgFrame; // callDepth of the frame to read back (-1 = none)
// FOR loop stack
BasForStateT forStack[BAS_VM_MAX_FOR_DEPTH];
int32_t forDepth;
@ -463,6 +475,12 @@ typedef struct {
char appPath[DVX_MAX_PATH]; // App.Path -- project/app directory
char appConfig[DVX_MAX_PATH]; // App.Config -- writable config directory
char appData[DVX_MAX_PATH]; // App.Data -- writable data directory
// Dir$ iterator state -- per-VM so a second embedded VM (e.g. the IDE
// watch-expression evaluator) cannot clobber an in-progress enumeration.
void *dirHandle; // DIR * (opaque here; cast in vm.c)
char dirPattern[DVX_MAX_PATH]; // filename glob for Dir$
char dirPath[DVX_MAX_PATH]; // directory being enumerated
} BasVmT;
// ============================================================

View file

@ -55,19 +55,45 @@
#include <string.h>
#include <strings.h>
// Initial capacity of the source-concatenation buffer, and the hard
// ceiling its capacity may reach. The ceiling keeps the int32 capacity
// doubling in concatGrow from overflowing.
#define CONCAT_INITIAL_CAP 8192
#define CONCAT_MAX_CAP 0x40000000 // 1 GiB cap; keeps int32 doubling safe
// Function prototypes (alphabetical)
static void concatGrow(char **buf, int32_t *cap, int32_t need);
static bool concatGrow(char **buf, int32_t *cap, int32_t need);
static const char *extractFormCode(const char *frmText);
int main(int argc, char **argv);
static void usage(void);
static void concatGrow(char **buf, int32_t *cap, int32_t need) {
while (*cap < need) {
*cap *= 2;
static bool concatGrow(char **buf, int32_t *cap, int32_t need) {
if (need < 0 || need > CONCAT_MAX_CAP) {
return false;
}
*buf = (char *)realloc(*buf, *cap);
int32_t newCap = *cap;
while (newCap < need) {
if (newCap > CONCAT_MAX_CAP / 2) {
// Doubling would overflow int32; jump straight to need.
newCap = need;
break;
}
newCap *= 2;
}
char *grown = (char *)realloc(*buf, (size_t)newCap);
if (grown == NULL) {
return false;
}
*buf = grown;
*cap = newCap;
return true;
}
@ -240,18 +266,16 @@ int main(int argc, char **argv) {
if (fileCount == 0) {
fprintf(stderr, "Error: project has no source files.\n");
prefsClose(prefs);
return 1;
goto failFiles;
}
// Concatenate sources (modules first, then form code)
int32_t concatCap = 8192;
int32_t concatCap = CONCAT_INITIAL_CAP;
char *concatBuf = (char *)malloc(concatCap);
if (!concatBuf) {
fprintf(stderr, "Error: out of memory.\n");
prefsClose(prefs);
return 1;
goto failFiles;
}
int32_t pos = 0;
@ -267,9 +291,7 @@ int main(int argc, char **argv) {
if (!srcBuf) {
fprintf(stderr, "Error: cannot read %s\n", files[i].path);
free(concatBuf);
prefsClose(prefs);
return 1;
goto concatFail;
}
const char *code = srcBuf;
@ -277,28 +299,7 @@ int main(int argc, char **argv) {
if (files[i].isForm) {
// Extract form name from "Begin Form <name>"
char formName[BAS_MAX_SYMBOL_NAME] = "";
const char *bp = srcBuf;
while (*bp) {
bp = dvxSkipWs(bp);
if (strncasecmp(bp, "Begin Form ", 11) == 0) {
bp = dvxSkipWs(bp + 11);
int32_t ni = 0;
while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < BAS_MAX_SYMBOL_NAME - 1) {
formName[ni++] = *bp++;
}
formName[ni] = '\0';
break;
}
while (*bp && *bp != '\n') { bp++; }
if (*bp == '\n') { bp++; }
}
basExtractFormName(srcBuf, formName, BAS_MAX_SYMBOL_NAME);
code = extractFormCode(srcBuf);
@ -307,7 +308,12 @@ int main(int argc, char **argv) {
}
int32_t codeLen = (int32_t)strlen(code);
concatGrow(&concatBuf, &concatCap, pos + codeLen + 128);
if (!concatGrow(&concatBuf, &concatCap, pos + codeLen + 128)) {
fprintf(stderr, "Error: out of memory.\n");
free(srcBuf);
goto concatFail;
}
// Inject BEGINFORM directive before form code
if (formName[0]) {
@ -327,7 +333,12 @@ int main(int argc, char **argv) {
}
} else {
int32_t codeLen = (int32_t)strlen(code);
concatGrow(&concatBuf, &concatCap, pos + codeLen + 2);
if (!concatGrow(&concatBuf, &concatCap, pos + codeLen + 2)) {
fprintf(stderr, "Error: out of memory.\n");
free(srcBuf);
goto concatFail;
}
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
@ -350,9 +361,7 @@ int main(int argc, char **argv) {
if (!parser) {
fprintf(stderr, "Error: out of memory.\n");
free(concatBuf);
prefsClose(prefs);
return 1;
goto concatFail;
}
basParserInit(parser, concatBuf, pos);
@ -362,9 +371,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
basParserFree(parser);
free(parser);
free(concatBuf);
prefsClose(prefs);
return 1;
goto concatFail;
}
free(concatBuf);
@ -375,8 +382,7 @@ int main(int argc, char **argv) {
if (!mod) {
fprintf(stderr, "Error: failed to build module.\n");
prefsClose(prefs);
return 1;
goto failFiles;
}
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
@ -461,8 +467,7 @@ int main(int argc, char **argv) {
if (!modData) {
fprintf(stderr, "Error: failed to serialize module.\n");
basModuleFree(mod);
prefsClose(prefs);
return 1;
goto failForms;
}
// Serialize debug info
@ -485,8 +490,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "Error: cannot open %s to read embedded stub.\n", argv[0]);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
goto failForms;
}
uint32_t stubLen = 0;
@ -497,8 +501,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "Error: STUB resource not found in %s.\n", argv[0]);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
goto failForms;
}
// Write stub to output file
@ -509,14 +512,20 @@ int main(int argc, char **argv) {
free(stubData);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
goto failForms;
}
fwrite(stubData, 1, stubLen, out);
fclose(out);
size_t stubWritten = fwrite(stubData, 1, stubLen, out);
int stubClose = fclose(out);
free(stubData);
if (stubWritten != stubLen || stubClose != 0) {
fprintf(stderr, "Error: failed writing stub to %s\n", outputPath);
free(modData);
free(dbgData);
goto failForms;
}
// Pick the right form bytes per build mode: release builds use the
// obfuscated variant when available, debug builds always use the raw
// stripped text.
@ -561,11 +570,29 @@ int main(int argc, char **argv) {
spec.formData = emitFormData;
spec.formLens = emitFormLens;
basBuildEmitResources(outputPath, &spec);
int32_t emitRc = basBuildEmitResources(outputPath, &spec);
free(modData);
free(dbgData);
if (emitRc != 0) {
fprintf(stderr, "Error: failed writing resources to %s\n", outputPath);
for (int32_t i = 0; i < frmCount; i++) {
free(frmData[i]);
free(obfFrms[i].data);
}
arrfree(files);
arrfree(frmData);
arrfree(frmLens);
arrfree(obfFrms);
arrfree(emitFormData);
arrfree(emitFormLens);
prefsClose(prefs);
return 1;
}
// Copy help file to output directory (the HELPFILE resource itself was
// written by basBuildEmitResources).
if (helpFile[0]) {
@ -618,6 +645,51 @@ int main(int argc, char **argv) {
prefsClose(prefs);
printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen);
// Report the true on-disk size of the finished app, which includes the
// stub, bytecode, debug info, icon, metadata, and all FORMn resources
// appended by basBuildEmitResources -- not just stub + module bytes.
int32_t outBytes = 0;
FILE *szf = fopen(outputPath, "rb");
if (szf) {
fseek(szf, 0, SEEK_END);
long endPos = ftell(szf);
if (endPos > 0) {
outBytes = (int32_t)endPos;
}
fclose(szf);
}
printf("Created %s (%d bytes)\n", outputPath, (int)outBytes);
return 0;
// ----- Error cleanup paths -----
// failForms: frm arrays already exist; free their elements and the
// arrays, then fall through to free files.
// failFiles: only the files array exists.
// Each early error frees its own raw malloc'd buffers (modData,
// dbgData, stubData, concatBuf, parser) before jumping here.
failForms:
for (int32_t i = 0; i < frmCount; i++) {
free(frmData[i]);
free(obfFrms[i].data);
}
arrfree(frmData);
arrfree(frmLens);
arrfree(obfFrms);
// fall through
failFiles:
arrfree(files);
prefsClose(prefs);
return 1;
// The concat buffer is owned by paths that run before the frm arrays
// exist; free it here and route to the files-only cleanup. The caller
// has already printed an appropriate error message.
concatFail:
free(concatBuf);
goto failFiles;
}

View file

@ -69,6 +69,7 @@ static AppContextT *sAc = NULL;
// Function prototypes (alphabetical; main/appMain last)
void appShutdown(void);
static bool stubDoEvents(void *ctx);
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static void stubPrint(void *ctx, const char *text, bool newline);
@ -101,6 +102,16 @@ static void stubPrint(void *ctx, const char *text, bool newline) {
(void)text;
(void)newline;
}
// Resolved by the shell as _appShutdown and called on BOTH graceful reap and
// force-kill, before this app's DXE is unmapped. On a normal exit appMain
// already ran basFormRtDestroy; on a force-kill it never returns, so this is
// the only path that drops the serial/secLink idle pollers from the shell
// registry -- otherwise they would keep polling this app's freed terminals.
void appShutdown(void) {
basFormRtSerialShutdown();
}
int32_t appMain(DxeAppContextT *ctx) {
sAc = ctx->shellCtx;
@ -191,41 +202,38 @@ int32_t appMain(DxeAppContextT *ctx) {
BasFormRtT *rt = basFormRtCreate(sAc, vm, mod);
// Register .frm source text for lazy loading
for (int32_t i = 0; i < 256; i++) {
for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) {
char resName[16];
snprintf(resName, sizeof(resName), "FORM%ld", (long)i);
snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)i);
uint32_t frmSize = 0;
char *frmText = (char *)dvxResRead(res, resName, &frmSize);
if (!frmText) {
break;
// Skip, don't stop: a numbering gap (from an empty form on
// the writer side) must not drop every later form. The loop
// is bounded by BAS_MAX_FORM_RESOURCES.
continue;
}
// Extract form name from "Begin Form <name>" line
char frmName[BAS_MAX_CTRL_NAME] = "";
const char *p = frmText;
// dvxResRead returns exactly frmSize bytes with no terminator; the
// form text is not guaranteed NUL-terminated. Append one so the
// unbounded scan in basExtractFormName can't read past the buffer.
char *frmTextZ = (char *)realloc(frmText, frmSize + 1);
while (*p) {
p = dvxSkipWs(p);
if (strncasecmp(p, "Begin Form ", 11) == 0) {
p = dvxSkipWs(p + 11);
int32_t ni = 0;
while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && ni < BAS_MAX_CTRL_NAME - 1) {
frmName[ni++] = *p++;
if (!frmTextZ) {
free(frmText);
continue;
}
frmName[ni] = '\0';
break;
}
frmText = frmTextZ;
frmText[frmSize] = '\0';
while (*p && *p != '\n') { p++; }
if (*p == '\n') { p++; }
}
// Extract form name from "Begin Form <name>" line. Use the full
// form-name length so a 32-63 char form name is not truncated here
// (which would break name-keyed form-scope variable binding).
char frmName[BAS_MAX_FORM_NAME] = "";
basExtractFormName(frmText, frmName, BAS_MAX_FORM_NAME);
if (frmName[0]) {
basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize);

View file

@ -103,13 +103,19 @@ static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int
basVmSetPrintCallback(vm, captureCallback, &ob);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
BasVmResultE result = basVmRun(vm);
// Signal a real runtime error distinctly so testCompact's -1 branch flags
// it as a failure; otherwise two runs hitting the same VM error produce
// identical truncated output and falsely compare equal (PASS).
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
fprintf(stderr, "vm error %d: %s\n", result, basVmGetError(vm));
basVmDestroy(vm);
basModuleFree(mod);
return -1;
}
basVmDestroy(vm);

View file

@ -31,10 +31,23 @@
#include <stdio.h>
#include <string.h>
// Count of failed checks. main() returns non-zero when this is > 0 so
// `make tests` actually gates on these assertions instead of the harness
// always exiting 0 while printing FAIL.
static int32_t gFailCount = 0;
// Function prototypes
static void primeMainFrame(BasVmT *vm, const BasModuleT *mod);
static void runProgram(const char *name, const char *source);
// Set up callStack[0] as the implicit main frame for module-level code.
static void primeMainFrame(BasVmT *vm, const BasModuleT *mod) {
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
}
static void runProgram(const char *name, const char *source) {
printf("=== %s ===\n", name);
@ -45,6 +58,7 @@ static void runProgram(const char *name, const char *source) {
if (!basParse(&parser)) {
printf("COMPILE ERROR: %s\n\n", parser.error);
gFailCount++;
basParserFree(&parser);
return;
}
@ -54,6 +68,7 @@ static void runProgram(const char *name, const char *source) {
if (!mod) {
printf("MODULE BUILD FAILED\n\n");
gFailCount++;
return;
}
@ -61,13 +76,13 @@ static void runProgram(const char *name, const char *source) {
basVmLoadModule(vm, mod);
// Module-level code uses callStack[0] as implicit main frame
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
printf("[VM error %d: %s]\n", result, basVmGetError(vm));
gFailCount++;
}
basVmDestroy(vm);
@ -142,14 +157,14 @@ int main(void) {
// Test 7: SUB and FUNCTION
runProgram("SUB and FUNCTION",
"DECLARE SUB Greet(name AS STRING)\n"
"DECLARE SUB Greet(who AS STRING)\n"
"DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n"
"\n"
"CALL Greet(\"World\")\n"
"PRINT Square(7)\n"
"\n"
"SUB Greet(name AS STRING)\n"
" PRINT \"Hello, \" & name & \"!\"\n"
"SUB Greet(who AS STRING)\n"
" PRINT \"Hello, \" & who & \"!\"\n"
"END SUB\n"
"\n"
"FUNCTION Square(x AS INTEGER) AS INTEGER\n"
@ -863,6 +878,7 @@ int main(void) {
printf("Correctly caught: %s\n", parser.error);
} else {
printf("ERROR: should have failed\n");
gFailCount++;
}
basParserFree(&parser);
printf("\n");
@ -887,14 +903,16 @@ int main(void) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
BasModuleT *mod = basParserBuildModule(&parser);
if (mod) {
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
basVmRun(vm);
basVmDestroy(vm);
basModuleFree(mod);
}
}
basParserFree(&parser);
printf("\n");
@ -1010,6 +1028,8 @@ int main(void) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
BasModuleT *mod = basParserBuildModule(&parser);
if (mod) {
bool found = false;
for (int32_t i = 0; i < mod->codeLen; i++) {
@ -1020,7 +1040,12 @@ int main(void) {
}
printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n");
if (!found) { gFailCount++; }
basModuleFree(mod);
} else {
printf("MODULE BUILD FAILED\n");
gFailCount++;
}
}
basParserFree(&parser);
@ -1069,6 +1094,7 @@ int main(void) {
printf("Found Command1_Click at addr %d\n", p->codeAddr);
} else {
printf("FAIL: Command1_Click not found!\n");
gFailCount++;
}
p = basModuleFindProc(mod, "FORM1_LOAD");
@ -1077,13 +1103,13 @@ int main(void) {
printf("Found Form1_Load at addr %d\n", p->codeAddr);
} else {
printf("FAIL: Form1_Load not found!\n");
gFailCount++;
}
// Test basVmCallSub
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
p = basModuleFindProc(mod, "Command1_Click");
@ -1091,6 +1117,7 @@ int main(void) {
printf("Calling Command1_Click via basVmCallSub: ");
bool callOk = basVmCallSub(vm, p->codeAddr);
printf("result=%s\n", callOk ? "ok" : "FAIL");
if (!callOk) { gFailCount++; }
}
basVmDestroy(vm);
@ -1805,15 +1832,6 @@ int main(void) {
);
// Expected: 3 / 42 / 3 / 3 / [ 42]
// ============================================================
// Coverage: Me keyword (compilation only -- no form context)
// ============================================================
runProgram("Me keyword compiles",
"' Me compiles to OP_ME_REF\n"
"PRINT \"ok\"\n"
);
// ============================================================
// Coverage: Me.Show / Me.Hide as statements
// ============================================================
@ -1885,11 +1903,11 @@ int main(void) {
if (!mod) {
printf("MODULE BUILD FAILED\n\n");
gFailCount++;
} else {
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
// Test: find and call Form1_Load (no args)
const BasProcEntryT *loadProc = basModuleFindProc(mod, "Form1_Load");
@ -1898,6 +1916,7 @@ int main(void) {
basVmCallSub(vm, loadProc->codeAddr);
} else {
printf("Form1_Load not found!\n");
gFailCount++;
}
// Test: find and call Command1_Click (no args)
@ -1907,6 +1926,7 @@ int main(void) {
basVmCallSub(vm, clickProc->codeAddr);
} else {
printf("Command1_Click not found!\n");
gFailCount++;
}
// Test: find and call Text1_KeyPress with parameter
@ -1982,8 +2002,7 @@ int main(void) {
if (mod) {
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
// Call NoParams with no args
const BasProcEntryT *p = basModuleFindProc(mod, "NoParams");
@ -2080,8 +2099,7 @@ int main(void) {
if (mod) {
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
// Fire both events
const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load");
@ -2166,15 +2184,15 @@ int main(void) {
"END TYPE\n"
"\n"
"TYPE PersonT\n"
" name AS STRING\n"
" fullName AS STRING\n"
" addr AS AddressT\n"
"END TYPE\n"
"\n"
"DIM p AS PersonT\n"
"p.name = \"Alice\"\n"
"p.fullName = \"Alice\"\n"
"p.addr.city = \"NYC\"\n"
"p.addr.zip = 10001\n"
"PRINT p.name\n"
"PRINT p.fullName\n"
"PRINT p.addr.city\n"
"PRINT p.addr.zip\n"
);
@ -2231,10 +2249,10 @@ int main(void) {
runProgram("Multiple CONST",
"CONST PI = 3.14159\n"
"CONST E = 2.71828\n"
"CONST NAME = \"DVX\"\n"
"CONST APPNAME = \"DVX\"\n"
"PRINT INT(PI * 100)\n"
"PRINT INT(E * 100)\n"
"PRINT NAME\n"
"PRINT APPNAME\n"
);
// ============================================================
@ -2490,22 +2508,13 @@ int main(void) {
printf("Correctly caught: %s\n", parser.error);
} else {
printf("ERROR: should have failed\n");
gFailCount++;
}
basParserFree(&parser);
printf("\n");
}
// ============================================================
// Coverage: END statement terminates (distinct from HALT)
// ============================================================
runProgram("END statement",
"PRINT \"before\"\n"
"END\n"
"PRINT \"after\"\n"
);
// ============================================================
// Coverage: Nested UDT field store
// ============================================================
@ -2517,13 +2526,13 @@ int main(void) {
"\n"
"TYPE OuterT\n"
" child AS InnerT\n"
" name AS STRING\n"
" label AS STRING\n"
"END TYPE\n"
"\n"
"DIM o AS OuterT\n"
"o.name = \"test\"\n"
"o.label = \"test\"\n"
"o.child.val = 42\n"
"PRINT o.name\n"
"PRINT o.label\n"
"PRINT o.child.val\n"
);
@ -2918,12 +2927,13 @@ int main(void) {
printf("OK (expected error: %s)\n", parser.error);
} else {
printf("FAIL: should have rejected nested BEGINFORM\n");
gFailCount++;
}
basParserFree(&parser);
printf("\n");
}
printf("All tests complete.\n");
return 0;
printf("All tests complete. Failures: %d\n", (int)gFailCount);
return gFailCount > 0 ? 1 : 0;
}

View file

@ -60,7 +60,7 @@ int main(void) {
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
// Step limit

View file

@ -119,6 +119,13 @@ static void reportFailDiff(const char *name, const char *expected, const char *g
}
// Set up callStack[0] as the implicit main frame for module-level code.
static void primeMainFrame(BasVmT *vm, const BasModuleT *mod) {
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
}
// Compile + run. On success captures PRINT output into *out. Returns
// 0 on success, negative on compile error, positive for runtime errors.
// Runtime error text goes into outErr if provided.
@ -155,8 +162,7 @@ static int32_t runAndCapture(const char *source, char *out, int32_t outSize, cha
captureReset(&cap);
basVmSetPrintCallback(vm, capturePrint, &cap);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
BasVmResultE result = basVmRun(vm);
@ -269,11 +275,23 @@ static int32_t runSubAndCapture(const char *source, const char *subName,
captureReset(&cap);
basVmSetPrintCallback(vm, capturePrint, &cap);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
// Execute module-level code first (to initialize globals)
basVmRun(vm);
// Execute module-level code first (to initialize globals). A
// failure here (e.g. an error during global init) must be reported,
// not swallowed -- otherwise the SUB runs against half-built state
// and the test passes or fails for the wrong reason.
BasVmResultE initResult = basVmRun(vm);
if (initResult != BAS_VM_HALTED && initResult != BAS_VM_OK) {
if (outErr) {
snprintf(outErr, outErrSize, "module init error: %s", basVmGetError(vm));
}
basVmDestroy(vm);
basModuleFree(mod);
return -1;
}
// Find the target SUB by name
int32_t subAddr = -1;
@ -419,7 +437,7 @@ typedef struct MiniCtrlT {
typedef struct MiniFormT {
char name[MINI_MAX_NAME];
MiniCtrlT *ctrls; // stb_ds array
MiniCtrlT **ctrls; // stb_ds array of heap-alloc'd controls
BasValueT *formVars;
int32_t formVarCount;
} MiniFormT;
@ -449,13 +467,14 @@ static MiniFormT *miniCreateForm(MiniRtT *rt, const char *name, int32_t formVarC
static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName) {
MiniCtrlT c;
memset(&c, 0, sizeof(c));
snprintf(c.name, MINI_MAX_NAME, "%s", name);
snprintf(c.typeName, MINI_MAX_NAME, "%s", typeName ? typeName : "");
c.form = f;
// Store stable heap pointers: arrput may realloc f->ctrls, so returning
// an interior address would dangle once a second control is added.
MiniCtrlT *c = (MiniCtrlT *)calloc(1, sizeof(MiniCtrlT));
snprintf(c->name, MINI_MAX_NAME, "%s", name);
snprintf(c->typeName, MINI_MAX_NAME, "%s", typeName ? typeName : "");
c->form = f;
arrput(f->ctrls, c);
return &f->ctrls[arrlen(f->ctrls) - 1];
return c;
}
@ -650,11 +669,19 @@ static void testDispatchEq(const char *name, const TestDispatchT *d, const char
captureReset(&cap);
basVmSetPrintCallback(vm, capturePrint, &cap);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
primeMainFrame(vm, mod);
// Run module-level init (populates globals, runs BEGINFORM init block).
basVmRun(vm);
BasVmResultE initResult = basVmRun(vm);
if (initResult != BAS_VM_HALTED && initResult != BAS_VM_OK) {
char detail[256];
snprintf(detail, sizeof(detail), "module init error: %s", basVmGetError(vm));
reportFail(name, detail);
basVmDestroy(vm);
basModuleFree(mod);
return;
}
// Build the mini runtime + form + control.
MiniRtT rt;
@ -690,6 +717,9 @@ static void testDispatchEq(const char *name, const TestDispatchT *d, const char
}
free(f->formVars);
}
for (int32_t k = 0; k < (int32_t)arrlen(f->ctrls); k++) {
free(f->ctrls[k]);
}
arrfree(f->ctrls);
free(f);
}
@ -752,6 +782,14 @@ int main(void) {
TEST_EQ("if-block",
"DIM x AS INTEGER\nx = 5\nIF x > 3 THEN\n PRINT \"a\"\n PRINT \"b\"\nEND IF\n",
"a\nb\n");
// QuickBASIC single-line IF: ALL colon-separated statements after THEN
// belong to the conditional branch (a false condition runs none of them).
TEST_EQ("if-then-colon-false",
"DIM x AS INTEGER\nx = 0\nIF 0 THEN x = 1 : x = 2\nPRINT x\n",
"0 \n");
TEST_EQ("if-then-colon-true",
"DIM x AS INTEGER\nx = 0\nIF 1 THEN x = 1 : x = 2\nPRINT x\n",
"2 \n");
TEST_EQ("select-case",
"DIM n AS INTEGER\nn = 2\n"
"SELECT CASE n\n"
@ -2498,9 +2536,11 @@ int main(void) {
testDispatchEq("dispatch-on-error-reusable", &d, "caught\ncaught\n");
}
// Handler that recurses into the same SUB via a method-ish call.
// The re-entrancy guard rejects same-event re-entry. Outer run
// produces one PRINT; inner attempt is swallowed.
// Single fire of a handler that increments and prints a global.
// NOTE: this does NOT exercise the same-event re-entrancy guard in
// miniFireCtrlEvent -- the mini-runtime gives BASIC no way to re-fire
// the same control's event from within its own handler, so genuine
// re-entrant coverage would require a synthetic harness trigger.
{
TestDispatchT d = {
.source =
@ -2515,7 +2555,7 @@ int main(void) {
.ctrlType = "CommandButton",
.fires = DISPATCH_FIRE("Click"),
};
testDispatchEq("dispatch-guard-single-fire", &d, "1 \n");
testDispatchEq("dispatch-single-fire-counter", &d, "1 \n");
}
// SetEvent override twice on the same event name: second wins.
@ -2614,6 +2654,12 @@ int main(void) {
TEST_EQ("prec-mul-add", "PRINT 2 + 3 * 4\n", "14 \n");
TEST_EQ("prec-eq-chain", "PRINT 1 + 2 = 3\n", "True \n");
TEST_EQ("prec-string-and-num", "PRINT \"n=\" + STR$(1 + 2)\n", "n= 3\n");
// True QuickBASIC precedence: * / bind tighter than \ which binds tighter
// than MOD, so the multiplicative operators sit on three distinct levels.
TEST_EQ("prec-mod-below-mul", "PRINT 8 MOD 3 * 2\n", "2 \n"); // 8 MOD (3*2)
TEST_EQ("prec-idiv-below-mul", "PRINT 10 \\ 2 * 3\n", "1 \n"); // 10 \ (2*3)
// Unary sign on the exponent operand (QB: 2 ^ -2 == 0.25).
TEST_EQ("pow-neg-exponent", "PRINT 2 ^ -2 = 0.25\n", "True \n");
// ============================================================
// More string operations

View file

@ -63,8 +63,7 @@ static void emit8(uint8_t b) {
static void emitU16(uint16_t v) {
memcpy(&sCode[sCodeLen], &v, 2);
sCodeLen += 2;
emit16((int16_t)v);
}

View file

@ -115,6 +115,13 @@
static AppContextT *sAc = NULL;
static DxeAppContextT *sDxeCtx = NULL;
// wmDestroyWindow frees neither win->accelTable nor win->contextMenu, and
// widget teardown doesn't free widget->contextMenu, so track these here and
// release them in appShutdown to avoid leaking on every launch/close.
static AccelTableT *sAccel = NULL;
static MenuT *sWinCtxMenu = NULL;
static MenuT *sLbCtxMenu = NULL;
static const FileFilterT sFileFilters[] = {
{"All Files (*.*)"},
{"Text Files (*.txt)"},
@ -142,6 +149,7 @@ static void onPaintColor(WindowT *win, RectT *dirtyArea);
static void onPaintPattern(WindowT *win, RectT *dirtyArea);
static void onPaintText(WindowT *win, RectT *dirtyArea);
static void onToolbarClick(WidgetT *w);
static void setStatusByName(WidgetT *from, const char *name, const char *text);
static void setupControlsWindow(void);
static void setupMainWindow(void);
static void setupTerminalWindow(void);
@ -162,6 +170,27 @@ AppDescriptorT appDescriptor = {
};
// Cleanup hook the shell calls before dlclose. The accelerator table and the
// two context menus aren't freed by window/widget teardown, so release them
// here to avoid leaking into the shell heap on every launch/close.
void appShutdown(void) {
if (sAccel) {
dvxFreeAccelTable(sAccel);
sAccel = NULL;
}
if (sWinCtxMenu) {
wmFreeMenu(sWinCtxMenu);
sWinCtxMenu = NULL;
}
if (sLbCtxMenu) {
wmFreeMenu(sLbCtxMenu);
sLbCtxMenu = NULL;
}
}
static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
(void)drag;
wgtCanvasDrawLine(w, cx, cy, cx, cy);
@ -282,17 +311,7 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
// the named status label. This pattern avoids static coupling between the
// button and the status label -- they're connected only by the name string.
static void onOkClick(WidgetT *w) {
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
WidgetT *status = wgtFind(root, "status");
if (status) {
wgtSetText(status, "Button clicked!");
}
setStatusByName(w, "status", "Button clicked!");
}
@ -454,16 +473,23 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
static void onToolbarClick(WidgetT *w) {
WidgetT *root = w;
setStatusByName(w, "advStatus", wgtGetText(w));
}
// Walks to the window root from any widget and sets the text of the named
// status widget, if present. Shared by the demo's click handlers.
static void setStatusByName(WidgetT *from, const char *name, const char *text) {
WidgetT *root = from;
while (root->parent) {
root = root->parent;
}
WidgetT *status = wgtFind(root, "advStatus");
WidgetT *status = wgtFind(root, name);
if (status) {
wgtSetText(status, wgtGetText(w));
wgtSetText(status, text);
}
}
@ -619,7 +645,7 @@ static void setupControlsWindow(void) {
WidgetT *tb = wgtToolbar(page5tb);
char iconPath[272];
char iconPath[DVX_MAX_PATH];
snprintf(iconPath, sizeof(iconPath), "%s/new.bmp", sDxeCtx->appDir);
WidgetT *btnNew = wgtImageButtonFromFile(tb, iconPath);
@ -656,7 +682,7 @@ static void setupControlsWindow(void) {
// --- Tab 6: Media (Image from file) ---
WidgetT *page6m = wgtTabPage(tabs, "&Media");
char samplePath[272];
char samplePath[DVX_MAX_PATH];
snprintf(samplePath, sizeof(samplePath), "%s/sample.bmp", sDxeCtx->appDir);
wgtLabel(page6m, "ImageFromFile (sample.bmp):");
WidgetT *img = wgtImageFromFile(page6m, samplePath);
@ -902,23 +928,23 @@ static void setupMainWindow(void) {
// Accelerator table: keyboard shortcuts that work even without the
// menu being open. The WM dispatches these via the window's onMenu
// callback, making them indistinguishable from menu clicks.
AccelTableT *accel = dvxCreateAccelTable();
dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW);
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_FILE_SAVE);
dvxAddAccel(accel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT);
dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT);
win1->accelTable = accel;
sAccel = dvxCreateAccelTable();
dvxAddAccel(sAccel, 'N', ACCEL_CTRL, CMD_FILE_NEW);
dvxAddAccel(sAccel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
dvxAddAccel(sAccel, 'S', ACCEL_CTRL, CMD_FILE_SAVE);
dvxAddAccel(sAccel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT);
dvxAddAccel(sAccel, KEY_F1, 0, CMD_HELP_ABOUT);
win1->accelTable = sAccel;
// Right-click context menu, attached at the window level. If a widget
// has its own contextMenu it takes priority; otherwise this one fires.
MenuT *winCtx = wmCreateMenu();
wmAddMenuItem(winCtx, "Cu&t", CMD_CTX_CUT);
wmAddMenuItem(winCtx, "&Copy", CMD_CTX_COPY);
wmAddMenuItem(winCtx, "&Paste", CMD_CTX_PASTE);
wmAddMenuSeparator(winCtx);
wmAddMenuItem(winCtx, "&Properties...", CMD_CTX_PROPS);
win1->contextMenu = winCtx;
sWinCtxMenu = wmCreateMenu();
wmAddMenuItem(sWinCtxMenu, "Cu&t", CMD_CTX_CUT);
wmAddMenuItem(sWinCtxMenu, "&Copy", CMD_CTX_COPY);
wmAddMenuItem(sWinCtxMenu, "&Paste", CMD_CTX_PASTE);
wmAddMenuSeparator(sWinCtxMenu);
wmAddMenuItem(sWinCtxMenu, "&Properties...", CMD_CTX_PROPS);
win1->contextMenu = sWinCtxMenu;
// For raw-paint windows (no widget tree), we must manually update
// the content rect and allocate the content buffer after adding
@ -1102,15 +1128,15 @@ static void setupWidgetDemo(void) {
lb->weight = WGT_WEIGHT_FILL;
// Context menu on the list box
MenuT *lbCtx = wmCreateMenu();
wmAddMenuItem(lbCtx, "Cu&t", CMD_CTX_CUT);
wmAddMenuItem(lbCtx, "&Copy", CMD_CTX_COPY);
wmAddMenuItem(lbCtx, "&Paste", CMD_CTX_PASTE);
wmAddMenuSeparator(lbCtx);
wmAddMenuItem(lbCtx, "&Delete", CMD_CTX_DELETE);
wmAddMenuSeparator(lbCtx);
wmAddMenuItem(lbCtx, "Select &All", CMD_CTX_SELALL);
lb->contextMenu = lbCtx;
sLbCtxMenu = wmCreateMenu();
wmAddMenuItem(sLbCtxMenu, "Cu&t", CMD_CTX_CUT);
wmAddMenuItem(sLbCtxMenu, "&Copy", CMD_CTX_COPY);
wmAddMenuItem(sLbCtxMenu, "&Paste", CMD_CTX_PASTE);
wmAddMenuSeparator(sLbCtxMenu);
wmAddMenuItem(sLbCtxMenu, "&Delete", CMD_CTX_DELETE);
wmAddMenuSeparator(sLbCtxMenu);
wmAddMenuItem(sLbCtxMenu, "Select &All", CMD_CTX_SELALL);
lb->contextMenu = sLbCtxMenu;
wgtLabel(listRow, "&Multi:");
WidgetT *mlb = wgtListBox(listRow);
wgtListBoxSetMultiSelect(mlb, true);

View file

@ -79,6 +79,10 @@
#define HELP_NOTE_PAD 6
#define HELP_NOTE_PREFIX_W 4
#define HELP_CODE_PAD 4
#define HELP_RULE_H 5 // horizontal-rule widget min height
#define HELP_RULE_LINE_Y 2 // rule line offset within the widget
#define HELP_TITLE_MAX 300 // window-title buffer length
#define HELP_CASE_FOLD ('a' - 'A') // ASCII upper->lower fold delta
// MAX_HISTORY -- fixed back-button history depth. 64 entries is plenty for a
// help viewer; the oldest is dropped when exceeded (see the memmove below).
@ -140,7 +144,6 @@ typedef struct {
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
static WidgetT *sTocTree = NULL;
@ -215,6 +218,7 @@ static void historyPush(int32_t topicIdx);
static const char *hlpString(uint32_t offset);
static bool isFirstChild(WidgetT *w);
static int32_t maxLineWidth(const BitmapFontT *font, const char *text);
static bool nextSiblingHasOwnBorder(WidgetT *w);
static void navigateBack(void);
static void navigateForward(void);
static void navigateToTopic(int32_t topicIdx);
@ -229,6 +233,7 @@ static bool openHelpFile(const char *path);
static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg);
static void populateToc(void);
static void registerWidgetClasses(void);
static void showTopic(int32_t topicIdx);
static void updateNavButtons(void);
static void updateWrapWidth(void);
static char *wrapText(const char *text, int32_t colWidth);
@ -415,7 +420,7 @@ static void closeHelpFile(void) {
static int32_t countLines(const char *text) {
int32_t count = 1;
for (const char *p = text; *p; p++) {
for (const char *p = text; p && *p; p++) {
if (*p == '\n') {
count++;
}
@ -436,10 +441,13 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
if (w) {
HelpTextDataT *td = (HelpTextDataT *)dvxCalloc(1, sizeof(HelpTextDataT));
if (td) {
td->text = dvxStrdup(payload);
td->wrapWidth = -1;
w->data = td;
}
}
break;
}
@ -451,26 +459,40 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
if (w) {
HelpHeadingDataT *hd = (HelpHeadingDataT *)dvxCalloc(1, sizeof(HelpHeadingDataT));
if (hd) {
hd->text = dvxStrdup(payload);
hd->level = hdr->type - HLP_REC_HEADING1 + 1;
w->data = hd;
}
}
break;
}
case HLP_REC_LINK: {
// Payload: target topic ID \0 display text \0
size_t targetLen = strlen(payload);
// Reject a payload with no separator/display text within bounds so
// displayText doesn't point past the allocation.
if (targetLen + 1 >= hdr->length) {
break;
}
const char *targetTopicId = payload;
const char *displayText = payload + strlen(payload) + 1;
const char *displayText = payload + targetLen + 1;
WidgetT *w = widgetAlloc(sContentBox, sHelpLinkTypeId);
if (w) {
HelpLinkDataT *ld = (HelpLinkDataT *)dvxCalloc(1, sizeof(HelpLinkDataT));
if (ld) {
ld->displayText = dvxStrdup(displayText);
ld->targetTopicId = dvxStrdup(targetTopicId);
w->data = ld;
}
}
break;
}
@ -514,6 +536,10 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
if (imgWgt && hasAlpha) {
wgtImageSetTransparent(imgWgt, true, keyColor);
} else if (!imgWgt) {
// wgtImage takes ownership only on success;
// free the decoded pixels if it failed.
dvxFreeImage(pixels);
}
}
}
@ -533,6 +559,8 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
if (w) {
HelpListItemDataT *ld = (HelpListItemDataT *)dvxCalloc(1, sizeof(HelpListItemDataT));
if (ld) {
// Prepend bullet + space to the text
int32_t pLen = (int32_t)strlen(payload);
ld->text = (char *)dvxMalloc(pLen + 3);
@ -546,6 +574,7 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
ld->wrapWidth = -1;
w->data = ld;
}
}
break;
}
@ -560,37 +589,31 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
if (w) {
HelpNoteDataT *nd = (HelpNoteDataT *)dvxCalloc(1, sizeof(HelpNoteDataT));
if (nd) {
nd->text = dvxStrdup(payload);
nd->wrapWidth = -1;
nd->noteType = hdr->flags;
w->data = nd;
}
}
break;
}
// Tables render as preformatted text, identical to code blocks.
case HLP_REC_TABLE:
case HLP_REC_CODE: {
WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId);
if (w) {
HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT));
if (cd) {
cd->text = dvxStrdup(payload);
cd->lineCount = countLines(cd->text);
w->data = cd;
}
break;
}
case HLP_REC_TABLE: {
// Render tables as preformatted text
WidgetT *w = widgetAlloc(sContentBox, sHelpCodeTypeId);
if (w) {
HelpCodeDataT *cd = (HelpCodeDataT *)dvxCalloc(1, sizeof(HelpCodeDataT));
cd->text = dvxStrdup(payload);
cd->lineCount = countLines(cd->text);
w->data = cd;
}
break;
@ -704,9 +727,7 @@ static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinH = top + textH + HELP_HEADING1_PAD * 2;
} else if (hd->level == 2) {
int32_t top = first ? 0 : HELP_HEADING2_TOP;
bool nextHasBorder = (w->nextSibling &&
(w->nextSibling->type == sHelpCodeTypeId ||
w->nextSibling->type == sHelpRuleTypeId));
bool nextHasBorder = nextSiblingHasOwnBorder(w);
int32_t extra = nextHasBorder ? 0 : 2;
w->calcMinH = top + textH + HELP_HEADING2_PAD + extra;
} else {
@ -748,9 +769,7 @@ static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
// top border, so we don't end up with two nearly-adjacent lines.
// Tables and code blocks both use sHelpCodeTypeId; .hr uses
// sHelpRuleTypeId.
bool nextHasBorder = (w->nextSibling &&
(w->nextSibling->type == sHelpCodeTypeId ||
w->nextSibling->type == sHelpRuleTypeId));
bool nextHasBorder = nextSiblingHasOwnBorder(w);
if (!nextHasBorder) {
int32_t lineY = textY + font->charHeight + 1;
@ -905,13 +924,13 @@ static void helpNotePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
static void helpRuleCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = 0;
w->calcMinH = 5;
w->calcMinH = HELP_RULE_H;
}
static void helpRulePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
int32_t lineY = w->y + 2;
int32_t lineY = w->y + HELP_RULE_LINE_Y;
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow);
drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY + 1, w->w - HELP_CONTENT_PAD * 2, colors->windowHighlight);
}
@ -982,6 +1001,10 @@ static int32_t maxLineWidth(const BitmapFontT *font, const char *text) {
int32_t maxW = 0;
int32_t lineLen = 0;
if (!text) {
return 0;
}
for (const char *p = text; ; p++) {
if (*p == '\n' || *p == '\0') {
int32_t w = lineLen * font->charWidth;
@ -1010,10 +1033,7 @@ static void navigateBack(void) {
}
sHistoryPos--;
int32_t topicIdx = sHistory[sHistoryPos];
sCurrentTopic = topicIdx;
buildContentWidgets();
updateNavButtons();
showTopic(sHistory[sHistoryPos]);
}
@ -1023,10 +1043,7 @@ static void navigateForward(void) {
}
sHistoryPos++;
int32_t topicIdx = sHistory[sHistoryPos];
sCurrentTopic = topicIdx;
buildContentWidgets();
updateNavButtons();
showTopic(sHistory[sHistoryPos]);
}
@ -1040,28 +1057,16 @@ static void navigateToTopic(int32_t topicIdx) {
}
historyPush(topicIdx);
sCurrentTopic = topicIdx;
buildContentWidgets();
updateNavButtons();
showTopic(topicIdx);
}
// Update window title
const char *title = hlpString(sTopicDir[topicIdx].titleStr);
char winTitle[300];
snprintf(winTitle, sizeof(winTitle), "%s - DVX Help", title);
dvxSetTitle(sAc, sWin, winTitle);
// Sync TOC tree selection
if (sTocTree) {
WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx);
if (item) {
// Suppress onChange to avoid re-entering navigateToTopic
void (*savedOnChange)(WidgetT *) = sTocTree->onChange;
sTocTree->onChange = NULL;
wgtTreeViewSetSelected(sTocTree, item);
sTocTree->onChange = savedOnChange;
}
}
// True if w's next sibling is a code or rule widget, which draws its own top
// border -- the heading then omits its bottom spacing to avoid a double rule.
static bool nextSiblingHasOwnBorder(WidgetT *w) {
return w->nextSibling &&
(w->nextSibling->type == sHelpCodeTypeId ||
w->nextSibling->type == sHelpRuleTypeId);
}
@ -1187,11 +1192,11 @@ static void onSearch(WidgetT *w) {
char sc = *s;
if (kc >= 'A' && kc <= 'Z') {
kc += 32;
kc += HELP_CASE_FOLD;
}
if (sc >= 'A' && sc <= 'Z') {
sc += 32;
sc += HELP_CASE_FOLD;
}
if (kc != sc) {
@ -1301,6 +1306,12 @@ static bool openHelpFile(const char *path) {
return false;
}
// Force-terminate the table so a truncated/corrupt file can't make any
// hlpString() consumer walk past the allocation.
if (sHeader.stringTableSize > 0) {
sStringTable[sHeader.stringTableSize - 1] = '\0';
}
// Read TOC entries
if (sHeader.tocCount > 0) {
sTocEntries = (HlpTocEntryT *)dvxMalloc(sHeader.tocCount * sizeof(HlpTocEntryT));
@ -1340,6 +1351,10 @@ static bool openHelpFile(const char *path) {
static void paintLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t lineCount, uint32_t fg) {
if (!text) {
return;
}
const char *line = text;
for (int32_t i = 0; i < lineCount; i++) {
@ -1410,6 +1425,33 @@ static void registerWidgetClasses(void) {
}
// Display a topic and sync the chrome (title + TOC selection). Shared by all
// three navigation paths so Back/Forward don't leave a stale title/selection.
// Does NOT push history -- callers manage sHistory.
static void showTopic(int32_t topicIdx) {
sCurrentTopic = topicIdx;
buildContentWidgets();
updateNavButtons();
const char *title = hlpString(sTopicDir[topicIdx].titleStr);
char winTitle[HELP_TITLE_MAX];
snprintf(winTitle, sizeof(winTitle), "%s - DVX Help", title);
dvxSetTitle(sAc, sWin, winTitle);
if (sTocTree) {
WidgetT *item = findTreeItemByTopic(sTocTree, topicIdx);
if (item) {
// Suppress onChange to avoid re-entering navigation
void (*savedOnChange)(WidgetT *) = sTocTree->onChange;
sTocTree->onChange = NULL;
wgtTreeViewSetSelected(sTocTree, item);
sTocTree->onChange = savedOnChange;
}
}
}
static void updateNavButtons(void) {
if (sBtnBack) {
wgtSetEnabled(sBtnBack, sHistoryPos > 0);
@ -1502,7 +1544,6 @@ static char *wrapText(const char *text, int32_t colWidth) {
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
// Register private widget classes

View file

@ -80,7 +80,7 @@
// ---------------------------------------------------------------------------
// File header (64 bytes, stored at EOF)
// File header (60 bytes, stored at EOF)
// ---------------------------------------------------------------------------
typedef struct {

View file

@ -63,6 +63,7 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/stat.h>
#include "dvxMem.h"
#include "stb_ds_wrap.h"
@ -82,6 +83,7 @@
#define PM_GRID_SPACING 8
#define PM_SYSINFO_WIN_W 400
#define PM_SYSINFO_WIN_H 360
#define PM_APP_EXT ".app" // application module extension
// Menu command IDs
#define CMD_RUN 100
@ -435,6 +437,17 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
// The apps/ path is relative to the working directory, which the shell sets
// to the root of the DVX install before loading any apps.
static void scanAppsDir(void) {
// scanAppsDirRecurse pumps dvxUpdate(), which could re-enter a rescan and
// free sAppFiles mid-iteration. Make the existing structural safety
// explicit with a reentrancy guard.
static bool sScanning = false;
if (sScanning) {
return;
}
sScanning = true;
// Free icons from previous scan
for (int32_t i = 0; i < sAppCount; i++) {
free(sAppFiles[i].iconData);
@ -450,6 +463,7 @@ static void scanAppsDir(void) {
}
dvxLog("Progman: found %ld app(s)", (long)sAppCount);
sScanning = false;
}
@ -484,7 +498,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
continue;
}
if (!dvxHasExt(names[i], ".app") || strcasecmp(names[i], "progman.app") == 0) {
if (!dvxHasExt(names[i], PM_APP_EXT) || strcasecmp(names[i], "progman.app") == 0) {
continue;
}
@ -494,7 +508,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
// Default name from filename (without .app extension)
int32_t len = (int32_t)strlen(names[i]);
int32_t nameLen = len - 4;
int32_t nameLen = len - (int32_t)strlen(PM_APP_EXT);
if (nameLen >= SHELL_APP_NAME_MAX) {
nameLen = SHELL_APP_NAME_MAX - 1;
@ -503,9 +517,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
memcpy(newEntry.name, names[i], nameLen);
newEntry.name[nameLen] = '\0';
if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') {
newEntry.name[0] -= 32;
}
newEntry.name[0] = (char)toupper((unsigned char)newEntry.name[0]);
// Override from embedded resources if available
newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch);

View file

@ -342,6 +342,8 @@ shellConfigPath(ctx, "settings.ini", path, sizeof(path));
.toc 1 Desktop Callbacks
.index shellRegisterDesktopUpdate
.index shellUnregisterDesktopUpdate
.index shellRegisterIdle
.index shellUnregisterIdle
.index shellDesktopUpdate
.index shellCtrlEscFn
@ -354,7 +356,7 @@ The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange,
.table
AppContextT field Handler Purpose
----------------- ------- -------
idleCallback idleYield Yields to cooperative tasks when dvxUpdate has no input events and no dirty rects.
idleCallback shellIdleDispatch Runs every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
onCtrlEsc ctrlEscHandler Invokes shellCtrlEscFn if the Task Manager DXE is loaded.
onF1 f1HelpHandler Launches the help viewer, optionally passing the focused app's helpFile and helpTopic.
onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh.
@ -376,6 +378,22 @@ void shellUnregisterDesktopUpdate(void (*updateFn)(void));
Remove a previously registered callback. Call this before app shutdown to avoid dangling function pointers.
.h2 shellRegisterIdle
.code
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
.endcode
Register an idle handler invoked every idle frame by the shell main loop, regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook, which the shell calls on both graceful reap and force-kill.
.h2 shellUnregisterIdle
.code
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
.endcode
Remove a previously registered idle handler. No-op if the (fn, ctx) pair is absent, so a double-unregister is harmless.
.h2 shellDesktopUpdate
.code

View file

@ -46,7 +46,6 @@
// Slot 0 is reserved (represents the shell itself); apps use slots 1+.
// New slots are appended as needed; freed slots are recycled.
static ShellAppT *sApps = NULL;
static int32_t sAppsCap = 0; // number of slots allocated (including slot 0)
// Ctrl+Esc handler -- set by taskmgr DXE constructor, NULL if not loaded
void (*shellCtrlEscFn)(AppContextT *ctx) = NULL;
@ -60,7 +59,7 @@ static void appTaskWrapper(void *arg);
static void cleanupTempFile(ShellAppT *app);
static int32_t copyFile(const char *src, const char *dst);
static ShellAppT *findLoadedPath(const char *path);
static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
void shellAppInit(void);
int32_t shellAppSlotCount(void);
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);
@ -113,9 +112,19 @@ static void appTaskWrapper(void *arg) {
ShellAppT *app = &sApps[appId];
app->dxeCtx->shellCtx->currentAppId = app->appId;
app->entryFn(app->dxeCtx);
// Re-resolve the slot: the app's main loop may have run for a long
// time, and any app loaded meanwhile can grow (realloc, move) sApps
// -- writing through the pointer captured above would corrupt freed
// memory and the app would never reach Terminating to be reaped.
app = &sApps[appId];
app->dxeCtx->shellCtx->currentAppId = 0;
// App returned from its main loop -- mark for reaping
// App returned from its main loop -- mark for reaping. This task is
// about to self-terminate (taskTrampoline -> tsExit), which frees its
// slot; clear mainTaskId so the deferred reap does not tsKill a recycled
// slot that a later tsCreate may already have handed to a new task.
app->mainTaskId = 0;
app->state = AppStateTerminatingE;
}
@ -146,7 +155,11 @@ static int32_t copyFile(const char *src, const char *dst) {
return -1;
}
char buf[32768];
// Static rather than on-stack: shellLoadApp runs on app task stacks as
// small as TS_DEFAULT_STACK_SIZE (32KB), which a 32KB stack frame would
// overflow. The cooperative switcher is single-threaded, so a shared
// copy buffer is safe.
static char buf[32768];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
@ -182,7 +195,7 @@ static ShellAppT *findLoadedPath(const char *path) {
// environment variable if set, otherwise falls back to the current directory.
// The slot ID is embedded in the filename to ensure uniqueness.
// Example: TEMP=C:\TEMP -> "C:\TEMP\_dvx02.app"
static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize) {
static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize) {
// Find extension from original path
const char *lastSlash = platformPathDirEnd(origPath);
const char *dot = strrchr(origPath, '.');
@ -202,8 +215,6 @@ static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t
} else {
snprintf(out, outSize, "_dvx%02ld%s", (long)id, dot);
}
return 0;
}
@ -212,7 +223,6 @@ void shellAppInit(void) {
ShellAppT slot0;
memset(&slot0, 0, sizeof(slot0));
arrput(sApps, slot0);
sAppsCap = 1;
}
@ -231,8 +241,11 @@ int32_t shellEnsureConfigDir(const DxeAppContextT *ctx) {
}
// Forcible kill -- no shutdown hook is called. Used for crashed apps
// (where running more app code would be unsafe) and for "End Task".
// Forcible kill -- the shutdownFn IS still called (so the app can unregister
// shell callbacks that would otherwise dangle), but defensively: currentAppId
// is cleared first so a re-fault during shutdown lands in shell-level crash
// recovery rather than re-entering this function. Used for crashed apps and
// for "End Task".
// Cleanup order matters: windows first (removes them from the compositor),
// then the task (frees the stack), then the DXE handle (unmaps the code).
// If we closed the DXE first, destroying windows could call into unmapped
@ -242,13 +255,17 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
return;
}
// Call shutdown hook so the app can unregister callbacks, close
// handles, etc. Without this, dangling function pointers (e.g.
// shellDesktopUpdate callbacks) cause crashes after dlclose.
// Call shutdown hook so the app can unregister shell callbacks (e.g.
// shellRegisterDesktopUpdate) before its DXE is unmapped; otherwise the
// dangling function pointers crash the next shellDesktopUpdate().
// currentAppId is cleared BEFORE the call so that if a crashed app
// re-faults inside its own shutdownFn, the longjmp returns to crash
// recovery (shellMain.c) with currentAppId == 0 -- recovery then treats
// it as a shell-level fault and does NOT re-enter shellForceKillApp,
// breaking the otherwise-infinite kill loop.
if (app->shutdownFn) {
ctx->currentAppId = app->appId;
app->shutdownFn();
ctx->currentAppId = 0;
app->shutdownFn();
}
// Destroy all windows belonging to this app. Walk backwards because
@ -316,10 +333,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
// Allocate a slot
int32_t id = allocSlot();
if (id < 0) {
dvxErrorBox(ctx, NULL, "Maximum number of applications reached.");
return -1;
}
// allocSlot grows the dynamic slot array on demand, so it never fails.
// Check if this DXE is already loaded. If so, check whether the app
// allows multiple instances. We read the descriptor from the existing
@ -432,9 +446,14 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
if (!app->dxeCtx) {
dvxLog("Shell: failed to allocate app context for %s", app->name);
ctx->currentAppId = 0;
dvxSetBusy(ctx, false);
dlclose(handle);
app->state = AppStateFreeE;
if (tempPath[0]) {
remove(tempPath);
}
return -1;
}
@ -493,6 +512,11 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
free(app->dxeCtx);
app->dxeCtx = NULL;
app->state = AppStateFreeE;
if (tempPath[0]) {
remove(tempPath);
}
return -1;
}
@ -535,7 +559,6 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
ctx->currentAppId = app->appId;
app->shutdownFn();
ctx->currentAppId = 0;
ctx->currentAppId = 0;
}
// Destroy all windows belonging to this app

View file

@ -154,18 +154,20 @@ int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *arg
// Reap finished apps (call each frame from main loop). Returns true if
// any apps were reaped, so the caller can notify the desktop to refresh.
// For callback-only apps, termination is triggered by shellWrapDestroyWindow
// when the last window closes. For main-loop apps, termination happens
// when appMain returns (via appTaskWrapper marking AppStateTerminatingE).
// For callback-only apps, termination is detected here by scanning for
// zero remaining windows owned by the app (they have no main loop to set
// AppStateTerminatingE). For main-loop apps, termination happens when
// appMain returns (via appTaskWrapper marking AppStateTerminatingE).
bool shellReapApps(AppContextT *ctx);
// Gracefully shut down a single app -- calls shutdownFn if present,
// destroys windows, kills task, closes DXE handle.
void shellReapApp(AppContextT *ctx, ShellAppT *app);
// Forcibly kill an app (Task Manager "End Task"). Skips shutdownFn --
// used when the app is hung or has crashed and cannot be trusted to
// run its own cleanup code.
// Forcibly kill an app (Task Manager "End Task"). Still calls shutdownFn so
// the app can unregister shell callbacks (which would otherwise dangle after
// dlclose), but currentAppId is cleared first so a re-fault during shutdown
// is caught by shell-level crash recovery rather than looping back here.
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
// Terminate all running apps (shell shutdown)
@ -208,6 +210,17 @@ void shellUnregisterDesktopUpdate(void (*updateFn)(void));
// Notify the desktop app that app state has changed (load, reap, crash).
void shellDesktopUpdate(void);
// Register an idle handler called every idle frame by the shell main loop,
// regardless of which app is foreground. Serial/secLink pollers register
// here so background connections keep pumping. Idempotent on the (fn, ctx)
// pair. The registering module MUST unregister before it is unloaded (for
// app DXEs, from the _appShutdown hook) or the handler will dangle.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
// Remove a previously registered idle handler. No-op if absent, so a
// double-unregister is harmless.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
// ============================================================
// Ctrl+Esc handler (set by taskmgr library)
// ============================================================

View file

@ -80,21 +80,51 @@ static volatile int sCrashSignal = 0;
typedef void (*DesktopUpdateFnT)(void);
static DesktopUpdateFnT *sDesktopUpdateFns = NULL;
// Idle handler list (dynamic, managed via stb_ds arrput/arrdel). Walked
// every idle frame by shellIdleDispatch. Serial/secLink pollers register
// here instead of overwriting the context's single idleCallback slot, so a
// background connection keeps pumping regardless of which app is foreground
// and the shell's cooperative yield can never be clobbered.
typedef struct {
void (*fn)(void *ctx);
void *ctx;
} IdleHandlerT;
static IdleHandlerT *sIdleHandlers = NULL;
// ============================================================
// Prototypes
// ============================================================
static void appIdRestore(int32_t value);
static int32_t appIdSave(void);
static void ctrlEscHandler(void *ctx);
static void f1HelpHandler(void *ctx);
static void idleYield(void *ctx);
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
void shellDesktopUpdate(void);
static void shellIdleDispatch(void *ctx);
int shellMain(int argc, char *argv[]);
void shellRegisterDesktopUpdate(void (*updateFn)(void));
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
void shellUnregisterDesktopUpdate(void (*updateFn)(void));
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
static void titleChangeHandler(void *ctx);
// Task-switch hooks: currentAppId is semantically per-task (each app
// task runs as its own appId; the shell's main task is 0). Without
// save/restore on every switch, the first yield back from an app left
// the shell's allocations and windows attributed to that app.
static void appIdRestore(int32_t value) {
sCtx.currentAppId = value;
}
static int32_t appIdSave(void) {
return sCtx.currentAppId;
}
static void ctrlEscHandler(void *ctx) {
if (shellCtrlEscFn) {
shellCtrlEscFn((AppContextT *)ctx);
@ -135,29 +165,11 @@ static void f1HelpHandler(void *ctx) {
}
// Fall back to system help if no app-specific help
char viewerPath[DVX_MAX_PATH];
snprintf(viewerPath, sizeof(viewerPath), "APPS" DVX_PATH_SEP "KPUNCH" DVX_PATH_SEP "DVXHELP" DVX_PATH_SEP "DVXHELP.APP");
if (!args[0]) {
snprintf(args, sizeof(args), "APPS" DVX_PATH_SEP "KPUNCH" DVX_PATH_SEP "PROGMAN" DVX_PATH_SEP "DVXHELP.HLP");
}
shellLoadAppWithArgs(ac, viewerPath, args);
}
// Registered as sCtx.idleCallback. dvxUpdate calls this when it has
// 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
// 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
// (which would do a scheduler scan) when the shell is the only task.
static void idleYield(void *ctx) {
(void)ctx;
if (tsActiveCount() > 1) {
tsYield();
}
shellLoadAppWithArgs(ac, DVX_HELP_VIEWER_APP_LITERAL, args);
}
@ -174,11 +186,52 @@ void shellDesktopUpdate(void) {
}
// Installed as the context's single idleCallback. dvxUpdate calls this when
// all events are processed and nothing is dirty. We walk the idle handler
// list (serial/secLink pollers register here), then perform the cooperative
// yield unconditionally as the tail step -- so a registered poller can never
// suppress task switching the way the old single-slot scheme did, where BASIC
// overwrote idleYield outright and never restored it. The tsActiveCount > 1
// check avoids a scheduler scan when the shell is the only task. arrlen is
// re-read each iteration so a handler that unregisters one mid-walk (e.g. via
// a nested modal dvxUpdate) cannot index past the array.
static void shellIdleDispatch(void *ctx) {
(void)ctx;
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn) {
sIdleHandlers[i].fn(sIdleHandlers[i].ctx);
}
}
if (tsActiveCount() > 1) {
tsYield();
}
}
void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
arrput(sDesktopUpdateFns, updateFn);
}
// Idempotent: a (fn, ctx) pair already present is not added twice, so a
// producer can call this on every attach without tracking its own state.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx) {
IdleHandlerT handler;
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
return;
}
}
handler.fn = fn;
handler.ctx = ctx;
arrput(sIdleHandlers, handler);
}
void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
if (sDesktopUpdateFns[i] == updateFn) {
@ -189,6 +242,17 @@ void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
}
// No-op if the (fn, ctx) pair is absent, so double-unregister is safe.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx) {
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
arrdel(sIdleHandlers, i);
return;
}
}
}
static void titleChangeHandler(void *ctx) {
(void)ctx;
shellDesktopUpdate();
@ -207,7 +271,7 @@ int shellMain(int argc, char *argv[]) {
// Initialize task system (no GUI needed)
if (tsInit() != TS_OK) {
dvxLog("Failed to initialize task system");
dvxShutdown(&sCtx);
// dvxInit has not run yet at this point, so there is nothing to shut down.
return 1;
}
@ -224,6 +288,10 @@ int shellMain(int argc, char *argv[]) {
// attributed to whichever app is currently executing.
dvxMemAppIdPtr = &sCtx.currentAppId;
// Make currentAppId task-local: saved/restored on every task switch
// so the shell's baseline (0) is not clobbered by app main loops.
tsSetContextHooks(appIdSave, appIdRestore);
// Set up idle callback for cooperative yielding. When dvxUpdate has
// no work to do (no input events, no dirty rects), it calls this
// instead of busy-looping. This is the main mechanism for giving
@ -250,7 +318,7 @@ int shellMain(int argc, char *argv[]) {
}
// Set callbacks AFTER dvxInit (which memsets the context to zero)
sCtx.idleCallback = idleYield;
sCtx.idleCallback = shellIdleDispatch;
sCtx.idleCtx = &sCtx;
sCtx.onCtrlEsc = ctrlEscHandler;
sCtx.ctrlEscCtx = &sCtx;

View file

@ -389,7 +389,6 @@ Software-rendered 16x16 cursor using AND/XOR mask encoding.
CHROME_TOTAL_TOP 26 Total inset from top of frame to content
CHROME_TOTAL_SIDE 6 Total inset from side of frame to content
CHROME_TOTAL_BOTTOM 6 Total inset from bottom of frame to content
CHROME_CLOSE_BTN_SIZE 16 Close button gadget size
.endtable
.h2 Hit Test Constants
@ -1394,15 +1393,16 @@ Returns: Pointer to the new MenuBarT.
.h3 wmDestroyMenuBar
.code
void wmDestroyMenuBar(WindowT *win);
void wmDestroyMenuBar(WindowT *win, const DisplayT *d);
.endcode
Free the menu bar and reclaim the content area.
Free the menu bar and reclaim the content area. The content buffer is reallocated to fit the grown content area.
.table
Parameter Description
--------- -----------
win Window to remove menu bar from
d Display context; pass NULL only during window teardown
.endtable
.h3 wmAddMenu
@ -4800,7 +4800,6 @@ Extern variables defined in widgetCore.c. These hold system-wide widget state: o
sKeyPressedBtn WidgetT* Button being pressed via keyboard (for release tracking)
sOpenPopup WidgetT* Currently open popup (dropdown/combo list)
sDragWidget WidgetT* Widget being dragged (any drag type)
sPollWidgetCount int32_t Count of widgets with WCLASS_NEEDS_POLL flag set
sPollWidgets WidgetT** Dynamic array of poll widgets (stb_ds)
sCursorBlinkFn fn ptr Callback invoked when the blink flips (repaints focused text)
.endtable
@ -4935,14 +4934,6 @@ const char *clipboardGet(int32_t *outLen);
Retrieve clipboard text.
.h3 clipboardMaxLen
.code
int32_t clipboardMaxLen(void);
.endcode
Return the fixed capacity of the clipboard buffer in bytes.
.h2 Scrollbar Helpers
.h3 ScrollHitE

View file

@ -262,7 +262,6 @@ Chrome constants are compile-time defines:
CHROME_INNER_BORDER = 2px
CHROME_MENU_HEIGHT = 20px
SCROLLBAR_WIDTH = 16px
CHROME_CLOSE_BTN_SIZE = 16px
.endcode
.h2 Hit Test Regions

File diff suppressed because it is too large Load diff

View file

@ -45,6 +45,12 @@
#include <time.h>
// Maximum windows that can be queued for deferred destruction during a
// single dispatch pass. On overflow the destroy happens immediately,
// which risks the callback use-after-free this mechanism prevents -- the
// limit is far above what any real dispatch produces.
#define DVX_PENDING_DESTROY_MAX 32
// ============================================================
// Application context
// ============================================================
@ -68,8 +74,6 @@ typedef struct AppContextT {
KbMoveResizeT kbMoveResize;
CursorT cursors[CURSOR_COUNT]; // indexed by CURSOR_xxx
int32_t cursorId; // active cursor shape
uint32_t cursorFg; // pre-packed cursor colors
uint32_t cursorBg;
bool running;
int32_t mouseX;
int32_t mouseY;
@ -106,6 +110,15 @@ typedef struct AppContextT {
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
void *idleCtx;
WindowT *modalWindow; // if non-NULL, only this window receives input
// Deferred window destruction. Destroying a window from inside a
// user callback (onClick, timer tick, BASIC event handler) would
// free widget memory the dispatch code still touches after the
// callback returns. While dispatchDepth > 0, dvxDestroyWindow
// hides the window and queues its id here; the queue is flushed in
// dvxUpdate once the dispatch stack has fully unwound.
int32_t dispatchDepth;
int32_t pendingDestroyIds[DVX_PENDING_DESTROY_MAX];
int32_t pendingDestroyCount;
void (*onCtrlEsc)(void *ctx); // system-wide Ctrl+Esc handler (e.g. task manager)
void *ctrlEscCtx;
void (*onF1)(void *ctx); // system-wide F1 handler (context-sensitive help)
@ -136,7 +149,6 @@ typedef struct AppContextT {
uint8_t colorRgb[ColorCountE][3];
// Available video modes (enumerated once at init)
VideoModeInfoT *videoModes; // stb_ds dynamic array
int32_t videoModeCount;
// Wallpaper -- pre-rendered to screen dimensions in native pixel format.
// NULL means no wallpaper (solid desktop color). wallpaperPath is
// kept so the image can be reloaded after a video mode or mode change.

View file

@ -53,6 +53,9 @@
// so adjacent chrome/content invalidations merge naturally.
#define DIRTY_MERGE_GAP CHROME_BORDER_WIDTH
// Per-slot cap on inner-scan restarts during a merge pass (see merge below).
#define DIRTY_MERGE_MAX_RESTARTS 3
// ============================================================
// Prototypes
// ============================================================
@ -159,7 +162,7 @@ void dirtyListMerge(DirtyListT *dl) {
int32_t restarts = 0;
bool merged = true;
while (merged && restarts < 3) {
while (merged && restarts < DIRTY_MERGE_MAX_RESTARTS) {
merged = false;
for (int32_t j = i + 1; j < dl->count; j++) {
@ -201,10 +204,10 @@ void flushRect(DisplayT *d, const RectT *r) {
// The min/max formulation avoids branches in the hot path.
bool rectIntersect(const RectT *a, const RectT *b, RectT *result) {
int32_t ix1 = a->x > b->x ? a->x : b->x;
int32_t iy1 = a->y > b->y ? a->y : b->y;
int32_t ix2 = (a->x + a->w) < (b->x + b->w) ? (a->x + a->w) : (b->x + b->w);
int32_t iy2 = (a->y + a->h) < (b->y + b->h) ? (a->y + a->h) : (b->y + b->h);
int32_t ix1 = DVX_MAX(a->x, b->x);
int32_t iy1 = DVX_MAX(a->y, b->y);
int32_t ix2 = DVX_MIN(a->x + a->w, b->x + b->w);
int32_t iy2 = DVX_MIN(a->y + a->h, b->y + b->h);
if (__builtin_expect(ix1 >= ix2 || iy1 >= iy2, 0)) {
return false;
@ -252,10 +255,10 @@ static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_
// region algebra on 486-class hardware.
static inline void rectUnion(const RectT *a, const RectT *b, RectT *result) {
int32_t x1 = a->x < b->x ? a->x : b->x;
int32_t y1 = a->y < b->y ? a->y : b->y;
int32_t x2 = (a->x + a->w) > (b->x + b->w) ? (a->x + a->w) : (b->x + b->w);
int32_t y2 = (a->y + a->h) > (b->y + b->h) ? (a->y + a->h) : (b->y + b->h);
int32_t x1 = DVX_MIN(a->x, b->x);
int32_t y1 = DVX_MIN(a->y, b->y);
int32_t x2 = DVX_MAX(a->x + a->w, b->x + b->w);
int32_t y2 = DVX_MAX(a->y + a->h, b->y + b->h);
result->x = x1;
result->y = y1;

View file

@ -51,10 +51,11 @@ void dirtyListInit(DirtyListT *dl);
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h);
// Consolidate the dirty list by merging overlapping and adjacent rects.
// Uses an iterative pairwise merge: for each pair of rects, if merging
// them doesn't increase the total area by more than a threshold, they're
// combined. This reduces the number of compositor passes and LFB flush
// operations at the cost of potentially redrawing some clean pixels.
// Uses an iterative pairwise merge: for each pair of rects, if they overlap
// or the gap between them is within DIRTY_MERGE_GAP pixels, they're combined
// into their axis-aligned bounding box. This reduces the number of compositor
// passes and LFB flush operations at the cost of potentially redrawing some
// clean pixels.
void dirtyListMerge(DirtyListT *dl);
// Reset the dirty list to empty.

View file

@ -54,6 +54,11 @@
#define CURSOR_CROSSHAIR 6 // crosshair (+) for placement
#define CURSOR_COUNT 7
// All cursors are square; bitmaps are one uint16_t per row.
#define CURSOR_DIM 16
// Center hot spot used by the resize/busy/crosshair cursors.
#define CURSOR_HOTSPOT 7
// ============================================================
// Standard arrow cursor, 16x16
// ============================================================
@ -63,7 +68,7 @@
// 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.
static const uint16_t cursorArrowAnd[16] = {
static const uint16_t cursorArrowAnd[CURSOR_DIM] = {
0x3FFF, // 0011111111111111 row 0
0x1FFF, // 0001111111111111 row 1
0x0FFF, // 0000111111111111 row 2
@ -82,7 +87,7 @@ static const uint16_t cursorArrowAnd[16] = {
0xFC3F // 1111110000111111 row 15
};
static const uint16_t cursorArrowXor[16] = {
static const uint16_t cursorArrowXor[CURSOR_DIM] = {
0x0000, // 0000000000000000 row 0
0x4000, // 0100000000000000 row 1
0x6000, // 0110000000000000 row 2
@ -130,7 +135,7 @@ static const uint16_t cursorArrowXor[16] = {
// row 10: ..XXXX....XXXX..
// row 11: ...XXX....XXX...
static const uint16_t cursorResizeHAnd[16] = {
static const uint16_t cursorResizeHAnd[CURSOR_DIM] = {
0xFFFF, // 1111111111111111 row 0
0xFFFF, // 1111111111111111 row 1
0xFFFF, // 1111111111111111 row 2
@ -149,7 +154,7 @@ static const uint16_t cursorResizeHAnd[16] = {
0xFFFF // 1111111111111111 row 15
};
static const uint16_t cursorResizeHXor[16] = {
static const uint16_t cursorResizeHXor[CURSOR_DIM] = {
0x0000, // 0000000000000000 row 0
0x0000, // 0000000000000000 row 1
0x0000, // 0000000000000000 row 2
@ -173,7 +178,7 @@ static const uint16_t cursorResizeHXor[16] = {
// ============================================================
// Hot spot at center (7, 7)
static const uint16_t cursorResizeVAnd[16] = {
static const uint16_t cursorResizeVAnd[CURSOR_DIM] = {
0xFFFF, // row 0 (transparent)
0xFFFF, // row 1
0xFE7F, // 1111111001111111 row 2 top arrow
@ -192,7 +197,7 @@ static const uint16_t cursorResizeVAnd[16] = {
0xFFFF // row 15
};
static const uint16_t cursorResizeVXor[16] = {
static const uint16_t cursorResizeVXor[CURSOR_DIM] = {
0x0000, // row 0
0x0000, // row 1
0x0100, // 0000000100000000 row 2
@ -233,7 +238,7 @@ static const uint16_t cursorResizeVXor[16] = {
// ...........XXXX.
// ..........XXXXX. SE arrowhead (mirror of NW)
static const uint16_t cursorResizeDiagNWSEAnd[16] = {
static const uint16_t cursorResizeDiagNWSEAnd[CURSOR_DIM] = {
0x01FF, // 0000000111111111 row 0 NW outline
0x01FF, // 0000000111111111 row 1
0x01FF, // 0000000111111111 row 2
@ -252,7 +257,7 @@ static const uint16_t cursorResizeDiagNWSEAnd[16] = {
0xFF80 // 1111111110000000 row 15
};
static const uint16_t cursorResizeDiagNWSEXor[16] = {
static const uint16_t cursorResizeDiagNWSEXor[CURSOR_DIM] = {
0x0000, // 0000000000000000 row 0
0x7C00, // 0111110000000000 row 1 XXXXX
0x7800, // 0111100000000000 row 2 XXXX
@ -291,7 +296,7 @@ static const uint16_t cursorResizeDiagNWSEXor[16] = {
// .XXXX...........
// .XXXXX.......... SW arrowhead
static const uint16_t cursorResizeDiagNESWAnd[16] = {
static const uint16_t cursorResizeDiagNESWAnd[CURSOR_DIM] = {
0xFF80, // 1111111110000000 row 0 NE outline
0xFF80, // 1111111110000000 row 1
0xFF80, // 1111111110000000 row 2
@ -310,7 +315,7 @@ static const uint16_t cursorResizeDiagNESWAnd[16] = {
0x01FF // 0000000111111111 row 15
};
static const uint16_t cursorResizeDiagNESWXor[16] = {
static const uint16_t cursorResizeDiagNESWXor[CURSOR_DIM] = {
0x0000, // 0000000000000000 row 0
0x003E, // 0000000000111110 row 1 XXXXX
0x001E, // 0000000000011110 row 2 XXXX
@ -337,7 +342,7 @@ static const uint16_t cursorResizeDiagNESWXor[16] = {
// black sand accumulated in the bottom half, horizontal bars
// at top and bottom. Modeled after the Windows 3.1 wait cursor.
static const uint16_t cursorBusyAnd[16] = {
static const uint16_t cursorBusyAnd[CURSOR_DIM] = {
// .BBBBBBBBBBBBBB.
0x8001, // row 0 top bar
0x8001, // row 1 .BWWWWWWWWWWWWB.
@ -357,7 +362,7 @@ static const uint16_t cursorBusyAnd[16] = {
0xFFFF // row 15 ................ transparent
};
static const uint16_t cursorBusyXor[16] = {
static const uint16_t cursorBusyXor[CURSOR_DIM] = {
0x0000, // row 0 top bar (black)
0x3FFC, // row 1 white interior
0x1FF8, // row 2 white interior
@ -382,7 +387,7 @@ static const uint16_t cursorBusyXor[16] = {
// ============================================================
// Hot spot at center (7, 7)
static const uint16_t cursorCrosshairAnd[16] = {
static const uint16_t cursorCrosshairAnd[CURSOR_DIM] = {
0xFFFF, // row 0
0xFC3F, // 1111110000111111 row 1 black outline top
0xFC3F, // 1111110000111111 row 2
@ -401,7 +406,7 @@ static const uint16_t cursorCrosshairAnd[16] = {
0xFFFF // row 15
};
static const uint16_t cursorCrosshairXor[16] = {
static const uint16_t cursorCrosshairXor[CURSOR_DIM] = {
0x0000, // row 0
0x0180, // 0000000110000000 row 1 white inner
0x0180, // row 2
@ -426,17 +431,13 @@ static const uint16_t cursorCrosshairXor[16] = {
// ============================================================
static const CursorT dvxCursors[CURSOR_COUNT] = {
{ 16, 16, 0, 0, cursorArrowAnd, cursorArrowXor }, // CURSOR_ARROW
{ 16, 16, 7, 7, cursorResizeHAnd, cursorResizeHXor }, // CURSOR_RESIZE_H
{ 16, 16, 7, 7, cursorResizeVAnd, cursorResizeVXor }, // CURSOR_RESIZE_V
{ 16, 16, 7, 7, cursorResizeDiagNWSEAnd, cursorResizeDiagNWSEXor }, // CURSOR_RESIZE_DIAG_NWSE
{ 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW
{ 16, 16, 7, 7, cursorBusyAnd, cursorBusyXor }, // CURSOR_BUSY
{ 16, 16, 7, 7, cursorCrosshairAnd, cursorCrosshairXor }, // CURSOR_CROSSHAIR
{ CURSOR_DIM, CURSOR_DIM, 0, 0, cursorArrowAnd, cursorArrowXor }, // CURSOR_ARROW
{ CURSOR_DIM, CURSOR_DIM, CURSOR_HOTSPOT, CURSOR_HOTSPOT, cursorResizeHAnd, cursorResizeHXor }, // CURSOR_RESIZE_H
{ CURSOR_DIM, CURSOR_DIM, CURSOR_HOTSPOT, CURSOR_HOTSPOT, cursorResizeVAnd, cursorResizeVXor }, // CURSOR_RESIZE_V
{ CURSOR_DIM, CURSOR_DIM, CURSOR_HOTSPOT, CURSOR_HOTSPOT, cursorResizeDiagNWSEAnd, cursorResizeDiagNWSEXor }, // CURSOR_RESIZE_DIAG_NWSE
{ CURSOR_DIM, CURSOR_DIM, CURSOR_HOTSPOT, CURSOR_HOTSPOT, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW
{ CURSOR_DIM, CURSOR_DIM, CURSOR_HOTSPOT, CURSOR_HOTSPOT, cursorBusyAnd, cursorBusyXor }, // CURSOR_BUSY
{ CURSOR_DIM, CURSOR_DIM, CURSOR_HOTSPOT, CURSOR_HOTSPOT, cursorCrosshairAnd, cursorCrosshairXor }, // CURSOR_CROSSHAIR
};
// Legacy alias -- kept for backward compatibility with code that predates
// the multi-cursor support.
static const CursorT dvxCursor = { 16, 16, 0, 0, cursorArrowAnd, cursorArrowXor };
#endif // DVX_CUR_H

View file

@ -90,6 +90,9 @@
#define ICON_INNER_R2 (ICON_INNER_R * ICON_INNER_R)
#define FD_DIALOG_WIDTH 360 // file dialog window width
#define FD_DIALOG_HEIGHT 340 // file dialog window height
#define FD_MAX_FILTERS 16 // cap on filter labels shown in dropdown
#define FD_PATTERN_MAX 128 // max filter-pattern buffer length
#define FD_EXT_MAX 16 // max appended extension buffer length
// Input box layout constants
#define IB_DIALOG_WIDTH 300
@ -101,7 +104,27 @@
#define CB_LIST_HEIGHT 120
#define CB_PADDING 8
#define FD_NAME_LEN 64
// ============================================================
// Word-wrap iterator types
// ============================================================
//
// The greedy word-wrap state machine lives in a single iterator
// (wordWrapIterate) that invokes a per-line callback. Drawing and
// height-measuring are both thin wrappers over that iterator.
typedef void (*WordWrapLineCbT)(const char *line, int32_t lineLen, int32_t lineIndex, void *userData);
typedef struct {
DisplayT *d;
const BlitOpsT *ops;
const BitmapFontT *font;
int32_t x;
int32_t y;
int32_t lineH;
uint32_t fg;
uint32_t bg;
} WordWrapDrawCtxT;
// ============================================================
@ -182,8 +205,10 @@ static void cbOnCancel(WidgetT *w);
static void cbOnClose(WindowT *win);
static void cbOnDblClick(WidgetT *w);
static void cbOnOk(WidgetT *w);
static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color);
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color);
static bool fdAcceptFile(const char *name);
static void fdDisplayToDirName(const char *display, char *out, int32_t outSize);
static int fdEntryCompare(const void *a, const void *b);
static const char *fdExtractPattern(const FileFilterT *f, char *buf, int32_t bufSize);
static bool fdFilterMatch(const char *name, const char *pattern);
@ -197,7 +222,7 @@ static void fdOnFilterChange(WidgetT *w);
static void fdOnListClick(WidgetT *w);
static void fdOnListDblClick(WidgetT *w);
static void fdOnOk(WidgetT *w);
static void fdOnPathSubmit(WidgetT *w);
static void fdOnPathKey(WidgetT *w, int32_t keyCode, int32_t shift);
static bool fdValidateFilename(const char *name);
static void ibOnCancel(WidgetT *w);
static void ibOnClose(WindowT *win);
@ -209,7 +234,10 @@ static void onButtonClick(WidgetT *w);
static void onMsgBoxClose(WindowT *win);
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
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 wordWrapDrawLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData);
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW);
static void wordWrapHeightLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData);
static void wordWrapIterate(const BitmapFontT *font, const char *text, int32_t maxW, WordWrapLineCbT cb, void *userData);
static void cbOnCancel(WidgetT *w) {
@ -242,6 +270,23 @@ static void cbOnOk(WidgetT *w) {
}
// Draws the shared 2-pixel ring used by the circular message-box icons
// (info, error, question) via the integer distance-squared test.
static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color) {
for (int32_t row = 0; row < ICON_GLYPH_SIZE; row++) {
for (int32_t col = 0; col < ICON_GLYPH_SIZE; col++) {
int32_t dx = col - ICON_GLYPH_CENTER;
int32_t dy = row - ICON_GLYPH_CENTER;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= ICON_OUTER_R2 && d2 >= ICON_INNER_R2) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
}
// Procedurally draws message box icons using only integer math:
// MB_ICONINFO: circle with 'i' (information)
// MB_ICONWARNING: triangle with '!' (warning)
@ -265,17 +310,7 @@ static void cbOnOk(WidgetT *w) {
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color) {
if (iconType == MB_ICONINFO) {
// Circle outline with 'i'
for (int32_t row = 0; row < ICON_GLYPH_SIZE; row++) {
for (int32_t col = 0; col < ICON_GLYPH_SIZE; col++) {
int32_t dx = col - ICON_GLYPH_CENTER;
int32_t dy = row - ICON_GLYPH_CENTER;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= ICON_OUTER_R2 && d2 >= ICON_INNER_R2) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
drawIconCircle(d, ops, x, y, color);
rectFill(d, ops, x + 11, y + 6, 2, 2, color);
rectFill(d, ops, x + 11, y + 10, 2, 8, color);
@ -298,17 +333,7 @@ static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y
rectFill(d, ops, x + 11, y + 19, 2, 2, color);
} else if (iconType == MB_ICONERROR) {
// Circle outline with X
for (int32_t row = 0; row < ICON_GLYPH_SIZE; row++) {
for (int32_t col = 0; col < ICON_GLYPH_SIZE; col++) {
int32_t dx = col - ICON_GLYPH_CENTER;
int32_t dy = row - ICON_GLYPH_CENTER;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= ICON_OUTER_R2 && d2 >= ICON_INNER_R2) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
drawIconCircle(d, ops, x, y, color);
for (int32_t i = 0; i < ICON_GLYPH_CENTER; i++) {
rectFill(d, ops, x + 6 + i, y + 6 + i, 2, 2, color);
@ -316,17 +341,7 @@ static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y
}
} else if (iconType == MB_ICONQUESTION) {
// Circle outline with '?'
for (int32_t row = 0; row < ICON_GLYPH_SIZE; row++) {
for (int32_t col = 0; col < ICON_GLYPH_SIZE; col++) {
int32_t dx = col - ICON_GLYPH_CENTER;
int32_t dy = row - ICON_GLYPH_CENTER;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= ICON_OUTER_R2 && d2 >= ICON_INNER_R2) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
drawIconCircle(d, ops, x, y, color);
rectFill(d, ops, x + 9, y + 6, 6, 2, color);
rectFill(d, ops, x + 13, y + 8, 2, 4, color);
@ -382,6 +397,8 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
}
WidgetT *lb = wgtListBox(root);
if (lb) {
lb->weight = WGT_WEIGHT_FILL;
lb->minH = wgtPixels(CB_LIST_HEIGHT);
lb->onDblClick = cbOnDblClick;
@ -392,20 +409,30 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
}
sChoiceBox.listBox = lb;
}
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignCenterE;
WidgetT *okBtn = wgtButton(btnRow, "&OK");
if (okBtn) {
okBtn->minW = wgtPixels(BUTTON_WIDTH);
okBtn->minH = wgtPixels(BUTTON_HEIGHT);
okBtn->onClick = cbOnOk;
}
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) {
cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
cancelBtn->minH = wgtPixels(BUTTON_HEIGHT);
cancelBtn->onClick = cbOnCancel;
}
}
}
dvxFitWindow(ctx, win);
@ -437,6 +464,10 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
// be active at a time.
bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize) {
if (!ctx || !outPath || outPathSize <= 0) {
return false;
}
memset(&sFd, 0, sizeof(sFd));
sFd.ctx = ctx;
sFd.flags = flags;
@ -467,7 +498,6 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
win->modal = true;
win->onClose = fdOnClose;
win->userData = ctx;
WidgetT *root = wgtInitWindow(ctx, win);
@ -478,25 +508,40 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
// Path row
WidgetT *pathRow = wgtHBox(root);
if (pathRow) {
wgtLabel(pathRow, "&Path:");
sFd.pathInput = wgtTextInput(pathRow, DVX_MAX_PATH);
sFd.pathInput->onChange = fdOnPathSubmit;
// Navigate on Enter, not on every keystroke: onChange fires per
// character, so the user could never type a path -- a transiently
// valid directory string would navigate and rewrite the input
// mid-edit (and stat() on each key).
if (sFd.pathInput) {
sFd.pathInput->onKeyDown = fdOnPathKey;
}
}
// File list
sFd.fileList = wgtListBox(root);
if (sFd.fileList) {
sFd.fileList->weight = WGT_WEIGHT_FILL;
sFd.fileList->onChange = fdOnListClick;
sFd.fileList->onDblClick = fdOnListDblClick;
}
// Filter row (if filters provided)
if (filters && filterCount > 0) {
WidgetT *filterRow = wgtHBox(root);
if (filterRow) {
wgtLabel(filterRow, "F&ilter:");
sFd.filterDd = wgtDropdown(filterRow);
// Build filter label array (static -- lives for dialog lifetime)
static const char *filterLabels[16];
int32_t fc = filterCount < 16 ? filterCount : 16;
static const char *filterLabels[FD_MAX_FILTERS];
int32_t fc = filterCount < FD_MAX_FILTERS ? filterCount : FD_MAX_FILTERS;
for (int32_t i = 0; i < fc; i++) {
filterLabels[i] = filters[i].label;
@ -504,36 +549,57 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
wgtDropdownSetItems(sFd.filterDd, filterLabels, fc);
wgtDropdownSetSelected(sFd.filterDd, 0);
if (sFd.filterDd) {
sFd.filterDd->onChange = fdOnFilterChange;
}
}
}
// Filename row
WidgetT *nameRow = wgtHBox(root);
if (nameRow) {
wgtLabel(nameRow, "File &name:");
sFd.nameInput = wgtTextInput(nameRow, DVX_MAX_PATH);
}
// Button row
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignEndE;
WidgetT *okBtn = wgtButton(btnRow, (flags & FD_SAVE) ? "&Save" : "&Open");
if (okBtn) {
okBtn->onClick = fdOnOk;
okBtn->minW = wgtPixels(80);
okBtn->minW = wgtPixels(BUTTON_WIDTH);
}
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) {
cancelBtn->onClick = fdOnCancel;
cancelBtn->minW = wgtPixels(80);
cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
}
}
// Load initial directory
fdLoadDir();
wgtInvalidate(root);
// Modal loop
// Modal loop. Save/restore the previous modal so a file dialog
// opened from inside another modal (e.g. a Browse button) does not
// clear the outer modal's input protection on close.
WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win;
while (!sFd.done && ctx->running) {
dvxUpdate(ctx);
}
ctx->modalWindow = NULL;
ctx->modalWindow = prevModal;
// Build result path
bool result = false;
@ -547,10 +613,9 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
strncpy(sFd.outPath, tmp, DVX_MAX_PATH - 1);
sFd.outPath[DVX_MAX_PATH - 1] = '\0';
if (outPath && outPathSize > 0) {
// outPath/outPathSize are validated non-NULL/positive at entry.
strncpy(outPath, sFd.outPath, outPathSize - 1);
outPath[outPathSize - 1] = '\0';
}
result = true;
}
@ -617,27 +682,39 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const
// Text input
WidgetT *input = wgtTextInput(root, IB_INPUT_MAXLEN);
if (input) {
input->weight = 0;
sInputBox.input = input;
if (defaultText) {
wgtSetText(input, defaultText);
}
}
// Button row
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignCenterE;
WidgetT *okBtn = wgtButton(btnRow, "&OK");
if (okBtn) {
okBtn->minW = wgtPixels(BUTTON_WIDTH);
okBtn->minH = wgtPixels(BUTTON_HEIGHT);
okBtn->onClick = ibOnOk;
}
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) {
cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
cancelBtn->minH = wgtPixels(BUTTON_HEIGHT);
cancelBtn->onClick = ibOnCancel;
}
}
}
dvxFitWindow(ctx, win);
@ -702,22 +779,34 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int
}
WidgetT *spinner = wgtSpinner(root, minVal, maxVal, step);
if (spinner) {
wgtSpinnerSetValue(spinner, defaultVal);
sIntBox.spinner = spinner;
}
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignCenterE;
WidgetT *okBtn = wgtButton(btnRow, "&OK");
if (okBtn) {
okBtn->minW = wgtPixels(BUTTON_WIDTH);
okBtn->minH = wgtPixels(BUTTON_HEIGHT);
okBtn->onClick = iibOnOk;
}
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) {
cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
cancelBtn->minH = wgtPixels(BUTTON_HEIGHT);
cancelBtn->onClick = iibOnCancel;
}
}
}
dvxFitWindow(ctx, win);
@ -761,8 +850,12 @@ int32_t dvxInfoBox(AppContextT *ctx, const char *title, const char *message) {
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
int32_t btnFlags = flags & 0x000F;
int32_t iconFlags = flags & 0x00F0;
if (!ctx || !message) {
return ID_CANCEL;
}
int32_t btnFlags = flags & MB_BUTTON_MASK;
int32_t iconFlags = flags & MB_ICON_MASK;
// Determine button labels and IDs
const char *btnLabels[3];
@ -992,6 +1085,24 @@ static bool fdAcceptFile(const char *name) {
}
// Convert a list display name to a real directory name by stripping the
// surrounding brackets used for visual distinction ("[name]" -> "name").
static void fdDisplayToDirName(const char *display, char *out, int32_t outSize) {
if (display[0] == '[') {
strncpy(out, display + 1, outSize - 1);
out[outSize - 1] = '\0';
char *bracket = strchr(out, ']');
if (bracket) {
*bracket = '\0';
}
} else {
strncpy(out, display, outSize - 1);
out[outSize - 1] = '\0';
}
}
// Sort comparator for the indirect sort in fdLoadDir. Uses an index
// array rather than sorting the entryNames/entryIsDir arrays directly
// because qsort would need a struct or the comparator would need to
@ -1050,7 +1161,7 @@ static bool fdFilterMatch(const char *name, const char *pattern) {
}
// Work on a copy so we can tokenize with NUL
char buf[128];
char buf[FD_PATTERN_MAX];
strncpy(buf, pattern, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
@ -1111,8 +1222,14 @@ static void fdFreeEntries(void) {
static void fdLoadDir(void) {
fdFreeEntries();
// fdFreeEntries just freed sFd.listItems, but the listbox still holds
// that exact pointer array (wgtListBoxSetItems stores it uncopied).
// Drop the widget's reference now so any early return below cannot
// leave the listbox painting freed memory.
wgtListBoxSetItems(sFd.fileList, NULL, 0);
const char *pattern = NULL;
char patBuf[128];
char patBuf[FD_PATTERN_MAX];
if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) {
pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], patBuf, sizeof(patBuf));
@ -1149,16 +1266,29 @@ static void fdLoadDir(void) {
continue;
}
// Grow arrays
// Grow arrays. realloc through temporaries so a failure keeps the
// existing (valid) arrays instead of overwriting them with NULL --
// entryCount is not yet incremented, so on break the arrays stay
// consistent with entryCount and the sort below won't deref NULL.
int32_t idx = sFd.entryCount;
sFd.entryNames = (char **)realloc(sFd.entryNames, (idx + 1) * sizeof(char *));
sFd.entryIsDir = (bool *)realloc(sFd.entryIsDir, (idx + 1) * sizeof(bool));
char **newNames = (char **)realloc(sFd.entryNames, (idx + 1) * sizeof(char *));
if (!sFd.entryNames || !sFd.entryIsDir) {
dvxLog("Dialog: failed to realloc entry arrays");
if (!newNames) {
dvxLog("Dialog: failed to realloc entry name array");
break;
}
sFd.entryNames = newNames;
bool *newIsDir = (bool *)realloc(sFd.entryIsDir, (idx + 1) * sizeof(bool));
if (!newIsDir) {
dvxLog("Dialog: failed to realloc entry dir-flag array");
break;
}
sFd.entryIsDir = newIsDir;
sFd.entryIsDir[idx] = isDir;
if (isDir) {
@ -1198,6 +1328,8 @@ static void fdLoadDir(void) {
char **tmpNames = (char **)malloc(sFd.entryCount * sizeof(char *));
bool *tmpIsDir = (bool *)malloc(sFd.entryCount * sizeof(bool));
// On alloc failure, leave entries in their unsorted-but-valid order
// rather than failing the whole listing.
if (!tmpNames || !tmpIsDir) {
dvxLog("Dialog: failed to allocate temp sort arrays");
}
@ -1369,20 +1501,9 @@ static void fdOnListDblClick(WidgetT *w) {
if (sFd.entryIsDir[sel]) {
const char *display = sFd.entryNames[sel];
char dirName[FD_NAME_LEN];
char dirName[DVX_MAX_PATH];
if (display[0] == '[') {
strncpy(dirName, display + 1, sizeof(dirName) - 1);
dirName[sizeof(dirName) - 1] = '\0';
char *bracket = strchr(dirName, ']');
if (bracket) {
*bracket = '\0';
}
} else {
strncpy(dirName, display, sizeof(dirName) - 1);
dirName[sizeof(dirName) - 1] = '\0';
}
fdDisplayToDirName(display, dirName, sizeof(dirName));
fdNavigate(dirName);
wgtSetText(sFd.nameInput, "");
@ -1415,20 +1536,9 @@ static void fdOnOk(WidgetT *w) {
if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) {
const char *display = sFd.entryNames[sel];
char dirName[FD_NAME_LEN];
char dirName[DVX_MAX_PATH];
if (display[0] == '[') {
strncpy(dirName, display + 1, sizeof(dirName) - 1);
dirName[sizeof(dirName) - 1] = '\0';
char *bracket = strchr(dirName, ']');
if (bracket) {
*bracket = '\0';
}
} else {
strncpy(dirName, display, sizeof(dirName) - 1);
dirName[sizeof(dirName) - 1] = '\0';
}
fdDisplayToDirName(display, dirName, sizeof(dirName));
fdNavigate(dirName);
return;
@ -1437,10 +1547,6 @@ static void fdOnOk(WidgetT *w) {
return;
}
if (!name || name[0] == '\0') {
return;
}
// If user typed a directory path, navigate there
char testPath[DVX_MAX_PATH * 2];
snprintf(testPath, sizeof(testPath), "%s" DVX_PATH_SEP "%s", sFd.curDir, name);
@ -1459,10 +1565,14 @@ static void fdOnOk(WidgetT *w) {
char nameWithExt[DVX_MAX_PATH];
if ((sFd.flags & FD_SAVE) && !strchr(name, '.')) {
char extPatBuf[128];
const char *pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], extPatBuf, sizeof(extPatBuf));
char extPatBuf[FD_PATTERN_MAX];
const char *pattern = NULL;
const char *ext = NULL;
if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) {
pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], extPatBuf, sizeof(extPatBuf));
}
// Find first non-wildcard extension in pattern (may be "*.txt;*.doc")
if (pattern) {
const char *p = pattern;
@ -1491,10 +1601,10 @@ static void fdOnOk(WidgetT *w) {
if (ext) {
// Extract just the extension (up to ; or end)
char extBuf[16];
char extBuf[FD_EXT_MAX];
int32_t ei = 0;
while (ext[ei] && ext[ei] != ';' && ei < 15) {
while (ext[ei] && ext[ei] != ';' && ei < FD_EXT_MAX - 1) {
extBuf[ei] = ext[ei];
ei++;
}
@ -1513,7 +1623,13 @@ static void fdOnOk(WidgetT *w) {
// Enter pressed in path input.
static void fdOnPathSubmit(WidgetT *w) {
static void fdOnPathKey(WidgetT *w, int32_t keyCode, int32_t shift) {
(void)shift;
if (keyCode != KEY_ASCII_ENTER) {
return;
}
const char *path = wgtGetText(w);
if (path && path[0]) {
@ -1666,20 +1782,80 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
}
// Draw word-wrapped text.
//
// 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
// 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
// chars per line" is just maxW / charWidth -- no per-character width
// accumulation needed.
// Draw word-wrapped text. Delegates the line-breaking to wordWrapIterate
// and draws each emitted line via the wordWrapDrawLine callback.
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) {
WordWrapDrawCtxT ctx;
ctx.d = d;
ctx.ops = ops;
ctx.font = font;
ctx.x = x;
ctx.y = y;
ctx.lineH = font->charHeight;
ctx.fg = fg;
ctx.bg = bg;
wordWrapIterate(font, text, maxW, wordWrapDrawLine, &ctx);
}
// Per-line draw callback for wordWrapIterate. Computes the line's pixel Y
// from its index and draws it. Empty lines (lineLen == 0, from explicit
// newlines) draw nothing because drawTextN no-ops on a zero count.
static void wordWrapDrawLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData) {
WordWrapDrawCtxT *ctx = (WordWrapDrawCtxT *)userData;
int32_t curY = ctx->y + lineIndex * ctx->lineH;
drawTextN(ctx->d, ctx->ops, ctx->font, ctx->x, curY, line, lineLen, ctx->fg, ctx->bg, true);
}
// Compute height of word-wrapped text. Delegates the line-breaking to
// wordWrapIterate and counts the emitted lines via wordWrapHeightLine.
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) {
int32_t lines = 0;
wordWrapIterate(font, text, maxW, wordWrapHeightLine, &lines);
if (lines == 0) {
lines = 1;
}
return lines * font->charHeight;
}
// Per-line count callback for wordWrapIterate. Increments the caller's
// line counter; the line contents are not needed for measuring.
static void wordWrapHeightLine(const char *line, int32_t lineLen, int32_t lineIndex, void *userData) {
(void)line;
(void)lineLen;
(void)lineIndex;
*(int32_t *)userData += 1;
}
// Greedy word-wrap state machine.
//
// Fills each line with as many characters as fit 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). Explicit newlines are
// honored. This is a fixed-width font, so "max chars per line" is just
// maxW / charWidth -- no per-character width accumulation needed.
//
// For each completed line the callback is invoked with the line's start
// pointer, length, and zero-based index. The callbacks compute any pixel
// offsets from the index, so no Y math lives here.
static void wordWrapIterate(const BitmapFontT *font, const char *text, int32_t maxW, WordWrapLineCbT cb, void *userData) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t curY = y;
int32_t lineIndex = 0;
if (maxChars < 1) {
maxChars = 1;
@ -1695,9 +1871,10 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
break;
}
// Handle explicit newlines
// Handle explicit newlines (emit an empty line slot)
if (*text == '\n') {
curY += lineH;
cb(text, 0, lineIndex, userData);
lineIndex++;
text++;
continue;
}
@ -1719,8 +1896,8 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
lineLen = lastSpace;
}
drawTextN(d, ops, font, x, curY, text, lineLen, fg, bg, true);
curY += lineH;
cb(text, lineLen, lineIndex, userData);
lineIndex++;
text += lineLen;
// Skip the newline that ended this line (if any) so the
@ -1730,67 +1907,3 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
}
}
}
// Compute height of word-wrapped text.
//
// Duplicates the word-wrap logic from wordWrapDraw but only counts lines
// instead of drawing. This is needed to compute the dialog height before
// creating the window. The duplication is intentional -- combining them
// into a single function with a "just measure" flag would add branching
// to the draw path and make both harder to read.
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t lines = 0;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
if (*text == '\n') {
lines++;
text++;
continue;
}
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
lines++;
text += lineLen;
if (*text == '\n') {
text++;
}
}
if (lines == 0) {
lines = 1;
}
return lines * lineH;
}

View file

@ -45,6 +45,8 @@
#define MB_YESNOCANCEL 0x0003
#define MB_RETRYCANCEL 0x0004
#define MB_BUTTON_MASK 0x000F // isolates the button-set nibble
// ============================================================
// Message box icon flags (high nibble, OR with button flags)
// ============================================================
@ -54,6 +56,8 @@
#define MB_ICONERROR 0x0030
#define MB_ICONQUESTION 0x0040
#define MB_ICON_MASK 0x00F0 // isolates the icon-type nibble
// ============================================================
// Message box return values
// ============================================================
@ -89,9 +93,9 @@ int32_t dvxInfoBox(AppContextT *ctx, const char *title, const char *message);
// ============================================================
//
// Filters are displayed in a dropdown at the bottom of the file dialog.
// Pattern matching is case-insensitive and supports only single glob
// patterns (no semicolon-separated lists). This keeps the matching code
// trivial for a DOS filesystem where filenames are short and simple.
// Pattern matching is case-insensitive and supports semicolon-separated
// lists of glob patterns (e.g. "*.bmp;*.png"). A filename matches if any
// pattern in the list matches.
// File filter for the file dialog. Pattern is extracted from inside
// parentheses in the label. Example: "Text Files (*.txt)"

View file

@ -79,11 +79,25 @@
#include <string.h>
#include "dvxMem.h"
// File-internal token kinds for the '&' accelerator grammar walker.
// One logical token is decoded per accelNextToken call. This keeps the
// accelerator parsing rules in a single place, shared by accelParse,
// drawTextAccel, and textWidthAccel.
typedef enum { AccelTokenEndE, AccelTokenLiteralE, AccelTokenAccelE, AccelTokenNormalE } AccelTokenE;
// Fixed-point ITU-R luminance weights (0.299R + 0.587G + 0.114B scaled by
// 256) used by rectCopyGrayscale to convert color pixels to gray. Kept as
// named defines so the formula lives in exactly one place.
#define GRAY_WEIGHT_R 77
#define GRAY_WEIGHT_G 150
#define GRAY_WEIGHT_B 29
#define GRAY_WEIGHT_SHIFT 8
// ============================================================
// Prototypes
// ============================================================
char accelParse(const char *text);
static AccelTokenE accelNextToken(const char **text, char *outCh);
static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h);
static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp);
@ -114,44 +128,73 @@ char accelParse(const char *text) {
return 0;
}
while (*text) {
if (*text == '&') {
text++;
if (*text == '&') {
// Escaped && -- literal &, not an accelerator
text++;
continue;
}
if (*text && *text != '&') {
char ch = *text;
char ch;
AccelTokenE token;
while ((token = accelNextToken(&text, &ch)) != AccelTokenEndE) {
if (token == AccelTokenAccelE) {
if (ch >= 'A' && ch <= 'Z') {
return (char)(ch + 32);
}
if (ch >= 'a' && ch <= 'z') {
return ch;
}
if (ch >= '0' && ch <= '9') {
return ch;
return (char)(ch + ('a' - 'A'));
}
return ch;
}
break;
}
text++;
}
return 0;
}
// ============================================================
// accelNextToken
// ============================================================
//
// Decodes exactly one logical token of the '&' accelerator grammar from
// *text, advances *text past it, and reports the rendered/measured char
// in *outCh. This is the single source of truth for the accelerator
// rules shared by accelParse, drawTextAccel, and textWidthAccel:
//
// "" -> AccelTokenEndE (end of string)
// "&&" -> AccelTokenLiteralE (literal '&')
// "&x" -> AccelTokenAccelE (x is the accelerator char)
// trailing -> AccelTokenEndE (a lone '&' at end of string)
// "x" -> AccelTokenNormalE (ordinary char)
static AccelTokenE accelNextToken(const char **text, char *outCh) {
const char *p = *text;
if (*p == '\0') {
*outCh = 0;
return AccelTokenEndE;
}
if (*p == '&') {
char next = *(p + 1);
if (next == '&') {
*text = p + 2;
*outCh = '&';
return AccelTokenLiteralE;
}
if (next != '\0') {
*text = p + 2;
*outCh = next;
return AccelTokenAccelE;
}
// Trailing lone '&' -- consume it and report end of string
*text = p + 1;
*outCh = 0;
return AccelTokenEndE;
}
*text = p + 1;
*outCh = *p;
return AccelTokenNormalE;
}
// ============================================================
// clipRect
// ============================================================
@ -219,8 +262,9 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *
void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style) {
int32_t bw = style->width;
// Fill interior if requested
if (style->face != 0) {
// Fill interior unless the caller requested no fill. Tested against the
// sentinel (not 0) so a black face (packed 0) still fills correctly.
if (style->face != BEVEL_NO_FILL) {
rectFill(d, ops, x + bw, y + bw, w - bw * 2, h - bw * 2, style->face);
}
@ -673,7 +717,6 @@ void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
void drawInit(BlitOpsT *ops, const DisplayT *d) {
ops->bytesPerPixel = d->format.bytesPerPixel;
ops->pitch = d->pitch;
switch (d->format.bytesPerPixel) {
case 1:
@ -736,6 +779,13 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
if (x < clipX1) { colStart = clipX1 - x; }
if (x + w > clipX2) { colEnd = clipX2 - x; }
// The mask rows index sMaskBit[col], so a width above the table size
// would read past it. Today the only caller passes width 16, but
// clamp defensively to keep the signature's wider w safe.
if (colEnd > (int32_t)(sizeof(sMaskBit) / sizeof(sMaskBit[0]))) {
colEnd = (int32_t)(sizeof(sMaskBit) / sizeof(sMaskBit[0]));
}
if (colStart >= colEnd || rowStart >= rowEnd) {
return;
}
@ -788,6 +838,105 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
}
// Draws a 1px rectangle outline (top/bottom HLine, left/right VLine) in a
// single color. Factors the repeated 4-line outline idiom.
void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) {
drawHLine(d, ops, x, y, w, color);
drawHLine(d, ops, x, y + h - 1, w, color);
drawVLine(d, ops, x, y, h, color);
drawVLine(d, ops, x + w - 1, y, h, color);
}
// Axis-agnostic scrollbar painter shared by the window manager and the
// widget toolkit. Lays down a sunken trough bevel, an arrow button at
// each end, and a raised thumb at (barW + thumbPos) along the track.
//
// The layering order matters: the trough bevel fills and frames the whole
// track first, then arrow button bevels + glyphs paint over the trough
// ends, and finally the thumb bevel sits in the middle. A thumbSize of
// 0 (or less) suppresses the thumb, used when the range is too small or
// the track is too short to display one.
void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize) {
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
if (orient == ScrollbarVerticalE) {
// Sunken trough
drawBevel(d, ops, x, y, barW, length, &troughBevel);
// Up arrow button
drawBevel(d, ops, x, y, barW, barW, &btnBevel);
drawScrollbarArrow(d, ops, colors, x, y, barW, ScrollArrowUpE);
// Down arrow button
int32_t downY = y + length - barW;
drawBevel(d, ops, x, downY, barW, barW, &btnBevel);
drawScrollbarArrow(d, ops, colors, x, downY, barW, ScrollArrowDownE);
// Thumb
if (thumbSize > 0) {
drawBevel(d, ops, x, y + barW + thumbPos, barW, thumbSize, &btnBevel);
}
} else {
// Sunken trough
drawBevel(d, ops, x, y, length, barW, &troughBevel);
// Left arrow button
drawBevel(d, ops, x, y, barW, barW, &btnBevel);
drawScrollbarArrow(d, ops, colors, x, y, barW, ScrollArrowLeftE);
// Right arrow button
int32_t rightX = x + length - barW;
drawBevel(d, ops, rightX, y, barW, barW, &btnBevel);
drawScrollbarArrow(d, ops, colors, rightX, y, barW, ScrollArrowRightE);
// Thumb
if (thumbSize > 0) {
drawBevel(d, ops, x + barW + thumbPos, y, thumbSize, barW, &btnBevel);
}
}
}
// Draws a small triangle arrow glyph inside a scrollbar button.
//
// The triangle is built row-by-row from the tip outward: each successive
// 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,
// which fits well inside SCROLLBAR_WIDTH (16px) buttons. The glyph is
// centered on the button using integer division -- no sub-pixel alignment
// needed at these sizes.
void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir) {
int32_t cx = x + size / 2;
int32_t cy = y + size / 2;
uint32_t fg = colors->scrollbarFg;
// Draw a small triangle
for (int32_t i = 0; i < SB_ARROW_ROWS; i++) {
switch (dir) {
case ScrollArrowUpE:
drawHLine(d, ops, cx - i, cy - SB_ARROW_HALF + i, 1 + i * 2, fg);
break;
case ScrollArrowDownE:
drawHLine(d, ops, cx - i, cy + SB_ARROW_HALF - i, 1 + i * 2, fg);
break;
case ScrollArrowLeftE:
drawVLine(d, ops, cx - SB_ARROW_HALF + i, cy - i, 1 + i * 2, fg);
break;
case ScrollArrowRightE:
drawVLine(d, ops, cx + SB_ARROW_HALF - i, cy - i, 1 + i * 2, fg);
break;
}
}
}
// ============================================================
// drawTermRow
// ============================================================
@ -1001,46 +1150,23 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in
int32_t cw = font->charWidth;
int32_t clipX2 = d->clipX + d->clipW;
while (*text) {
char ch;
AccelTokenE token;
while ((token = accelNextToken(&text, &ch)) != AccelTokenEndE) {
if (__builtin_expect(x >= clipX2, 0)) {
break;
}
if (*text == '&') {
text++;
if (*text == '&') {
// Escaped && -- draw literal &
if (x + cw > d->clipX) {
drawChar(d, ops, font, x, y, '&', fg, bg, opaque);
}
drawChar(d, ops, font, x, y, ch, fg, bg, opaque);
x += cw;
text++;
continue;
}
if (*text) {
// Accelerator character -- draw it then underline
if (x + cw > d->clipX) {
drawChar(d, ops, font, x, y, *text, fg, bg, opaque);
if (token == AccelTokenAccelE) {
drawHLine(d, ops, x, y + font->charHeight - 1, cw, fg);
}
x += cw;
text++;
continue;
}
break;
}
if (x + cw > d->clipX) {
drawChar(d, ops, font, x, y, *text, fg, bg, opaque);
}
x += cw;
text++;
}
}
@ -1224,14 +1350,12 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
// different scanline. Instead we advance by d->pitch per pixel
// and write directly, branching on bpp once at the top.
//
// The ops parameter is unused (suppressed with (void)ops) because
// spanFill operates on contiguous horizontal runs and is useless
// for vertical lines. We keep the parameter for API consistency
// with the rest of the draw layer.
// spanFill operates on contiguous horizontal runs and is useless for
// vertical lines, so the ops vtable's span pointers are not used here.
// The bytesPerPixel field is still read from ops to match the single
// source-of-truth convention used by the rest of the draw layer.
void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color) {
(void)ops;
// Inline single-pixel-wide fill to avoid rectFill overhead for narrow lines
if (__builtin_expect(x < d->clipX || x >= d->clipX + d->clipW, 0)) {
return;
@ -1243,7 +1367,7 @@ void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h
if (y2 > d->clipY + d->clipH) { y2 = d->clipY + d->clipH; }
if (y1 >= y2) { return; }
int32_t bpp = d->format.bytesPerPixel;
int32_t bpp = ops->bytesPerPixel;
uint8_t *dst = d->backBuf + y1 * d->pitch + x * bpp;
int32_t pitch = d->pitch;
@ -1377,7 +1501,7 @@ void rectCopyGrayscale(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t d
uint32_t r8 = pal[idx * 3 + 0];
uint32_t g8 = pal[idx * 3 + 1];
uint32_t b8 = pal[idx * 3 + 2];
uint32_t gray = (r8 * 77 + g8 * 150 + b8 * 29) >> 8;
uint32_t gray = (r8 * GRAY_WEIGHT_R + g8 * GRAY_WEIGHT_G + b8 * GRAY_WEIGHT_B) >> GRAY_WEIGHT_SHIFT;
*dst++ = dvxNearestPalEntry(pal, (uint8_t)gray, (uint8_t)gray, (uint8_t)gray);
}
}
@ -1389,9 +1513,9 @@ void rectCopyGrayscale(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t d
int32_t rBits = d->format.redBits;
int32_t gBits = d->format.greenBits;
int32_t bBits = d->format.blueBits;
uint32_t rMask = ((1 << rBits) - 1) << rShift;
uint32_t gMask = ((1 << gBits) - 1) << gShift;
uint32_t bMask = ((1 << bBits) - 1) << bShift;
uint32_t rMask = d->format.redMask;
uint32_t gMask = d->format.greenMask;
uint32_t bMask = d->format.blueMask;
for (int32_t row = 0; row < h; row++) {
const uint8_t *src = srcBuf + (srcY + row) * srcPitch + srcX * bpp;
@ -1409,7 +1533,7 @@ void rectCopyGrayscale(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t d
uint32_t r8 = ((px & rMask) >> rShift) << (8 - rBits);
uint32_t g8 = ((px & gMask) >> gShift) << (8 - gBits);
uint32_t b8 = ((px & bMask) >> bShift) << (8 - bBits);
uint32_t gray = (r8 * 77 + g8 * 150 + b8 * 29) >> 8;
uint32_t gray = (r8 * GRAY_WEIGHT_R + g8 * GRAY_WEIGHT_G + b8 * GRAY_WEIGHT_B) >> GRAY_WEIGHT_SHIFT;
uint32_t grayPx = ((gray >> (8 - rBits)) << rShift) |
((gray >> (8 - gBits)) << gShift) |
@ -1564,29 +1688,10 @@ int32_t textWidth(const BitmapFontT *font, const char *text) {
int32_t textWidthAccel(const BitmapFontT *font, const char *text) {
int32_t w = 0;
while (*text) {
if (*text == '&') {
text++;
char ch;
if (*text == '&') {
// Escaped && -- counts as one character
while (accelNextToken(&text, &ch) != AccelTokenEndE) {
w += font->charWidth;
text++;
continue;
}
if (*text) {
// Accelerator character -- counts as one character, & is skipped
w += font->charWidth;
text++;
continue;
}
break;
}
w += font->charWidth;
text++;
}
return w;

View file

@ -134,6 +134,21 @@ void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
// alone. Used by buttons, checkboxes, toggle bars, etc.
void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors);
// Draw a 1px rectangle outline (4 edges) in a single color. Issues a
// top/bottom drawHLine and left/right drawVLine, clipped like the underlying
// primitives. Replaces the repeated 4-line outline idiom used for tooltips,
// debug borders, and title-bar gadget icons.
void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);
// Paint a complete scrollbar: sunken trough bevel, an arrow button at each
// end, and a raised thumb at (barW + thumbPos) along the track. orient
// selects the axis; thumbSize <= 0 suppresses the thumb entirely.
void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize);
// Draw a small triangular arrow glyph centered in a scrollbar button.
// dir selects the direction (up/down/left/right).
void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir);
// Horizontal line (1px tall rectangle fill, but with simpler clipping).
void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);

View file

@ -51,7 +51,7 @@
// 256 glyphs, 16 bytes per glyph, MSB = leftmost pixel
// ============================================================
static const uint8_t font8x16[256 * 16] = {
static const uint8_t font8x16[256 * FONT_CHAR_HEIGHT] = {
// Char 0: NULL (blank)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Char 1: Smiley (outline)
@ -573,8 +573,8 @@ static const uint8_t font8x16[256 * 16] = {
// ============================================================
static const BitmapFontT dvxFont8x16 = {
.charWidth = 8,
.charHeight = 16,
.charWidth = FONT_CHAR_WIDTH,
.charHeight = FONT_CHAR_HEIGHT,
.firstChar = 0,
.numChars = 256,
.glyphData = font8x16,

View file

@ -20,7 +20,7 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// dvxIcon.c -- stb_image implementation for DVX GUI
// dvxImage.c -- stb_image implementation for DVX GUI
//
// This file exists solely to instantiate the stb_image implementation.
// stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION
@ -52,6 +52,3 @@
#include "thirdparty/stb_image_wrap.h"
#pragma GCC diagnostic pop
#include <stdlib.h>
#include "dvxMem.h"

View file

@ -85,45 +85,45 @@ static inline void dvxGeneratePalette(uint8_t *pal)
}
// Entries 232-239: UI chrome colors
// 232: highlight (white)
pal[232 * 3 + 0] = 255;
pal[232 * 3 + 1] = 255;
pal[232 * 3 + 2] = 255;
// highlight (white)
pal[PAL_CHROME_HIGHLIGHT * 3 + 0] = 255;
pal[PAL_CHROME_HIGHLIGHT * 3 + 1] = 255;
pal[PAL_CHROME_HIGHLIGHT * 3 + 2] = 255;
// 233: shadow (dark grey)
pal[233 * 3 + 0] = 80;
pal[233 * 3 + 1] = 80;
pal[233 * 3 + 2] = 80;
// shadow (dark grey)
pal[PAL_CHROME_SHADOW * 3 + 0] = 80;
pal[PAL_CHROME_SHADOW * 3 + 1] = 80;
pal[PAL_CHROME_SHADOW * 3 + 2] = 80;
// 234: active title bg (navy blue)
pal[234 * 3 + 0] = 0;
pal[234 * 3 + 1] = 0;
pal[234 * 3 + 2] = 128;
// active title bg (navy blue)
pal[PAL_CHROME_ACTIVE_BG * 3 + 0] = 0;
pal[PAL_CHROME_ACTIVE_BG * 3 + 1] = 0;
pal[PAL_CHROME_ACTIVE_BG * 3 + 2] = 128;
// 235: inactive title bg (grey)
pal[235 * 3 + 0] = 128;
pal[235 * 3 + 1] = 128;
pal[235 * 3 + 2] = 128;
// inactive title bg (grey)
pal[PAL_CHROME_INACTIVE_BG * 3 + 0] = 128;
pal[PAL_CHROME_INACTIVE_BG * 3 + 1] = 128;
pal[PAL_CHROME_INACTIVE_BG * 3 + 2] = 128;
// 236: desktop (steel blue)
pal[236 * 3 + 0] = 51;
pal[236 * 3 + 1] = 102;
pal[236 * 3 + 2] = 153;
// desktop (steel blue)
pal[PAL_CHROME_DESKTOP * 3 + 0] = 51;
pal[PAL_CHROME_DESKTOP * 3 + 1] = 102;
pal[PAL_CHROME_DESKTOP * 3 + 2] = 153;
// 237: selection (navy)
pal[237 * 3 + 0] = 0;
pal[237 * 3 + 1] = 0;
pal[237 * 3 + 2] = 128;
// selection (navy)
pal[PAL_CHROME_SELECTION * 3 + 0] = 0;
pal[PAL_CHROME_SELECTION * 3 + 1] = 0;
pal[PAL_CHROME_SELECTION * 3 + 2] = 128;
// 238: text (black)
pal[238 * 3 + 0] = 0;
pal[238 * 3 + 1] = 0;
pal[238 * 3 + 2] = 0;
// text (black)
pal[PAL_CHROME_TEXT * 3 + 0] = 0;
pal[PAL_CHROME_TEXT * 3 + 1] = 0;
pal[PAL_CHROME_TEXT * 3 + 2] = 0;
// 239: bright white
pal[239 * 3 + 0] = 255;
pal[239 * 3 + 1] = 255;
pal[239 * 3 + 2] = 255;
// bright white
pal[PAL_CHROME_WHITE * 3 + 0] = 255;
pal[PAL_CHROME_WHITE * 3 + 1] = 255;
pal[PAL_CHROME_WHITE * 3 + 2] = 255;
// Entries 240-255: reserved (black)
for (int32_t i = 240; i < 256; i++) {

View file

@ -56,6 +56,11 @@ struct PrefsHandleT {
char *filePath; // path used by prefsSave
};
// Maximum physical INI line length, including the NUL. Lines longer than
// this are read in fragments by fgets; the remainder is discarded so an
// over-long value cannot become bogus entries.
#define PREFS_LINE_MAX 512
// ============================================================
// Prototypes
@ -243,10 +248,25 @@ PrefsHandleT *prefsLoad(const char *filename) {
return h;
}
char line[512];
char line[PREFS_LINE_MAX];
char *currentSection = dupStr("");
while (fgets(line, sizeof(line), fp)) {
// Detect a physical line longer than the buffer (fgets returns it in
// fragments) and discard the remainder, so an over-long value can't
// be re-parsed as bogus key=value / comment entries. Capture this
// before dvxTrimRight strips the trailing newline.
size_t lineLen = strlen(line);
bool truncated = (lineLen == sizeof(line) - 1 && line[lineLen - 1] != '\n');
if (truncated) {
int ch;
while ((ch = fgetc(fp)) != '\n' && ch != EOF) {
// discard the remainder of the over-long physical line
}
}
dvxTrimRight(line);
char *p = (char *)dvxSkipWs(line);
@ -376,6 +396,13 @@ void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const
return;
}
// Normalize NULL to empty so every stored value is a valid string:
// otherwise prefsGetString returns NULL instead of the default and
// prefsSaveAs silently drops the key.
if (!value) {
value = "";
}
int32_t idx = findEntry(h, section, key);
if (idx >= 0) {
@ -405,11 +432,6 @@ void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const
while (insertAt < arrlen(h->entries)) {
PrefsEntryT *e = &h->entries[insertAt];
if (!e->key && !e->value && e->section &&
strcmpci(e->section, section) != 0) {
break;
}
if (e->section && strcmpci(e->section, section) != 0) {
break;
}

View file

@ -77,6 +77,15 @@ DvxResHandleT *dvxResOpen(const char *path) {
return NULL;
}
// Measure the file so the directory size read from the (possibly
// corrupt/hostile) footer can be bounds-checked before allocating.
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
int64_t fileSize = ftell(f);
// Read footer from end of file
if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) {
fclose(f);
@ -95,6 +104,17 @@ DvxResHandleT *dvxResOpen(const char *path) {
return NULL;
}
// Reject an entryCount that can't fit in the file: this both stops a
// size_t overflow in the malloc below (entryCount * 48 wraps on a
// 32-bit target) and prevents the fread from overrunning a tiny
// allocation when the footer is corrupt.
if (fileSize < 0 ||
(int64_t)footer.dirOffset > fileSize ||
(int64_t)footer.entryCount > fileSize / (int64_t)sizeof(DvxResDirEntryT)) {
fclose(f);
return NULL;
}
// Read directory
if (fseek(f, footer.dirOffset, SEEK_SET) != 0) {
fclose(f);
@ -114,6 +134,13 @@ DvxResHandleT *dvxResOpen(const char *path) {
return NULL;
}
// Force-terminate names read from the file before any strcmp (qsort /
// bsearch); a corrupt unterminated name would otherwise read past the
// field, and past the allocation for the last entry.
for (uint32_t i = 0; i < footer.entryCount; i++) {
entries[i].name[DVX_RES_NAME_MAX - 1] = '\0';
}
fclose(f);
DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT));
@ -190,7 +217,12 @@ int32_t dvxResRemove(const char *path, const char *name) {
DvxResDirEntryT *entries = NULL;
uint32_t count = 0;
uint8_t **data = NULL;
dvxResReadExisting(path, dxeSize, &entries, &count, &data);
int readResult = dvxResReadExisting(path, dxeSize, &entries, &count, &data);
// Distinguish a read/OOM error from a file with no resources to remove.
if (readResult < 0) {
return -1;
}
if (!entries || count == 0) {
return -1;

View file

@ -32,6 +32,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
// ============================================================
// ============================================================
@ -125,9 +126,9 @@ typedef struct {
// through these two functions, so even a small speedup here multiplies
// across the entire frame.
//
// Storing bytesPerPixel and pitch alongside the function pointers keeps
// the hot data in one cache line and avoids re-dereferencing the DisplayT
// on every scanline of a fill or copy.
// Storing bytesPerPixel alongside the function pointers keeps the hot data
// in one cache line and avoids re-dereferencing the DisplayT on every
// scanline of a fill or copy.
typedef void (*SpanFillFnT)(uint8_t *dst, uint32_t color, int32_t count);
typedef void (*SpanCopyFnT)(uint8_t *dst, const uint8_t *src, int32_t count);
@ -136,7 +137,6 @@ typedef struct {
SpanFillFnT spanFill;
SpanCopyFnT spanCopy;
int32_t bytesPerPixel;
int32_t pitch;
} BlitOpsT;
// ============================================================
@ -153,10 +153,16 @@ typedef struct {
// face = 0 means "don't fill the interior", which is used when drawing
// a bevel frame around content that's already been painted.
// Sentinel face value meaning "draw the border only, no interior fill".
// A plain 0 cannot be used because packed pure black (RGB 0,0,0) is also 0,
// which would wrongly suppress the fill of a black-faced widget. 0xFFFFFFFF
// is never produced by any DVX pixel format (XRGB, no 32-bit alpha).
#define BEVEL_NO_FILL 0xFFFFFFFFu
typedef struct {
uint32_t highlight; // lighter color (top/left edges)
uint32_t shadow; // darker color (bottom/right edges)
uint32_t face; // fill color for interior (0 = no fill)
uint32_t face; // fill color for interior (BEVEL_NO_FILL = no fill)
int32_t width; // border thickness in pixels (typically 2)
} BevelStyleT;
@ -165,6 +171,7 @@ typedef struct {
#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_SB_BUTTON(cs) (BevelStyleT){ (cs)->windowHighlight, (cs)->windowShadow, (cs)->scrollbarBg, 1 }
#define BEVEL_POPUP(cs, bw) (BevelStyleT){ (cs)->windowHighlight, (cs)->windowShadow, (cs)->menuBg, (bw) }
// ============================================================
// Bitmap font
@ -183,6 +190,7 @@ typedef struct {
// boundaries, enabling per-scanline glyph rendering without bit shifting.
#define FONT_CHAR_WIDTH 8 // fixed glyph width in pixels
#define FONT_CHAR_HEIGHT 16 // fixed glyph height in pixels
typedef struct {
int32_t charWidth; // fixed width per glyph (e.g. 8)
@ -257,6 +265,8 @@ typedef enum {
ColorCountE
} ColorIdE;
_Static_assert(sizeof(ColorSchemeT) == (size_t)ColorCountE * sizeof(uint32_t), "ColorSchemeT field count must match ColorIdE with no padding");
typedef enum {
WallpaperStretchE,
WallpaperTileE,
@ -274,12 +284,13 @@ typedef struct {
// Dirty rectangle list
// ============================================================
//
// Fixed-size array rather than a linked list because: (a) allocation
// in a DOS protected-mode environment should be predictable, (b) 128
// rects is more than enough for any realistic frame (window moves,
// menu opens, etc.), and (c) linear scanning for merge candidates is
// cache-friendly at this size. If the list fills up, the compositor
// can merge aggressively or fall back to a full-screen repaint.
// Dynamic array of dirty rects (realloc'd on demand) rather than a linked
// list because: (a) the buffer is pre-allocated once to a soft cap so the
// per-frame add path never reallocs, (b) the rects sit contiguously, making
// the linear merge scan cache-friendly, and (c) reordering/merging works in
// place. When the count crosses the soft cap the array doubles; the
// compositor can also merge aggressively or fall back to a full-screen
// repaint.
typedef struct {
RectT *rects; // dynamic array (realloc'd on demand)
@ -310,10 +321,11 @@ typedef struct {
// stack as a uniform vertical rhythm. If the two ever need to differ,
// split these back into independent values.
#define CHROME_MENU_HEIGHT CHROME_TITLE_HEIGHT
// Y offset from window top to the bottom of the title bar (= top of menu bar)
#define CHROME_TITLEBAR_BOTTOM (CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT)
#define CHROME_TOTAL_TOP (CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_INNER_BORDER)
#define CHROME_TOTAL_SIDE (CHROME_BORDER_WIDTH + CHROME_INNER_BORDER)
#define CHROME_TOTAL_BOTTOM (CHROME_BORDER_WIDTH + CHROME_INNER_BORDER)
#define CHROME_CLOSE_BTN_SIZE 16
// ============================================================
// Menu types
@ -367,7 +379,7 @@ struct MenuT {
// menu bar is actually drawn, avoiding redundant work when multiple
// menus are added in sequence during window setup.
typedef struct {
MenuT *menus; // dynamic array (realloc'd on demand)
MenuT **menus; // dynamic array of owned MenuT pointers (realloc'd on demand)
int32_t menuCount;
int32_t menuCap;
int32_t activeIdx; // menu bar item with open popup (-1 = none)
@ -391,7 +403,19 @@ typedef enum {
ScrollbarHorizontalE
} ScrollbarOrientE;
// Arrow glyph direction for the scrollbar painter. Values 0..3 must
// preserve the ordering the draw-layer arrow helper switches on (up,
// down, left, right).
typedef enum {
ScrollArrowUpE,
ScrollArrowDownE,
ScrollArrowLeftE,
ScrollArrowRightE
} ScrollArrowDirE;
#define SCROLLBAR_WIDTH 16
#define SB_ARROW_ROWS 4 // number of rows in scrollbar arrow glyph
#define SB_ARROW_HALF 2 // half-width of scrollbar arrow tip
typedef struct {
ScrollbarOrientE orient;
@ -453,11 +477,14 @@ typedef struct {
// ASCII control / printable ranges (non-extended codes).
#define KEY_ESCAPE 0x1B
#define KEY_ASCII_ENTER 0x0D // Enter/Return (carriage return)
#define KEY_ASCII_DEL 0x7F // not the same as KEY_DELETE (scancode 0x53)
#define KEY_BACKSPACE 0x08 // ASCII backspace (not KEY_DELETE scancode)
#define KEY_ASCII_PRINT_FIRST 0x20 // space
#define KEY_ASCII_PRINT_LAST 0x7E // tilde
// Ctrl-letter codes (A=1, B=2, ..., Z=26) used for clipboard shortcuts.
#define KEY_CTRL_A 0x01
#define KEY_CTRL_C 0x03
#define KEY_CTRL_V 0x16
#define KEY_CTRL_X 0x18
@ -507,6 +534,7 @@ typedef struct {
#define MIN_WINDOW_W 80
#define MIN_WINDOW_H 60
#define WM_MAX_FROM_SCREEN (-1) // use screen dimension as max (for maxW/maxH)
#define DRAG_MIN_VISIBLE 50 // min pixels of window kept on-screen by move/resize
// Default / minimum VESA video mode. Used as the lower bound when
// enumerating available modes and as the fallback mode when the
@ -561,6 +589,7 @@ typedef struct WindowT {
bool maximized;
bool resizable;
bool modal;
bool destroyPending; // hidden and queued for deferred destruction
bool iconNeedsRefresh; // true when contentBuf has changed since last icon refresh
// Paint state: PAINT_NONE = idle, PAINT_PARTIAL = only dirty widgets,
// PAINT_FULL = clear background + relayout + repaint all.

View file

@ -155,9 +155,9 @@ void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint
return;
}
uint32_t rv = (color >> d->format.redShift) & ((1u << d->format.redBits) - 1);
uint32_t gv = (color >> d->format.greenShift) & ((1u << d->format.greenBits) - 1);
uint32_t bv = (color >> d->format.blueShift) & ((1u << d->format.blueBits) - 1);
uint32_t rv = (color & d->format.redMask) >> d->format.redShift;
uint32_t gv = (color & d->format.greenMask) >> d->format.greenShift;
uint32_t bv = (color & d->format.blueMask) >> d->format.blueShift;
*r = (uint8_t)(rv << (8 - d->format.redBits));
*g = (uint8_t)(gv << (8 - d->format.greenBits));

View file

@ -156,7 +156,6 @@ typedef enum {
//
// Flags encode static properties checked by the framework:
// FOCUSABLE -- can receive keyboard focus (Tab navigation)
// BOX_CONTAINER -- uses the generic VBox/HBox layout algorithm
// HORIZ_CONTAINER -- lays out children horizontally (vs. vertical)
// PAINTS_CHILDREN -- widget handles child rendering itself
// NO_HIT_RECURSE -- hit testing stops here, no child recursion
@ -201,7 +200,8 @@ typedef enum {
#define WGT_METHOD_POLL 18 // void (w, win)
#define WGT_METHOD_QUICK_REPAINT 19 // int32_t (w, outY, outH)
#define WGT_METHOD_SCROLL_CHILD_INTO_VIEW 20 // void (parent, child)
#define WGT_METHOD_COUNT 21
#define WGT_METHOD_ON_BLUR 21 // void (w) -- widget lost keyboard focus
#define WGT_METHOD_COUNT 22
#define WGT_METHOD_MAX 32 // room for future methods
#define WGT_CLASS_VERSION 1 // bump on breaking ABI change
@ -367,6 +367,16 @@ static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (fn) { fn(w, key, mod); }
}
// Dispatched on the widget LOSING keyboard focus, before the app-level
// onBlur callback. Editing widgets (e.g. the spinner) commit/clamp their
// in-progress text here. Must be called from every focus-loss transition
// (mouse focus change, Tab/accel navigation, window blur, wgtSetFocused).
static inline void wclsOnBlur(WidgetT *w) {
typedef void (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_BLUR] : NULL;
if (fn) { fn(w); }
}
static inline void wclsOnAccelActivate(WidgetT *w, WidgetT *root) {
typedef void (*FnT)(WidgetT *, WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_ACCEL_ACTIVATE] : NULL;
@ -479,6 +489,15 @@ static inline void wclsScrollChildIntoView(WidgetT *parent, const WidgetT *child
// root container. The window's userData is set to the AppContextT pointer.
WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win);
// Default window focus handlers installed by wgtInitWindow into
// win->onBlur / win->onFocus. widgetOnBlur clears the widget focus and
// fires the focused widget's onBlur (LostFocus + data write-back);
// widgetOnFocus marks the window's minimized-icon thumbnail dirty. Apps
// that replace win->onBlur/onFocus must call these to keep the widget
// focus bridge working.
void widgetOnBlur(WindowT *win);
void widgetOnFocus(WindowT *win);
// ============================================================
// Operations
// ============================================================
@ -526,6 +545,11 @@ WidgetT *wgtFind(WidgetT *root, const char *name);
// Destroy a widget and all its children (removes from parent)
void wgtDestroy(WidgetT *w);
// Destroy all children of w (w itself stays). Each child is fully
// destroyed (fires onChildChanged, clears global refs), so use this
// when rebuilding a container's contents to avoid leaking old subtrees.
void wgtDestroyChildren(WidgetT *w);
// Default window resize handler installed by wgtInitWindow. Re-evaluates
// scrollbars and relayouts the widget tree. Call this from custom onResize
// handlers to chain to the widget system before firing application events.
@ -680,6 +704,7 @@ typedef enum {
// Widget interface descriptor (registered by each .wgt)
typedef struct {
// Fields are ordered by alignment to minimize struct padding.
const char *basName; // VB-style name (e.g. "CommandButton"), or NULL
const WgtPropDescT *props; // type-specific properties
int32_t propCount;
@ -687,11 +712,11 @@ typedef struct {
int32_t methodCount;
const WgtEventDescT *events; // extra events beyond common set
int32_t eventCount;
uint8_t createSig; // WgtCreateSigE: how to call create fn
int32_t createArgs[WGT_MAX_CREATE_ARGS]; // default numeric args
bool isContainer; // can hold child widgets
const char *defaultEvent; // default event name (e.g. "Click")
const char *namePrefix; // auto-name prefix (NULL = use basName)
uint8_t createSig; // WgtCreateSigE: how to call create fn
bool isContainer; // can hold child widgets
} WgtIfaceT;
// Register/retrieve interface descriptors by widget type name.

View file

@ -64,16 +64,7 @@ extern const WidgetClassT **widgetClassTable;
#define VALIDATE_WIDGET_VOID(w, wtype) \
do { if (!(w) || (w)->type != (wtype)) { return; } } while (0)
// ============================================================
// Cursor shape IDs (mirrors dvxCursor.h defines)
// ============================================================
#define CURSOR_ARROW 0
#define CURSOR_RESIZE_H 1
#define CURSOR_RESIZE_V 2
#define CURSOR_RESIZE_DIAG_NWSE 3
#define CURSOR_RESIZE_DIAG_NESW 4
#define CURSOR_BUSY 5
// Cursor shape IDs (CURSOR_*) come from dvxCur.h via dvxApp.h.
// ============================================================
// Keyboard modifier flags
@ -126,9 +117,14 @@ extern WidgetT *sFocusedWidget;
extern WidgetT *sKeyPressedBtn;
extern WidgetT *sOpenPopup;
extern WidgetT *sDragWidget; // widget being dragged (any drag type)
extern int32_t sPollWidgetCount;
extern WidgetT **sPollWidgets; // stb_ds dynamic array
// Incremented every time a widget is destroyed. Dispatch code snapshots
// it before invoking a user callback; a changed value afterwards means
// the callback destroyed widgets, so cached WidgetT pointers (the hit
// widget, the focused widget) may be dangling and must not be touched.
extern uint32_t sWidgetGen;
extern void (*sCursorBlinkFn)(void);
extern void (*sWidgetDestroyFn)(WidgetT *w);
// ============================================================
// Core widget functions (widgetCore.c)
@ -139,6 +135,10 @@ void widgetAddChild(WidgetT *parent, WidgetT *child);
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
void widgetDestroyChildren(WidgetT *w);
// Clears every global interaction pointer that references w and removes w
// from the poll list. Call for every destroyed node.
void widgetClearReferences(WidgetT *w);
// Allocation
WidgetT *widgetAlloc(WidgetT *parent, int32_t type);
@ -170,7 +170,6 @@ int32_t multiClickDetect(int32_t vx, int32_t vy);
// Clipboard
void clipboardCopy(const char *text, int32_t len);
const char *clipboardGet(int32_t *outLen);
int32_t clipboardMaxLen(void);
// Hit testing
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
@ -211,8 +210,9 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
// ============================================================
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
void widgetOnBlur(WindowT *win);
void widgetOnFocus(WindowT *win);
// widgetOnBlur / widgetOnFocus are the default window focus handlers
// installed by wgtInitWindow; declared in dvxWgt.h so apps that override
// win->onBlur/onFocus can chain them.
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod);
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);

File diff suppressed because it is too large Load diff

View file

@ -51,8 +51,9 @@ void wmInit(WindowStackT *stack);
WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable);
// Free the window's content buffer and all attached resources (menu bar,
// scrollbars, widget tree), then remove it from the stack and dirty the
// region it occupied.
// scrollbars, widget tree), then remove it from the stack. Does not dirty
// the vacated region -- the caller is responsible for marking it for
// repaint before calling (the window geometry is read here for cleanup).
void wmDestroyWindow(WindowStackT *stack, WindowT *win);
// Move window at stack index idx to the top of the Z-order. Dirties both
@ -81,8 +82,10 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d);
// menu bar per window is supported.
MenuBarT *wmAddMenuBar(WindowT *win);
// Free the menu bar and reclaim the content area.
void wmDestroyMenuBar(WindowT *win);
// Free the menu bar and reclaim the content area. The content buffer is
// reallocated to the grown content rect; pass d as NULL only when the
// window is being destroyed and the buffer is already freed.
void wmDestroyMenuBar(WindowT *win, const DisplayT *d);
// Get the minimum window size (accounts for chrome, gadgets, and menu bar).
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH);
@ -113,12 +116,25 @@ bool wmRemoveMenuItem(MenuT *menu, int32_t id);
// Remove all items from a menu (preserves the menu itself on the menu bar).
void wmClearMenuItems(MenuT *menu);
// Recursively search a single menu (and all of its submenus, to any
// depth) for an item with the given command ID. Returns the item, or
// NULL if not found. Unlike the menu-bar lookups below, this takes a
// standalone MenuT so it works for popup/context menus not attached to
// any menu bar.
MenuItemT *wmMenuFindItemInMenu(MenuT *menu, int32_t id);
// Query or set the checked state of a menu item by command ID.
// Searches all menus in the menu bar. For radio items, setting
// checked=true also unchecks other radio items in the same group.
bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id);
void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked);
// Set the checked state of an item within a standalone menu (a popup or
// context menu with no menu bar). Mirrors wmMenuItemSetChecked's radio
// behavior: setting a radio item checked unchecks its group siblings.
// Searches submenus recursively.
void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked);
// Enable or disable a menu item by command ID.
void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled);
@ -176,8 +192,9 @@ int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my);
// drag start) is applied so the window tracks the mouse smoothly.
void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mouseY, int32_t screenW, int32_t screenH);
// Update window dimensions during an active resize. Enforces MIN_WINDOW_W/H
// and optional maxW/maxH constraints. Reallocates the content buffer if the
// Update window dimensions during an active resize. Enforces the dynamic
// wmMinWindowSize minimum, optional maxW/maxH constraints, and keeps the
// window reachable on-screen. Reallocates the content buffer if the
// content area size changed, then calls onResize to notify the application.
// mouseX/mouseY are in/out: on return they hold the clamped position so the
// caller can warp the hardware cursor to keep it stuck to the window edge.
@ -194,6 +211,10 @@ void wmDragEnd(WindowStackT *stack);
// and the initial mouse position for delta computation.
void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY);
// Apply a completed geometry change: recompute content rect, reallocate the
// content buffer, fire onResize, and request a PAINT_FULL repaint.
void wmResizeCommit(WindowT *win, const DisplayT *d);
// End the current resize operation. Clears resizeWindow state.
void wmResizeEnd(WindowStackT *stack);

View file

@ -188,7 +188,7 @@ void platformMouseWarp(int32_t x, int32_t y);
// Return the current modifier key state in BIOS shift-state format:
// bits 0-1 = either shift, bit 2 = ctrl, bit 3 = alt. On DOS this
// reads the BIOS data area at 0040:0017.
// uses INT 16h AH=12h (get extended shift status) and returns the low byte.
int32_t platformKeyboardGetModifiers(void);
// Non-blocking read of the next key from the keyboard buffer. Returns

View file

@ -98,11 +98,60 @@
// Key-up detection ring buffer size
#define KEYUP_BUF_SIZE 16
// Worst-case byte span of int9Handler that must be locked into physical
// memory against DPMI paging. Matches the rs232 driver's ISR_SIZE; the
// handler is small but a generous span guarantees full coverage.
#define KEYUP_ISR_SIZE 2048
// PS/2 scancode prefix and fake-shift bytes. 0xE0 precedes a single
// extended-key scancode; 0xE1 precedes the two 2-byte halves of the
// Pause/Break burst (E1 1D 45 E1 9D C5), which carries no real break code.
// The grey navigation keys are bracketed by "fake shift" releases (0xAA /
// 0xB6 emitted right after an 0xE0) that must not be reported as genuine
// Shift key-ups, while a standalone 0xAA / 0xB6 (real Left / Right Shift
// release) must be.
#define PS2_SCAN_PREFIX_E0 0xE0
#define PS2_SCAN_PREFIX_E1 0xE1
#define PS2_SCAN_FAKE_SHIFT_LE 0xAA
#define PS2_SCAN_FAKE_SHIFT_RE 0xB6
// Number of scancodes to swallow after each 0xE1 prefix in the Pause burst.
#define PS2_E1_TAIL_BYTES 2
// Sentinel for sLfbLinearAddr/sLfbMappedSize meaning "no LFB currently mapped".
#define LFB_NOT_MAPPED 0
// VGA splash screen (mode 13h) constants
#define SPLASH_VGA_W 320
#define SPLASH_VGA_H 200
#define SPLASH_VGA_ADDR 0xA0000
// 8-bit palette: 256 entries of 3 bytes (RGB) each
#define PALETTE_ENTRIES 256
#define PALETTE_BYTES (PALETTE_ENTRIES * 3)
// Max VBE mode numbers we read from the BIOS mode list
#define MAX_VBE_MODES 256
// DOS drive numbers (1=A, 2=B, 3=C ...): enumerate fixed drives C: through Z:
#define FIRST_FIXED_DRIVE 3
#define MAX_DRIVE_NUMBER 26
// Mouse absolute-promotion thresholds (see the cursor-state block and
// platformMousePoll). A driver that ignores the range we set clamps absolute
// reads into a low-res "trap" box (~640x200 on ctmouse); an absolute X/Y at or
// below the trap dimension is indistinguishable from a trap and never promotes.
// The height (240) sits above the observed ~200 trap with a guard band yet far
// below an honoured report (up to screenH-1). Two consecutive out-of-trap
// polls are required: a hard-clamping driver cannot emit even one, so false
// promotion on real hardware is impossible while DOSBox costs at most a frame.
#define MOUSE_TRAP_BOX_W 640
#define MOUSE_TRAP_BOX_H 240
#define MOUSE_PROMOTE_CONSEC 2
// Mouse button bitmask for INT 33h function 03h (BL bits 0-2 = L/R/M).
#define MOUSE_BUTTON_MASK 0x07
// Per-app memory tracking header prepended to each allocation
typedef struct {
@ -129,6 +178,7 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int3
static dxe_symbol_table sDxeExportTable[];
static int32_t setVesaMode(uint16_t mode);
static void sysInfoAppend(const char *fmt, ...);
static void unmapLfb(DisplayT *d);
// ============================================================
@ -141,12 +191,41 @@ static void sysInfoAppend(const char *fmt, ...);
static bool sHasMouseWheel = false;
static int32_t sLastWheelDelta = 0;
// Mouse coordinate range set by functions 07h/08h in platformMouseInit.
// Position read directly from function 03h (absolute coordinates).
// Software-tracked mouse cursor. sMouseRangeW/H are the clamp bounds, set to
// the VESA resolution in platformMouseInit. Position comes from one of two
// schemes selected at RUNTIME (sMouseMode):
//
// MICKEY (default): integrate INT 33h function 0Bh relative deltas into
// sCurX/sCurY. This is the ONLY scheme that works on real hardware / 86Box:
// many DOS drivers (e.g. ctmouse) ignore the range functions 07h/08h in
// unrecognised VESA modes and clamp absolute position (fn 03h) to a ~640x200
// virtual screen, trapping the cursor in the upper-left of a high-res
// display. First fixed in commit 157d79f, silently reverted during the
// refactor in 163959a.
//
// ABSOLUTE: consume fn 03h position directly. Required for DOSBox, which
// honours fn 07h/08h but STARVES the fn 0Bh mickey counters when the host
// mouse is uncaptured -- so the mickey scheme leaves the cursor dead there.
//
// DO NOT default to ABSOLUTE and DO NOT consume fn 03h position unless the
// driver has PROVEN at runtime it honours the high-res range: platformMousePoll
// starts in MICKEY and promotes to ABSOLUTE only after fn 03h reports a
// position outside the low-res trap box (which a trapped driver can never do).
// fn 07h/08h are issued at init purely as bait for this detector -- a trapped
// driver ignores them (the whole premise), an honouring driver then reports
// full-range coordinates. Keep the promotion gate; removing it re-traps real
// hardware (the 163959a regression).
typedef enum {
MouseModeMickeyE = 0,
MouseModeAbsoluteE = 1
} MouseModeE;
static int32_t sMouseRangeW = 0;
static int32_t sMouseRangeH = 0;
static int32_t sCurX = 0;
static int32_t sCurY = 0;
static MouseModeE sMouseMode = MouseModeMickeyE;
static int32_t sPromoteCounter = 0;
// Alt+key scan code to ASCII lookup table (indexed by BIOS scan code).
// INT 16h returns these scan codes with ascii=0 for Alt+key combos.
@ -195,9 +274,19 @@ static int32_t sAppMemCap = 0;
// hardware keyboard IRQ) and read the scan code directly from port 0x60.
// Break codes have bit 7 set. We queue them in a small ring buffer that
// platformKeyUpRead drains.
static PlatformKeyEventT sKeyUpBuf[KEYUP_BUF_SIZE];
static volatile int32_t sKeyUpHead = 0;
static volatile int32_t sKeyUpTail = 0;
//
// All state the ISR touches lives in one struct so a single DPMI lock pins
// every byte contiguously; independent statics give no contiguity guarantee
// (a page fault inside the handler under a paging host would be fatal).
typedef struct {
PlatformKeyEventT buf[KEYUP_BUF_SIZE];
volatile int32_t head;
volatile int32_t tail;
volatile uint8_t scanPrefix; // pending 0xE0 prefix, or 0
volatile int32_t e1Pending; // scancodes left to swallow after 0xE1
} KeyUpStateT;
static KeyUpStateT sKeyUp = { 0 };
static _go32_dpmi_seginfo sOldInt9;
static _go32_dpmi_seginfo sNewInt9;
static bool sKeyUpInstalled = false;
@ -208,8 +297,21 @@ static jmp_buf *sPlatformCrashJmp = NULL;
static volatile int *sPlatformCrashSignal = NULL;
static PlatformLogFnT sPlatformLogFn = NULL;
// Linear address and size of the currently mapped physical LFB, saved by
// mapLfb so platformVideoFreeBuffers / platformVideoShutdown can unlock and
// free the DPMI physical mapping. This is the RAW linear address returned by
// __dpmi_physical_address_mapping, NOT d->lfb (which has
// __djgpp_conventional_base folded in). LFB_NOT_MAPPED means nothing mapped.
static uint32_t sLfbLinearAddr = LFB_NOT_MAPPED;
static uint32_t sLfbMappedSize = 0;
void *dvxCalloc(size_t nmemb, size_t size) {
// Guard against nmemb*size overflowing size_t (calloc contract).
if (size != 0 && nmemb > SIZE_MAX / size) {
return NULL;
}
size_t total = nmemb * size;
void *ptr = dvxMalloc(total);
@ -247,6 +349,13 @@ void dvxFree(void *ptr) {
void *dvxMalloc(size_t size) {
int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
// Guard the header addition against size_t wrap, which would otherwise
// hand back a non-NULL pointer over an undersized block.
if (size > SIZE_MAX - sizeof(DvxAllocHeaderT)) {
return NULL;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)malloc(sizeof(DvxAllocHeaderT) + size);
if (!hdr) {
@ -491,10 +600,10 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff;
uint16_t modes[256];
uint16_t modes[MAX_VBE_MODES];
int32_t modeCount = 0;
for (int32_t i = 0; i < 256; i++) {
for (int32_t i = 0; i < MAX_VBE_MODES; i++) {
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
if (mode == 0xFFFF) {
@ -700,14 +809,42 @@ static bool hasCpuid(void) {
static void int9Handler(void) {
uint8_t scan = inportb(0x60);
// Break code: bit 7 set and not the 0xE0 prefix
if ((scan & 0x80) && scan != 0xE0) {
int32_t next = (sKeyUpHead + 1) % KEYUP_BUF_SIZE;
// Pause/Break emits a fixed E1 1D 45 E1 9D C5 burst with no real break
// code. Swallow the two scancodes following each 0xE1 prefix so 0x9D /
// 0xC5 are never queued as phantom key-ups.
if (sKeyUp.e1Pending > 0) {
sKeyUp.e1Pending--;
return;
}
if (next != sKeyUpTail) {
sKeyUpBuf[sKeyUpHead].scancode = scan & 0x7F;
sKeyUpBuf[sKeyUpHead].ascii = 0;
sKeyUpHead = next;
if (scan == PS2_SCAN_PREFIX_E1) {
sKeyUp.e1Pending = PS2_E1_TAIL_BYTES;
return;
}
// 0xE0 marks the following byte as an extended-key scancode.
if (scan == PS2_SCAN_PREFIX_E0) {
sKeyUp.scanPrefix = PS2_SCAN_PREFIX_E0;
return;
}
bool extended = (sKeyUp.scanPrefix == PS2_SCAN_PREFIX_E0);
sKeyUp.scanPrefix = 0;
// Drop the keyboard's fake-shift releases that bracket the grey keys; a
// standalone 0xAA / 0xB6 (a genuine Shift release) still falls through.
if (extended && (scan == PS2_SCAN_FAKE_SHIFT_LE || scan == PS2_SCAN_FAKE_SHIFT_RE)) {
return;
}
// Break code: bit 7 set.
if (scan & 0x80) {
int32_t next = (sKeyUp.head + 1) % KEYUP_BUF_SIZE;
if (next != sKeyUp.tail) {
sKeyUp.buf[sKeyUp.head].scancode = scan & 0x7F;
sKeyUp.buf[sKeyUp.head].ascii = 0;
sKeyUp.head = next;
}
}
@ -757,13 +894,24 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
__dpmi_meminfo lockInfo;
lockInfo.address = info.address;
lockInfo.size = fbSize;
__dpmi_lock_linear_region(&lockInfo);
if (__dpmi_lock_linear_region(&lockInfo) != 0) {
fprintf(stderr, "VBE: Failed to lock LFB linear region\n");
__dpmi_free_physical_address_mapping(&info);
return -1;
}
if (__djgpp_nearptr_enable() == 0) {
fprintf(stderr, "VBE: Failed to enable near pointers\n");
__dpmi_unlock_linear_region(&lockInfo);
__dpmi_free_physical_address_mapping(&info);
return -1;
}
// Save the RAW linear address (not d->lfb) so we can later unlock/free it.
sLfbLinearAddr = info.address;
sLfbMappedSize = fbSize;
// Convert linear address to near pointer by adding the DS base offset
d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base);
@ -1196,7 +1344,7 @@ const char *platformGetSystemInfo(const DisplayT *display) {
sysInfoAppend("");
sysInfoAppend("=== Disk Drives ===");
for (int32_t drv = 3; drv <= 26; drv++) {
for (int32_t drv = FIRST_FIXED_DRIVE; drv <= MAX_DRIVE_NUMBER; drv++) {
// INT 21h AH=36h: Get disk free space
memset(&r, 0, sizeof(r));
r.x.ax = 0x3600;
@ -1330,10 +1478,44 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) {
void platformKeyUpInit(void) {
unsigned long codeAddr;
unsigned long dataAddr;
__dpmi_meminfo codeRegion;
__dpmi_meminfo dataRegion;
if (sKeyUpInstalled) {
return;
}
// The INT 9 handler runs at hardware-interrupt time, so its code and the
// ring-buffer state it touches must be locked into physical memory. A
// page fault inside the ISR under a paging DPMI host (e.g. CWSDPMI) would
// be fatal. On lock failure we degrade gracefully: key-up detection is
// simply unavailable rather than risking a crash.
if (__dpmi_get_segment_base_address(_my_cs(), &codeAddr) != 0 ||
__dpmi_get_segment_base_address(_my_ds(), &dataAddr) != 0) {
fprintf(stderr, "KEYUP: Failed to read segment base addresses\n");
return;
}
codeRegion.handle = 0;
codeRegion.size = KEYUP_ISR_SIZE;
codeRegion.address = codeAddr + (unsigned long)int9Handler;
dataRegion.handle = 0;
dataRegion.size = sizeof(sKeyUp);
dataRegion.address = dataAddr + (unsigned long)&sKeyUp;
if (__dpmi_lock_linear_region(&codeRegion) != 0) {
fprintf(stderr, "KEYUP: Failed to lock handler code\n");
return;
}
if (__dpmi_lock_linear_region(&dataRegion) != 0) {
fprintf(stderr, "KEYUP: Failed to lock ISR state\n");
__dpmi_unlock_linear_region(&codeRegion);
return;
}
_go32_dpmi_get_protected_mode_interrupt_vector(9, &sOldInt9);
sNewInt9.pm_offset = (unsigned long)int9Handler;
@ -1346,23 +1528,43 @@ void platformKeyUpInit(void) {
bool platformKeyUpRead(PlatformKeyEventT *evt) {
if (sKeyUpTail == sKeyUpHead) {
if (sKeyUp.tail == sKeyUp.head) {
return false;
}
*evt = sKeyUpBuf[sKeyUpTail];
sKeyUpTail = (sKeyUpTail + 1) % KEYUP_BUF_SIZE;
*evt = sKeyUp.buf[sKeyUp.tail];
sKeyUp.tail = (sKeyUp.tail + 1) % KEYUP_BUF_SIZE;
return true;
}
void platformKeyUpShutdown(void) {
unsigned long codeAddr;
unsigned long dataAddr;
__dpmi_meminfo region;
if (!sKeyUpInstalled) {
return;
}
_go32_dpmi_set_protected_mode_interrupt_vector(9, &sOldInt9);
sKeyUpInstalled = false;
// Unlock the regions pinned in platformKeyUpInit, after the IRQ is no
// longer pointed at our handler.
if (__dpmi_get_segment_base_address(_my_ds(), &dataAddr) == 0) {
region.handle = 0;
region.size = sizeof(sKeyUp);
region.address = dataAddr + (unsigned long)&sKeyUp;
__dpmi_unlock_linear_region(&region);
}
if (__dpmi_get_segment_base_address(_my_cs(), &codeAddr) == 0) {
region.handle = 0;
region.size = KEYUP_ISR_SIZE;
region.address = codeAddr + (unsigned long)int9Handler;
__dpmi_unlock_linear_region(&region);
}
}
@ -1407,13 +1609,21 @@ void platformLogCrashDetail(int sig, PlatformLogFnT logFn) {
// (or emulated by the DOS environment) that tracks position and
// buttons independently of the application.
//
// We must set the movement range to match our VESA resolution,
// because the default range may be 640x200 (CGA text mode).
// Without this, mouse coordinates would be wrong or clipped.
// Boots in MICKEY mode (relative integration), the only scheme safe on real
// hardware (see the cursor-state block). We start the cursor at screen center
// and flush the mickey counters so the first poll begins clean. The latch is
// reset unconditionally here because dvxApp re-calls this on every live video-
// mode change -- a promotion earned in a prior mode must never carry over.
//
// The hardware cursor is never shown -- DVX composites its own
// software cursor on top of the backbuffer. We only use INT 33h
// for position/button state via polling (function 03h).
// Functions 07h/08h tell the driver our true screen range. This is NOT
// trusted positioning: it is bait for the runtime detector in platformMousePoll
// -- a driver that honours the range (DOSBox) then reports full-screen absolute
// coordinates, a driver that ignores it (ctmouse/86Box) clamps to a low-res
// trap and the detector never promotes.
//
// The hardware cursor is never shown -- DVX composites its own software cursor
// on top of the backbuffer. We use INT 33h for button state (function 03h),
// mickey deltas (function 0Bh), and absolute position after promotion (03h).
void platformMouseInit(int32_t screenW, int32_t screenH) {
__dpmi_regs r;
@ -1422,57 +1632,110 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
sMouseRangeH = screenH;
sCurX = screenW / 2;
sCurY = screenH / 2;
sMouseMode = MouseModeMickeyE;
sPromoteCounter = 0;
// Function 00h: reset driver
// Function 00h: reset driver, detect mouse hardware
memset(&r, 0, sizeof(r));
r.x.ax = 0x0000;
__dpmi_int(0x33, &r);
// Function 07h: set horizontal range
// Function 07h: set horizontal range (bait for the absolute detector).
memset(&r, 0, sizeof(r));
r.x.ax = 0x0007;
r.x.cx = 0;
r.x.dx = screenW - 1;
__dpmi_int(0x33, &r);
// Function 08h: set vertical range
// Function 08h: set vertical range (bait for the absolute detector).
memset(&r, 0, sizeof(r));
r.x.ax = 0x0008;
r.x.cx = 0;
r.x.dx = screenH - 1;
__dpmi_int(0x33, &r);
// Function 04h: warp cursor to center
// Flush any stale mickey counters so the first poll starts clean.
// Function 0Bh returns and resets the accumulated motion counters.
memset(&r, 0, sizeof(r));
r.x.ax = 0x0004;
r.x.cx = screenW / 2;
r.x.dx = screenH / 2;
r.x.ax = 0x000B;
__dpmi_int(0x33, &r);
}
// Reads button state and cursor position via INT 33h function 03h.
// Coordinate range was set by functions 07h/08h in platformMouseInit.
// Reads button state and wheel via function 03h, then updates the software
// cursor by whichever scheme sMouseMode selects (see the cursor-state block):
// MICKEY integrates function 0Bh relative deltas (real-hardware path), while
// ABSOLUTE consumes the function 03h position directly (DOSBox path). Starts
// in MICKEY and promotes to ABSOLUTE once the driver proves -- via a function
// 03h position outside the low-res trap box -- that it honours the real range.
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
__dpmi_regs r;
// Function 03h: read button state and absolute position.
// Coordinate range was set by functions 07h/08h in platformMouseInit.
// Function 03h: read button state, wheel, and absolute position in one go.
memset(&r, 0, sizeof(r));
r.x.ax = 0x0003;
__dpmi_int(0x33, &r);
*buttons = r.x.bx & 0x07;
*buttons = r.x.bx & MOUSE_BUTTON_MASK;
// BH = signed 8-bit wheel counter (cleared on read by the driver).
// Accumulate so a second poll before platformMouseWheelPoll reads it
// doesn't drop the first delta (dvxPlat.h: accumulated between polls).
if (sHasMouseWheel) {
sLastWheelDelta = (int32_t)(int8_t)(r.x.bx >> 8);
sLastWheelDelta += (int32_t)(int8_t)(r.x.bx >> 8);
}
sCurX = (int16_t)r.x.cx;
sCurY = (int16_t)r.x.dx;
int32_t absX = (int16_t)r.x.cx;
int32_t absY = (int16_t)r.x.dx;
// While still on mickeys, watch the absolute report for proof the driver
// honours the high-res range. A trapped driver (ctmouse/86Box) clamps
// every read inside the trap box and can never satisfy this; DOSBox does
// as soon as the pointer leaves the top band. The per-axis screen-vs-trap
// guard keeps a genuine low-res mode (where trap and honest are
// indistinguishable) from ever promoting.
if (sMouseMode == MouseModeMickeyE) {
bool outOfTrap = false;
if (sMouseRangeH > MOUSE_TRAP_BOX_H && absY >= MOUSE_TRAP_BOX_H) {
outOfTrap = true;
}
if (sMouseRangeW > MOUSE_TRAP_BOX_W && absX >= MOUSE_TRAP_BOX_W) {
outOfTrap = true;
}
if (outOfTrap) {
sPromoteCounter++;
} else {
sPromoteCounter = 0;
}
if (sPromoteCounter >= MOUSE_PROMOTE_CONSEC) {
// The absolute branch below seeds sCurX/sCurY from absX/absY this
// same poll, so the cursor does not teleport from its integrated
// position.
sMouseMode = MouseModeAbsoluteE;
}
}
if (sMouseMode == MouseModeAbsoluteE) {
// Consume the driver position directly. Do NOT read function 0Bh
// here: integrating mickeys on top of an absolute snap would double-
// count motion in DOSBox's uncaptured (emulate=false) path.
sCurX = absX;
sCurY = absY;
} else {
// Function 0Bh: read mickey motion counters (signed 16-bit deltas,
// cleared on read). Accumulate into software cursor position.
memset(&r, 0, sizeof(r));
r.x.ax = 0x000B;
__dpmi_int(0x33, &r);
sCurX += (int16_t)r.x.cx;
sCurY += (int16_t)r.x.dx;
}
if (sCurX < 0) {
sCurX = 0;
@ -1528,27 +1791,33 @@ void platformMouseSetMickeys(int32_t horizMickeys, int32_t vertMickeys) {
}
// Moves the mouse cursor to an absolute screen position via INT 33h
// function 04h. Used to clamp the cursor to window edges during resize
// so the pointer visually sticks to the border.
// Moves the software cursor to an absolute screen position. Used to clamp the
// cursor to window edges during resize so the pointer visually sticks to the
// border. The driver-side follow-up depends on the tracking mode: in ABSOLUTE
// mode the next poll reads the driver position, so we must move the driver
// cursor too (function 04h) or the warp is immediately undone; in MICKEY mode
// we only flush the pending relative deltas (function 0Bh).
void platformMouseWarp(int32_t x, int32_t y) {
__dpmi_regs r;
sCurX = x;
sCurY = y;
__dpmi_regs r;
// Tell the driver the new position (function 04h) and flush
// pending mickeys (function 0Bh) so neither mode gets stale data.
if (sMouseMode == MouseModeAbsoluteE) {
// Function 04h: move the driver cursor so the next 03h read agrees.
memset(&r, 0, sizeof(r));
r.x.ax = 0x0004;
r.x.cx = x;
r.x.dx = y;
__dpmi_int(0x33, &r);
} else {
// Function 0Bh: flush pending mickeys so the next poll doesn't undo
// the warp by applying stale deltas on top of it.
memset(&r, 0, sizeof(r));
r.x.ax = 0x000B;
__dpmi_int(0x33, &r);
}
}
@ -1576,7 +1845,10 @@ bool platformMouseWheelInit(void) {
// movement or if the wheel is not supported.
int32_t platformMouseWheelPoll(void) {
return sLastWheelDelta;
int32_t delta = sLastWheelDelta;
sLastWheelDelta = 0;
return delta;
}
@ -1764,6 +2036,11 @@ void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) {
void platformSplashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t color) {
// Stage one filled scanline once, then block-move (rep movs) the clipped
// span per row instead of poking each pixel individually.
uint8_t rowBuf[SPLASH_VGA_W];
memset(rowBuf, color, SPLASH_VGA_W);
for (int32_t row = y; row < y + h; row++) {
if (row < 0 || row >= SPLASH_VGA_H) {
continue;
@ -1771,10 +2048,13 @@ void platformSplashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t
int32_t x0 = x < 0 ? 0 : x;
int32_t x1 = (x + w) > SPLASH_VGA_W ? SPLASH_VGA_W : (x + w);
int32_t spanW = x1 - x0;
for (int32_t col = x0; col < x1; col++) {
_farpokeb(_dos_ds, SPLASH_VGA_ADDR + row * SPLASH_VGA_W + col, color);
if (spanW <= 0) {
continue;
}
dosmemput(rowBuf, spanW, SPLASH_VGA_ADDR + row * SPLASH_VGA_W + x0);
}
}
@ -1795,16 +2075,16 @@ bool platformSplashLoadRaw(const char *path) {
}
// Read and set palette (768 bytes: 256 x RGB, 6-bit VGA values)
uint8_t pal[768];
uint8_t pal[PALETTE_BYTES];
if (fread(pal, 1, 768, fp) != 768) {
if (fread(pal, 1, PALETTE_BYTES, fp) != PALETTE_BYTES) {
fclose(fp);
return false;
}
outportb(0x3C8, 0);
for (int32_t i = 0; i < 768; i++) {
for (int32_t i = 0; i < PALETTE_BYTES; i++) {
outportb(0x3C9, pal[i]);
}
@ -1817,9 +2097,8 @@ bool platformSplashLoadRaw(const char *path) {
return false;
}
for (int32_t x = 0; x < SPLASH_VGA_W; x++) {
_farpokeb(_dos_ds, SPLASH_VGA_ADDR + y * SPLASH_VGA_W + x, rowBuf[x]);
}
// One block move per scanline (rep movs) instead of 320 byte pokes
dosmemput(rowBuf, SPLASH_VGA_W, SPLASH_VGA_ADDR + y * SPLASH_VGA_W);
}
fclose(fp);
@ -1950,10 +2229,10 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
// Copy mode numbers to a local buffer BEFORE calling 4F01h.
// Some VBE BIOSes store the mode list inside the VBE info block
// at __tb, which 4F01h overwrites when querying mode details.
uint16_t modes[256];
uint16_t modes[MAX_VBE_MODES];
int32_t modeCount = 0;
for (int32_t i = 0; i < 256; i++) {
for (int32_t i = 0; i < MAX_VBE_MODES; i++) {
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
if (mode == 0xFFFF) {
@ -2008,7 +2287,7 @@ void platformVideoFreeBuffers(DisplayT *d) {
d->palette = NULL;
}
d->lfb = NULL;
unmapLfb(d);
}
@ -2056,6 +2335,7 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i
if (!d->backBuf) {
fprintf(stderr, "VBE: Failed to allocate %lu byte backbuffer\n", (unsigned long)fbSize);
unmapLfb(d);
__djgpp_nearptr_disable();
return -1;
}
@ -2064,18 +2344,19 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i
// Set up palette for 8-bit mode
if (d->format.bitsPerPixel == 8) {
d->palette = (uint8_t *)malloc(768);
d->palette = (uint8_t *)malloc(PALETTE_BYTES);
if (!d->palette) {
fprintf(stderr, "VBE: Failed to allocate palette\n");
free(d->backBuf);
d->backBuf = NULL;
unmapLfb(d);
__djgpp_nearptr_disable();
return -1;
}
dvxGeneratePalette(d->palette);
platformVideoSetPalette(d->palette, 0, 256);
platformVideoSetPalette(d->palette, 0, PALETTE_ENTRIES);
}
// Initialize clip rect to full display
@ -2119,17 +2400,7 @@ void platformVideoShutdown(DisplayT *d) {
r.x.ax = 0x0003;
__dpmi_int(0x10, &r);
if (d->backBuf) {
free(d->backBuf);
d->backBuf = NULL;
}
if (d->palette) {
free(d->palette);
d->palette = NULL;
}
d->lfb = NULL;
platformVideoFreeBuffers(d);
__djgpp_nearptr_disable();
}
@ -2190,6 +2461,26 @@ static void sysInfoAppend(const char *fmt, ...) {
}
// Unlocks and frees the DPMI physical LFB mapping recorded by mapLfb.
// Safe to call when nothing is mapped (sLfbLinearAddr == LFB_NOT_MAPPED)
// and idempotent: the sentinel is reset so a second call is a no-op.
// Uses the raw linear address, NOT d->lfb (which has the conventional
// base folded in). Clears d->lfb so callers do not keep a stale pointer.
static void unmapLfb(DisplayT *d) {
if (sLfbLinearAddr != LFB_NOT_MAPPED) {
__dpmi_meminfo info;
info.address = sLfbLinearAddr;
info.size = sLfbMappedSize;
__dpmi_unlock_linear_region(&info);
__dpmi_free_physical_address_mapping(&info);
sLfbLinearAddr = LFB_NOT_MAPPED;
sLfbMappedSize = 0;
}
d->lfb = NULL;
}
// ============================================================
// DXE3 export table
// ============================================================
@ -2321,6 +2612,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformYield)
// --- DPMI/go32 (needed by rs232 serial driver) ---
DXE_EXPORT(__dpmi_free_physical_address_mapping)
DXE_EXPORT(__dpmi_get_segment_base_address)
DXE_EXPORT(__dpmi_lock_linear_region)
DXE_EXPORT(__dpmi_unlock_linear_region)
@ -2573,9 +2865,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(fcntl)
DXE_EXPORT(fileno)
DXE_EXPORT(isatty)
DXE_EXPORT(mkdir)
DXE_EXPORT(pipe)
DXE_EXPORT(rmdir)
DXE_EXPORT(truncate)
DXE_EXPORT(umask)

View file

@ -45,6 +45,9 @@
#include <dir.h>
#endif
// Permission bits for directories created by dvxMakeDirs (rwxr-xr-x).
#define MKDIR_MODE 0755
const char *dvxSkipWs(const char *s) {
if (!s) {
@ -203,7 +206,7 @@ int32_t platformMkdirRecursive(const char *path) {
*p = '\0';
if (buf[0] != '\0') {
mkdir(buf, 0755);
mkdir(buf, MKDIR_MODE);
}
*p = '/';
@ -211,7 +214,7 @@ int32_t platformMkdirRecursive(const char *path) {
}
// Create the final directory
if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
if (mkdir(buf, MKDIR_MODE) != 0 && errno != EEXIST) {
return -1;
}

View file

@ -73,14 +73,17 @@ bool sDebugLayout = false;
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
WidgetT *sDragWidget = NULL; // widget being dragged (any drag type)
int32_t sPollWidgetCount = 0;
WidgetT **sPollWidgets = NULL; // stb_ds dynamic array
uint32_t sWidgetGen = 0; // bumped per widget destroy (see dvxWgtP.h)
// Shared clipboard -- process-wide, not per-widget.
static char *sClipboard = NULL;
static int32_t sClipboardLen = 0;
static int32_t sClipboardCap = 0;
// Max pixel movement between clicks that still counts as a double-click.
#define DBLCLICK_TOLERANCE 4
// Multi-click state (used by widgetEvent.c for universal dbl-click detection)
static clock_t sLastClickTime = 0;
static int32_t sLastClickX = -1;
@ -127,11 +130,6 @@ const char *clipboardGet(int32_t *outLen) {
}
int32_t clipboardMaxLen(void) {
return 65536;
}
// Implements Tab-order navigation: finds the next focusable widget
// after 'after' in depth-first tree order. The two-pass approach
// (search from 'after' to end, then wrap to start) ensures circular
@ -180,7 +178,7 @@ int32_t multiClickDetect(int32_t vx, int32_t vy) {
}
if ((now - sLastClickTime) < sDblClickTicks &&
abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) {
abs(vx - sLastClickX) < DBLCLICK_TOLERANCE && abs(vy - sLastClickY) < DBLCLICK_TOLERANCE) {
sClickCount++;
} else {
sClickCount = 1;
@ -245,7 +243,6 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) {
if (w->wclass->flags & WCLASS_NEEDS_POLL) {
arrput(sPollWidgets, w);
sPollWidgetCount = (int32_t)arrlen(sPollWidgets);
}
if (parent) {
@ -271,15 +268,7 @@ void widgetAllocRollback(WidgetT *w) {
widgetRemoveChild(w->parent, w);
}
if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) {
for (int32_t i = 0; i < sPollWidgetCount; i++) {
if (sPollWidgets[i] == w) {
arrdel(sPollWidgets, i);
sPollWidgetCount = (int32_t)arrlen(sPollWidgets);
break;
}
}
}
widgetClearReferences(w);
free(w);
}
@ -308,6 +297,51 @@ WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, con
}
// Clears every global interaction pointer (focus/popup/drag/key-pressed)
// that references w and removes w from the poll list. Must be called for
// every destroyed node so no global is left pointing at freed memory.
// Also bumps sWidgetGen so in-flight dispatch code knows its cached
// widget pointers may now be dangling (see dvxWgtP.h).
void widgetClearReferences(WidgetT *w) {
sWidgetGen++;
if (sFocusedWidget == w) {
sFocusedWidget = NULL;
}
if (sOpenPopup == w) {
sOpenPopup = NULL;
}
if (sClosedPopup == w) {
sClosedPopup = NULL;
}
if (sDragWidget == w) {
sDragWidget = NULL;
}
if (sKeyPressedBtn == w) {
sKeyPressedBtn = NULL;
}
// Notify any DXE-registered hook (e.g. texthelp's selection tracker) so
// foreign static pointers to w are nulled before w is freed.
if (sWidgetDestroyFn) {
sWidgetDestroyFn(w);
}
if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) {
for (int32_t i = 0; i < (int32_t)arrlen(sPollWidgets); i++) {
if (sPollWidgets[i] == w) {
arrdel(sPollWidgets, i);
break;
}
}
}
}
int32_t widgetCountVisibleChildren(const WidgetT *w) {
int32_t count = 0;
@ -326,11 +360,10 @@ int32_t widgetCountVisibleChildren(const WidgetT *w) {
// itself) so that per-widget destroy callbacks see a consistent
// tree state.
//
// Critically, this function clears all global state pointers that
// reference destroyed widgets. Without this, any pending drag or
// focus state would become a dangling pointer. Each global is
// checked individually rather than cleared unconditionally to
// avoid disrupting unrelated ongoing interactions.
// Critically, every destroyed node is passed through
// widgetClearReferences() so that no global interaction pointer (focus,
// popup, drag, key-pressed) and no poll-list entry is left dangling at
// freed memory.
void widgetDestroyChildren(WidgetT *w) {
WidgetT *child = w->firstChild;
@ -341,18 +374,7 @@ void widgetDestroyChildren(WidgetT *w) {
wclsDestroy(child);
// Clear static references if they point to destroyed widgets
if (sFocusedWidget == child) {
sFocusedWidget = NULL;
}
if (sOpenPopup == child) {
sOpenPopup = NULL;
}
if (sDragWidget == child) {
sDragWidget = NULL;
}
widgetClearReferences(child);
free(child);
child = next;

View file

@ -60,6 +60,7 @@ static int32_t sPrevMouseY = -1;
// ============================================================
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons);
static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH);
// Manages automatic scrollbar addition/removal for widget-based windows.
@ -140,8 +141,12 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
}
wmAddVScrollbar(win, 0, maxV, pageV);
// wmAddVScrollbar leaves win->vScroll NULL if its allocation failed.
if (win->vScroll) {
win->vScroll->value = DVX_MIN(oldVValue, maxV);
}
}
if (needH) {
int32_t pageH = needV ? availW - SCROLLBAR_WIDTH : availW;
@ -152,8 +157,11 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
}
wmAddHScrollbar(win, 0, maxH, pageH);
if (win->hScroll) {
win->hScroll->value = DVX_MIN(oldHValue, maxH);
}
}
if (changed) {
// wmAddVScrollbar/wmAddHScrollbar already call wmUpdateContentRect
@ -164,8 +172,10 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
win->onScroll = widgetOnScroll;
// Layout at the virtual content size (the larger of content area and min size)
int32_t layoutW = DVX_MAX(win->contentW, minW);
int32_t layoutH = DVX_MAX(win->contentH, minH);
int32_t layoutW;
int32_t layoutH;
widgetVirtualSize(win, root, &layoutW, &layoutH);
wgtLayout(root, layoutW, layoutH, &ctx->font);
}
@ -179,11 +189,19 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
// cursor stays visible in the unfocused window.
void widgetOnBlur(WindowT *win) {
// Selection is intentionally NOT cleared here: window-level blur is
// transient (clicking another window's menu bar must not destroy the
// selection a menu Copy/Cut command is about to act on), and keeping
// the selection visible in an inactive window matches convention.
// Selection clears happen on widget-to-widget focus transitions.
if (sFocusedWidget && sFocusedWidget->window == win) {
WidgetT *prev = sFocusedWidget;
sFocusedWidget = NULL;
wgtInvalidatePaint(prev);
// Commit/clamp any in-progress edit before the app onBlur.
wclsOnBlur(prev);
if (prev->onBlur) {
prev->onBlur(prev);
}
@ -231,17 +249,27 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
// Attribute allocations during event handling to the owning app
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
// Snapshot the destroy generation: any handler below may destroy the
// focused widget (a BASIC Change handler unloading its form), after
// which focus is dangling and must not be dereferenced again.
uint32_t gen = sWidgetGen;
wclsOnKey(focus, key, mod);
// Fire user callbacks after the widget's internal handler
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && focus->onKeyPress) {
if (sWidgetGen == gen && key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && focus->onKeyPress) {
focus->onKeyPress(focus, key);
}
if (focus->onKeyDown) {
if (sWidgetGen == gen && focus->onKeyDown) {
focus->onKeyDown(focus, key, mod);
}
@ -268,6 +296,11 @@ void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) {
if (focus->onKeyUp) {
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
@ -302,6 +335,11 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
// Attribute allocations during event handling to the owning app
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
@ -402,8 +440,45 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// Fall through to normal click handling
}
// Left button is up. If it was just released, deliver the left-button
// MouseUp to the widget under the cursor (the remainder of this handler
// is press-time logic and is skipped on release). This is the only path
// that reaches a left onMouseUp, which the early return otherwise left
// unreachable.
if (!(buttons & MOUSE_LEFT)) {
sPrevMouseButtons = 0;
// Deliver the left-button MouseUp (release edge) and any right/middle
// press/release edges to the widget under the cursor, then return --
// the rest of this handler is left-press logic. Self-contained so the
// prevFocus/clearSelection block below does not run for a pure
// right/middle click (the WM already raised/focused the window).
WidgetT *upHit = widgetHitTest(root, x, y);
if (upHit && upHit->enabled) {
int32_t relX = x - upHit->x - upHit->contentOffX;
int32_t relY = y - upHit->y - upHit->contentOffY;
if ((sPrevMouseButtons & MOUSE_LEFT) && upHit->onMouseUp) {
upHit->onMouseUp(upHit, 1, relX, relY);
}
if ((buttons & MOUSE_RIGHT) && !(sPrevMouseButtons & MOUSE_RIGHT) && upHit->onMouseDown) {
upHit->onMouseDown(upHit, 2, relX, relY);
}
if (!(buttons & MOUSE_RIGHT) && (sPrevMouseButtons & MOUSE_RIGHT) && upHit->onMouseUp) {
upHit->onMouseUp(upHit, 2, relX, relY);
}
if ((buttons & MOUSE_MIDDLE) && !(sPrevMouseButtons & MOUSE_MIDDLE) && upHit->onMouseDown) {
upHit->onMouseDown(upHit, 3, relX, relY);
}
if (!(buttons & MOUSE_MIDDLE) && (sPrevMouseButtons & MOUSE_MIDDLE) && upHit->onMouseUp) {
upHit->onMouseUp(upHit, 3, relX, relY);
}
}
sPrevMouseButtons = buttons;
return;
}
@ -420,14 +495,37 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// Clear focus from the previously focused widget. Must set
// sFocusedWidget to NULL BEFORE invalidating so the inline paint
// sees the widget as unfocused and erases its highlight.
// sees the widget as unfocused and erases its highlight. Only
// clear the selection when the click landed on a DIFFERENT widget
// -- same-widget clicks (drag-select, shift-click) manage their
// own selection in the widget's mouse handler.
WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) {
sFocusedWidget = NULL;
if (hit != prevFocus) {
wclsClearSelection(prevFocus);
}
wgtInvalidatePaint(prevFocus);
}
// Snapshot the destroy generation: any callback below may destroy
// widgets (a BASIC Click handler unloading its form, a help link
// rebuilding the page). A changed generation means hit / prevFocus
// may be dangling, so every later dereference is gated on it.
uint32_t gen = sWidgetGen;
// Press-time dispatch must fire only on the left-button DOWN edge, not on
// every held-and-move event. Otherwise a non-drag-capturing widget
// (checkbox, spinner, radio) re-runs its onMouse on every move pixel while
// the button is held, re-toggling or re-stepping state. Drag widgets set
// sDragWidget on this first edge and are then handled by the sDragWidget
// fast paths above, so the edge gate still fires their initial press.
bool leftPressEdge = (buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT);
if (leftPressEdge) {
// Dispatch to the hit widget's mouse handler via vtable. The handler
// is responsible for setting sFocusedWidget if it wants focus.
if (hit->enabled) {
@ -439,7 +537,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// are excluded from onClick because they use press-release semantics
// (onClick fires on button-up, not button-down) and already handle it
// in the release handler above. They still get onDblClick here.
if (hit->enabled) {
if (sWidgetGen == gen && hit->enabled) {
int32_t clicks = multiClickDetect(vx, vy);
bool isBtn = (hit->wclass && (hit->wclass->flags & WCLASS_PRESS_RELEASE));
@ -449,53 +547,54 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
hit->onClick(hit);
}
}
}
// Fire mouse event callbacks (content-relative coordinates)
if (hit->enabled) {
if (sWidgetGen == gen && hit->enabled) {
int32_t relX = vx - hit->x - hit->contentOffX;
int32_t relY = vy - hit->y - hit->contentOffY;
// MouseDown: button just pressed
if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) {
if (hit->onMouseDown) {
if (sWidgetGen == gen && hit->onMouseDown) {
hit->onMouseDown(hit, 1, relX, relY);
}
}
if ((buttons & MOUSE_RIGHT) && !(sPrevMouseButtons & MOUSE_RIGHT)) {
if (hit->onMouseDown) {
if (sWidgetGen == gen && hit->onMouseDown) {
hit->onMouseDown(hit, 2, relX, relY);
}
}
if ((buttons & MOUSE_MIDDLE) && !(sPrevMouseButtons & MOUSE_MIDDLE)) {
if (hit->onMouseDown) {
if (sWidgetGen == gen && hit->onMouseDown) {
hit->onMouseDown(hit, 3, relX, relY);
}
}
// MouseUp: button just released
if (!(buttons & MOUSE_LEFT) && (sPrevMouseButtons & MOUSE_LEFT)) {
if (hit->onMouseUp) {
if (sWidgetGen == gen && hit->onMouseUp) {
hit->onMouseUp(hit, 1, relX, relY);
}
}
if (!(buttons & MOUSE_RIGHT) && (sPrevMouseButtons & MOUSE_RIGHT)) {
if (hit->onMouseUp) {
if (sWidgetGen == gen && hit->onMouseUp) {
hit->onMouseUp(hit, 2, relX, relY);
}
}
if (!(buttons & MOUSE_MIDDLE) && (sPrevMouseButtons & MOUSE_MIDDLE)) {
if (hit->onMouseUp) {
if (sWidgetGen == gen && hit->onMouseUp) {
hit->onMouseUp(hit, 3, relX, relY);
}
}
// MouseMove: position changed
if (vx != sPrevMouseX || vy != sPrevMouseY) {
if (hit->onMouseMove) {
if (sWidgetGen == gen && hit->onMouseMove) {
hit->onMouseMove(hit, buttons, relX, relY);
}
}
@ -507,14 +606,23 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// sFocusedWidget is now set directly by the widget's mouse handler
// Fire focus/blur callbacks on transitions
if (prevFocus && prevFocus != sFocusedWidget && prevFocus->onBlur) {
// Fire focus/blur callbacks on transitions. Skipped entirely if a
// callback destroyed widgets -- prevFocus may be dangling then.
if (sWidgetGen == gen) {
if (prevFocus && prevFocus != sFocusedWidget) {
// Internal blur (commit/clamp edits) fires before the app onBlur
// so a LostFocus handler sees the committed value.
wclsOnBlur(prevFocus);
if (prevFocus->onBlur) {
prevFocus->onBlur(prevFocus);
}
}
if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) {
sFocusedWidget->onFocus(sFocusedWidget);
}
}
}
@ -574,8 +682,10 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
// Apply scroll offset and re-layout at virtual size
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t layoutW = DVX_MAX(win->contentW, root->calcMinW);
int32_t layoutH = DVX_MAX(win->contentH, root->calcMinH);
int32_t layoutW;
int32_t layoutH;
widgetVirtualSize(win, root, &layoutW, &layoutH);
root->x = -scrollX;
root->y = -scrollY;
@ -586,8 +696,11 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
widgetLayoutChildren(root, &ctx->font);
}
// Auto-focus first focusable widget if nothing has focus yet
if (!sFocusedWidget) {
// Auto-focus first focusable widget if nothing has focus yet, but
// only for the window that actually holds WM focus. Background and
// just-blurred windows are repainted by the deferred-paint loop too,
// and must not steal global widget focus from the active window.
if (!sFocusedWidget && win->focused) {
WidgetT *first = widgetFindNextFocusable(root, NULL);
if (first) {
@ -647,3 +760,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
}
}
}
// Computes the virtual content size: the larger of the available content area
// and the widget tree's measured minimum. Used both to drive scroll ranges
// (widgetManageScrollbars) and the paint-time layout (widgetOnPaint) so the
// two never diverge.
static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH) {
*outW = DVX_MAX(win->contentW, root->calcMinW);
*outH = DVX_MAX(win->contentH, root->calcMinH);
}

View file

@ -79,9 +79,10 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
// Widgets with getLayoutMetrics override default padding/gap/extraTop
int32_t frameExtraTop = 0;
if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
int32_t metricBorderW = 0;
bool hasMetrics = wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS);
if (hasMetrics) {
wclsGetLayoutMetrics(w, font, &pad, &gap, &frameExtraTop, &metricBorderW);
}
@ -120,11 +121,12 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
w->calcMinH = mainSize + frameExtraTop;
}
// Border (Frame and similar containers with getLayoutMetrics)
if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
int32_t fb = widgetFrameBorderWidth(w);
w->calcMinW += fb * 2;
w->calcMinH += fb * 2;
// Border (Frame and similar containers with getLayoutMetrics).
// Reuse the border width already returned by wclsGetLayoutMetrics
// above instead of dispatching the metrics vtable call a second time.
if (hasMetrics) {
w->calcMinW += metricBorderW * 2;
w->calcMinH += metricBorderW * 2;
}
}

View file

@ -48,6 +48,7 @@ static bool sFullRepaint = false;
static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops);
static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y);
static WidgetT *wgtFindImpl(WidgetT *w, const char *name);
static void widgetNotifyChildChanged(WidgetT *w);
// Draws a 1px border in a neon color derived from the widget pointer.
@ -77,10 +78,7 @@ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
int32_t idx = (int32_t)((h >> 16) % 12);
uint32_t color = packColor(d, palette[idx][0], palette[idx][1], palette[idx][2]);
drawHLine(d, ops, w->x, w->y, w->w, color);
drawHLine(d, ops, w->x, w->y + w->h - 1, w->w, color);
drawVLine(d, ops, w->x, w->y, w->h, color);
drawVLine(d, ops, w->x + w->w - 1, w->y, w->h, color);
drawRectOutline(d, ops, w->x, w->y, w->w, w->h, color);
}
@ -112,14 +110,7 @@ void wgtDestroy(WidgetT *w) {
}
// Notify parent chain of child destruction via onChildChanged vtable
if (w->parent) {
for (WidgetT *p = w->parent; p; p = p->parent) {
if (wclsHas(p, WGT_METHOD_ON_CHILD_CHANGED)) {
wclsOnChildChanged(p, w);
break;
}
}
}
widgetNotifyChildChanged(w);
if (w->parent) {
widgetRemoveChild(w->parent, w);
@ -129,28 +120,7 @@ void wgtDestroy(WidgetT *w) {
wclsDestroy(w);
// Clear static references
if (sFocusedWidget == w) {
sFocusedWidget = NULL;
}
if (sOpenPopup == w) {
sOpenPopup = NULL;
}
if (sDragWidget == w) {
sDragWidget = NULL;
}
if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) {
for (int32_t i = 0; i < sPollWidgetCount; i++) {
if (sPollWidgets[i] == w) {
arrdel(sPollWidgets, i);
sPollWidgetCount = (int32_t)arrlen(sPollWidgets);
break;
}
}
}
widgetClearReferences(w);
// If this is the root, clear the window's reference
if (w->window && w->window->widgetRoot == w) {
@ -161,6 +131,27 @@ void wgtDestroy(WidgetT *w) {
}
// Destroys all children of w, leaving w itself intact and childless.
// Each child goes through the full wgtDestroy path -- so parents get
// onChildChanged notifications and global references are cleared --
// unlike the internal bulk widgetDestroyChildren used during teardown.
// Use this when rebuilding a container's contents at runtime so the
// old subtrees are freed instead of leaked.
void wgtDestroyChildren(WidgetT *w) {
if (!w) {
return;
}
WidgetT *child = w->firstChild;
while (child) {
WidgetT *next = child->nextSibling;
wgtDestroy(child);
child = next;
}
}
WidgetT *wgtFind(WidgetT *root, const char *name) {
if (!root || !name) {
return NULL;
@ -377,15 +368,21 @@ void wgtSetFocused(WidgetT *w) {
WidgetT *prev = sFocusedWidget;
if (prev && prev != w) {
wclsClearSelection(prev);
wgtInvalidatePaint(prev);
}
sFocusedWidget = w;
wgtInvalidatePaint(w);
if (prev && prev != w && prev->onBlur) {
if (prev && prev != w) {
// Commit/clamp any in-progress edit before the app onBlur.
wclsOnBlur(prev);
if (prev->onBlur) {
prev->onBlur(prev);
}
}
if (w->onFocus) {
w->onFocus(w);
@ -437,17 +434,27 @@ void wgtSetVisible(WidgetT *w, bool visible) {
w->visible = visible;
// Notify parent chain of child visibility change via onChildChanged vtable
if (w->parent) {
widgetNotifyChildChanged(w);
wgtInvalidate(w);
}
}
// Walks the ancestor chain and notifies the nearest ancestor that implements
// onChildChanged of a structural change to w (destroy, visibility toggle).
// Stops at the first ancestor that handles it.
static void widgetNotifyChildChanged(WidgetT *w) {
if (!w->parent) {
return;
}
for (WidgetT *p = w->parent; p; p = p->parent) {
if (wclsHas(p, WGT_METHOD_ON_CHILD_CHANGED)) {
wclsOnChildChanged(p, w);
break;
}
}
}
wgtInvalidate(w);
}
}
@ -573,7 +580,15 @@ void widgetPressableOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
if (pressableHitTest(w, root, x, y)) {
if (w->onClick) {
// The click handler may destroy this widget (a BASIC Click
// handler unloading its own form); only repaint if it didn't.
uint32_t gen = sWidgetGen;
w->onClick(w);
if (sWidgetGen != gen) {
return;
}
}
}

View file

@ -60,50 +60,15 @@ void widgetDrawScrollbarHEx(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
return;
}
// Trough background
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
drawBevel(d, ops, sbX, sbY, sbW, barW, &troughBevel);
// Left arrow button
BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
drawBevel(d, ops, sbX, sbY, barW, barW, &btnBevel);
// Left arrow triangle
{
int32_t cx = sbX + barW / 2;
int32_t cy = sbY + barW / 2;
uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) {
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
}
}
// Right arrow button
int32_t rightX = sbX + sbW - barW;
drawBevel(d, ops, rightX, sbY, barW, barW, &btnBevel);
// Right arrow triangle
{
int32_t cx = rightX + barW / 2;
int32_t cy = sbY + barW / 2;
uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) {
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
}
}
// Thumb
int32_t trackLen = sbW - barW * 2;
int32_t thumbPos = 0;
int32_t thumbSize = 0;
if (trackLen > 0 && totalSize > 0) {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawBevel(d, ops, sbX + barW + thumbPos, sbY, thumbSize, barW, &btnBevel);
}
drawScrollbar(d, ops, colors, ScrollbarHorizontalE, sbX, sbY, sbW, barW, thumbPos, thumbSize);
}
@ -117,50 +82,15 @@ void widgetDrawScrollbarVEx(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
return;
}
// Trough background
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
drawBevel(d, ops, sbX, sbY, barW, sbH, &troughBevel);
// Up arrow button
BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
drawBevel(d, ops, sbX, sbY, barW, barW, &btnBevel);
// Up arrow triangle
{
int32_t cx = sbX + barW / 2;
int32_t cy = sbY + barW / 2;
uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg);
}
}
// Down arrow button
int32_t downY = sbY + sbH - barW;
drawBevel(d, ops, sbX, downY, barW, barW, &btnBevel);
// Down arrow triangle
{
int32_t cx = sbX + barW / 2;
int32_t cy = downY + barW / 2;
uint32_t fg = colors->scrollbarFg;
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg);
}
}
// Thumb
int32_t trackLen = sbH - barW * 2;
int32_t thumbPos = 0;
int32_t thumbSize = 0;
if (trackLen > 0 && totalSize > 0) {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawBevel(d, ops, sbX, sbY + barW + thumbPos, barW, thumbSize, &btnBevel);
}
drawScrollbar(d, ops, colors, ScrollbarVerticalE, sbX, sbY, sbH, barW, thumbPos, thumbSize);
}
@ -181,7 +111,7 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
int32_t trackLen = sbLen - WGT_SB_W * 2;
if (trackLen > 0) {
if (trackLen > 0 && totalSize > 0) {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);

View file

@ -84,6 +84,13 @@ typedef enum {
// arbitrary context (e.g., a ShellAppT* for the DVX shell's app wrapper)
typedef void (*TaskEntryT)(void *arg);
// Per-task context-value hooks (tsSetContextHooks). The save hook is
// called for the outgoing task on every switch and its result stored
// per-task; the restore hook reinstates the incoming task's stored
// value. Lets the host make one global behave as task-local state.
typedef int32_t (*TsCtxSaveFnT)(void);
typedef void (*TsCtxRestoreFnT)(int32_t value);
// 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
// path (tsRecoverToMain) always returns control here. No separate stack is
@ -112,6 +119,11 @@ int32_t tsPause(uint32_t taskId);
// share of CPU time immediately rather than waiting for the next round.
int32_t tsResume(uint32_t taskId);
// Register the per-task context-value hooks (see TsCtxSaveFnT). Pass
// NULLs to unregister. The DVX shell uses this to keep currentAppId
// correct per task so allocations and windows are attributed properly.
void tsSetContextHooks(TsCtxSaveFnT save, TsCtxRestoreFnT restore);
// Set a task's scheduling priority. Also refills credits so the change
// takes effect immediately rather than at the next credit refill epoch.
int32_t tsSetPriority(uint32_t taskId, int32_t priority);

View file

@ -101,6 +101,7 @@ typedef struct {
int32_t credits;
TaskEntryT entry;
void *arg;
int32_t ctxValue; // per-task value saved/restored via tsSetContextHooks
bool isMain;
bool allocated; // true if slot is in use, false if free for reuse
} TaskBlockT;
@ -117,6 +118,21 @@ static TaskBlockT *tasks = NULL;
// tsPause (self-pause), and tsRecoverToMain.
static uint32_t currentIdx = 0;
static bool initialized = false;
// Per-task context-value hooks (see tsSetContextHooks). Lets the host
// treat one global (e.g. the shell's currentAppId) as task-local state:
// the save hook captures it from the outgoing task, the restore hook
// reinstates the incoming task's saved copy on every switch.
static TsCtxSaveFnT sCtxSaveFn = NULL;
static TsCtxRestoreFnT sCtxRestoreFn = NULL;
// Stack of a self-terminated (tsExit) task. It cannot be freed inside tsExit
// because ESP/RSP still points into it until contextSwitch swaps stacks.
// Instead the pointer is stashed here and released by the next entry into any
// scheduler routine, which by construction runs on a different, live stack. At
// most one stack is ever pending because the cooperative single-threaded
// switcher cannot re-enter tsExit until control resumes on another task and
// clears this first.
static uint8_t *sStackPendingFree = NULL;
// ============================================================================
// Forward declarations (alphabetical)
@ -124,7 +140,10 @@ static bool initialized = false;
static void contextSwitch(TaskContextT *save, TaskContextT *restore);
static int32_t findFreeSlot(void);
static void freePendingStack(void);
static int32_t scanReady(void);
static uint32_t scheduleNext(void);
static void switchContextValue(uint32_t prev, uint32_t next);
static void taskTrampoline(void);
uint32_t tsActiveCount(void);
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
@ -138,6 +157,7 @@ int32_t tsKill(uint32_t taskId);
int32_t tsPause(uint32_t taskId);
void tsRecoverToMain(void);
int32_t tsResume(uint32_t taskId);
void tsSetContextHooks(TsCtxSaveFnT save, TsCtxRestoreFnT restore);
int32_t tsSetPriority(uint32_t taskId, int32_t priority);
void tsShutdown(void);
void tsYield(void);
@ -248,6 +268,36 @@ static int32_t findFreeSlot(void) {
}
// Release the stack stashed by a previous tsExit. Called at the entry of every
// scheduler routine, where we are guaranteed to be running on a live stack
// (never the exited task's), so the deferred free is safe.
static void freePendingStack(void) {
if (sStackPendingFree) {
free(sStackPendingFree);
sStackPendingFree = NULL;
}
}
// Scan ready tasks (round-robin starting after currentIdx) for one that
// still has credits. On a hit, consume one credit and return its index.
// Returns -1 when no ready task has credits left.
static int32_t scanReady(void) {
uint32_t count = (uint32_t)arrlen(tasks);
for (uint32_t i = 1; i <= count; i++) {
uint32_t idx = (currentIdx + i) % count;
if (tasks[idx].allocated && tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) {
tasks[idx].credits--;
return (int32_t)idx;
}
}
return -1;
}
// Find the next task to run using credit-based weighted round-robin.
// Each ready task holds (priority + 1) credits. One credit is consumed
// per scheduling turn. When no ready task has credits left, every
@ -271,16 +321,15 @@ static uint32_t scheduleNext(void) {
uint32_t count = (uint32_t)arrlen(tasks);
// First pass: look for a ready task with remaining credits
for (uint32_t i = 1; i <= count; i++) {
uint32_t idx = (currentIdx + i) % count;
if (tasks[idx].allocated && tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) {
tasks[idx].credits--;
return idx;
}
int32_t found = scanReady();
if (found >= 0) {
return (uint32_t)found;
}
// All credits exhausted -- start a new epoch by refilling every ready task
bool anyReady = false;
for (uint32_t i = 0; i < count; i++) {
if (tasks[i].allocated && tasks[i].state == TaskStateReady) {
tasks[i].credits = tasks[i].priority + 1;
@ -293,18 +342,31 @@ static uint32_t scheduleNext(void) {
}
// Pick the first ready task after refill
for (uint32_t i = 1; i <= count; i++) {
uint32_t idx = (currentIdx + i) % count;
if (tasks[idx].allocated && tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) {
tasks[idx].credits--;
return idx;
}
found = scanReady();
if (found >= 0) {
return (uint32_t)found;
}
return currentIdx;
}
// Saves the outgoing task's context value and restores the incoming
// task's, via the host hooks (tsSetContextHooks). Called immediately
// before every contextSwitch so values like the shell's currentAppId
// behave as task-local state instead of leaking across switches.
static void switchContextValue(uint32_t prev, uint32_t next) {
if (sCtxSaveFn) {
tasks[prev].ctxValue = sCtxSaveFn();
}
if (sCtxRestoreFn) {
sCtxRestoreFn(tasks[next].ctxValue);
}
}
// 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
// rather than calling entry directly because we need to call tsExit() when
@ -338,6 +400,10 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
if (!initialized || !entry) {
return TS_ERR_PARAM;
}
// Reclaim a deferred-freed stack before recycling a slot / allocating.
freePendingStack();
if (stackSize == 0) {
stackSize = TS_DEFAULT_STACK_SIZE;
}
@ -371,6 +437,7 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
task->credits = priority + 1;
task->entry = entry;
task->arg = arg;
task->ctxValue = 0;
task->isMain = false;
task->allocated = true;
@ -425,10 +492,11 @@ void tsExit(void) {
tasks[currentIdx].state = TaskStateTerminated;
// Free the stack immediately -- safe because we're about to switch
// away and never return. The context switch itself doesn't touch
// the old stack after swapping ESP/RSP.
free(tasks[currentIdx].stack);
// Defer freeing the stack -- we are still executing on it (ESP/RSP points
// into this block until contextSwitch swaps stacks). Stash it and let the
// next scheduler entry (on a live stack) release it.
freePendingStack();
sStackPendingFree = tasks[currentIdx].stack;
tasks[currentIdx].stack = NULL;
tasks[currentIdx].allocated = false;
@ -438,6 +506,7 @@ void tsExit(void) {
currentIdx = next;
tasks[next].state = TaskStateRunning;
switchContextValue(prev, next);
contextSwitch(&tasks[prev].context, &tasks[next].context);
// Terminated task never resumes here
}
@ -547,6 +616,10 @@ int32_t tsPause(uint32_t taskId) {
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
return TS_ERR_PARAM;
}
// Self-pause resumes after a context switch; reclaim on this live stack.
freePendingStack();
if (!tasks[taskId].allocated) {
return TS_ERR_PARAM;
}
@ -571,6 +644,7 @@ int32_t tsPause(uint32_t taskId) {
currentIdx = next;
tasks[next].state = TaskStateRunning;
switchContextValue(prev, next);
contextSwitch(&tasks[prev].context, &tasks[next].context);
}
}
@ -597,6 +671,13 @@ void tsRecoverToMain(void) {
currentIdx = 0;
tasks[0].state = TaskStateRunning;
// Crash recovery longjmps here without a normal switch, so restore
// the main task's context value explicitly -- otherwise the crashed
// task's value (e.g. its app id) leaks into the shell.
if (sCtxRestoreFn) {
sCtxRestoreFn(tasks[0].ctxValue);
}
}
@ -620,6 +701,12 @@ int32_t tsResume(uint32_t taskId) {
}
void tsSetContextHooks(TsCtxSaveFnT save, TsCtxRestoreFnT restore) {
sCtxSaveFn = save;
sCtxRestoreFn = restore;
}
int32_t tsSetPriority(uint32_t taskId, int32_t priority) {
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
return TS_ERR_PARAM;
@ -642,6 +729,10 @@ void tsShutdown(void) {
return;
}
// Release any deferred-freed stack so it is not leaked at shutdown (the
// exited task's tasks[i].stack is already NULL).
freePendingStack();
ptrdiff_t count = arrlen(tasks);
for (ptrdiff_t i = 0; i < count; i++) {
free(tasks[i].stack);
@ -666,6 +757,9 @@ void tsYield(void) {
return;
}
// Resumes after a context switch; reclaim a deferred stack on this stack.
freePendingStack();
uint32_t next = scheduleNext();
if (next == currentIdx) {
return;
@ -682,5 +776,6 @@ void tsYield(void) {
currentIdx = next;
tasks[next].state = TaskStateRunning;
switchContextValue(prev, next);
contextSwitch(&tasks[prev].context, &tasks[next].context);
}

View file

@ -118,6 +118,12 @@ int32_t widgetMaxItemLen(const char **items, int32_t count) {
// Returns -1 for unrecognized keys so callers can check whether the
// key was consumed.
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
// An empty list has no valid index; leave the selection unchanged so
// callers treat it as 'no navigation' instead of selecting index 0.
if (count <= 0) {
return current;
}
if (key == KEY_DOWN) {
// Down arrow
if (current < count - 1) {

View file

@ -79,6 +79,8 @@
#define RX_BUF_SLACK 64
// Receive buffer must hold at least one max-size stuffed frame
#define RX_BUF_SIZE (MAX_STUFFED_SIZE + RX_BUF_SLACK)
// Scratch buffer size for draining the serial port in pktPoll
#define RX_READ_CHUNK 128
// Retransmit timeout. 500ms is conservative for a local serial link (RTT
// is < 1ms) but accounts for the remote side being busy. On a real BBS
@ -107,16 +109,18 @@
// on NAK or timeout without the caller keeping its buffer alive. The timer
// tracks when this slot was last (re)transmitted for timeout detection.
typedef struct {
uint8_t data[PKT_MAX_PAYLOAD];
// Fields are ordered by alignment to minimize struct padding.
int len;
uint8_t seq;
clock_t timer;
uint8_t seq;
uint8_t data[PKT_MAX_PAYLOAD];
} TxSlotT;
// Connection state. txSlots is a circular window indexed by [0..txCount-1],
// Connection state. txSlots is a sliding window indexed by [0..txCount-1],
// where slot 0 is the oldest unacked frame (sequence txAckSeq) and
// slot txCount-1 is the newest. When an ACK advances txAckSeq, we shift
// slots down (implicitly, by incrementing txAckSeq and decrementing txCount).
// slot txCount-1 is the newest. When an ACK advances txAckSeq, the
// surviving slots are shifted down with memmove so slot i always holds
// sequence txAckSeq + i.
struct PktConnS {
int com;
int windowSize;
@ -152,19 +156,22 @@ static PktConnT *sOpenConns[RS232_NUM_PORTS];
// CRC-16-CCITT lookup table
// ========================================================================
// Regenerated from the polynomial (previous table had 42 corrupted
// entries, breaking the guaranteed error-detection properties). To
// verify: table[i ^ j] == table[i] ^ table[j] must hold for all i, j.
static const uint16_t sCrcTable[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x54A5,
0xA54A, 0xB56B, 0x8508, 0x9529, 0xE5CE, 0xF5EF, 0xC58C, 0xD5AD,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
0x4864, 0x5845, 0x6826, 0x7807, 0x08E0, 0x18C1, 0x28A2, 0x38A3,
0xC94C, 0xD96D, 0xE90E, 0xF92F, 0x89C8, 0x99E9, 0xA98A, 0xB9AB,
0x5A55, 0x4A74, 0x7A17, 0x6A36, 0x1AD1, 0x0AF0, 0x3A93, 0x2AB2,
0xDB5D, 0xCB7C, 0xFB1F, 0xEB3E, 0x9BD9, 0x8BF8, 0xBB9B, 0xAB9A,
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
@ -176,7 +183,7 @@ static const uint16_t sCrcTable[256] = {
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
0x26D3, 0x36F2, 0x06B1, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
@ -192,6 +199,7 @@ static const uint16_t sCrcTable[256] = {
// Prototypes (alphabetical)
// ========================================================================
static int ackAdvance(PktConnT *conn, uint8_t seq);
static uint16_t crcCalc(const uint8_t *data, int len);
bool pktCanSend(PktConnT *conn);
void pktClose(PktConnT *conn);
@ -200,9 +208,9 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, voi
int pktPoll(PktConnT *conn);
int pktReset(PktConnT *conn);
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
static void processFrame(PktConnT *conn, const uint8_t *frame, int len);
static bool processFrame(PktConnT *conn, const uint8_t *frame, int len);
static void retransmitCheck(PktConnT *conn);
static void rxProcessByte(PktConnT *conn, uint8_t byte);
static bool rxProcessByte(PktConnT *conn, uint8_t byte);
static void sendAck(PktConnT *conn, uint8_t seq);
static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len);
static void sendNak(PktConnT *conn, uint8_t seq);
@ -211,6 +219,20 @@ static int seqInWindow(uint8_t seq, uint8_t base, int size);
static int txSlotIndex(PktConnT *conn, uint8_t seq);
// Returns how many outstanding slots a cumulative ACK frees, or -1 if the
// ACK's sequence number is outside the live send window. Wrap-safe via
// unsigned subtraction (txNextSeq - txAckSeq == txCount).
static int ackAdvance(PktConnT *conn, uint8_t seq) {
uint8_t diff = seq - conn->txAckSeq;
if (diff > (uint8_t)conn->txCount) {
return -1;
}
return diff;
}
// 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
// costs 512 bytes of .rodata -- a worthwhile trade for a function called
@ -304,7 +326,7 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call
// complete, verified, in-order data frame, so the caller should be prepared
// for re-entrant calls to pktSend from within the callback.
int pktPoll(PktConnT *conn) {
char buf[128];
char buf[RX_READ_CHUNK];
int nRead;
int delivered = 0;
@ -315,9 +337,7 @@ int pktPoll(PktConnT *conn) {
// Read available serial data and feed to state machine
while ((nRead = rs232Read(conn->com, buf, sizeof(buf))) > 0) {
for (int i = 0; i < nRead; i++) {
uint8_t prevExpect = conn->rxExpectSeq;
rxProcessByte(conn, (uint8_t)buf[i]);
if (conn->rxExpectSeq != prevExpect) {
if (rxProcessByte(conn, (uint8_t)buf[i])) {
delivered++;
}
}
@ -409,15 +429,16 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
//
// For NAK frames: the receiver wants us to retransmit from a specific
// sequence. Go-Back-N retransmits that frame AND all subsequent ones.
static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
uint8_t seq;
uint8_t type;
int payloadLen;
uint16_t rxCrc;
uint16_t calcCrc;
bool delivered = false;
if (len < MIN_FRAME_SIZE) {
return;
return false;
}
// Verify CRC
@ -426,7 +447,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
if (calcCrc != rxCrc) {
// CRC mismatch -- request retransmit of what we expect
sendNak(conn, conn->rxExpectSeq);
return;
return false;
}
seq = frame[0];
@ -435,7 +456,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
// Validate payload length against actual frame size
if (payloadLen + MIN_FRAME_SIZE != len) {
return;
return false;
}
switch (type) {
@ -447,19 +468,34 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
}
conn->rxExpectSeq++;
sendAck(conn, conn->rxExpectSeq);
delivered = true;
} else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) {
// Out of order but in window -- NAK the one we want
sendNak(conn, conn->rxExpectSeq);
} else {
// Duplicate (already delivered): the sender retransmitted
// because our ACK was lost. Re-send the cumulative ACK so
// its window can advance -- silently discarding here would
// stall the sender forever.
sendAck(conn, conn->rxExpectSeq);
}
// else: duplicate or out of window, silently discard
break;
case FRAME_ACK: {
// ACK carries the next expected sequence number (cumulative)
// Advance txAckSeq and free all slots up to seq
while (conn->txCount > 0 && conn->txAckSeq != seq) {
conn->txAckSeq++;
conn->txCount--;
// ACK carries the next expected sequence number (cumulative).
// Free exactly the acknowledged slots; ignore out-of-window ACKs
// (n < 0) and no-progress duplicates (n == 0). The surviving
// slots must be shifted down so slot i again holds sequence
// txAckSeq + i -- txSlotIndex, the NAK loop, and pktSend all
// depend on that invariant. Without the shift, a partial ACK
// misaligns the window and the next send overwrites the
// retransmit copy of an in-flight frame.
int n = ackAdvance(conn, seq);
if (n > 0) {
conn->txAckSeq = (uint8_t)(conn->txAckSeq + n);
conn->txCount -= n;
memmove(&conn->txSlots[0], &conn->txSlots[n], (size_t)conn->txCount * sizeof(TxSlotT));
}
break;
}
@ -487,6 +523,8 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
sendRst(conn);
break;
}
return delivered;
}
@ -515,7 +553,7 @@ static void retransmitCheck(PktConnT *conn) {
// byte, saving bandwidth. A frame is only processed if it meets the minimum
// size requirement (header + CRC), so spurious flags between frames are
// harmless (they just produce zero-length "frames" that are discarded).
static void rxProcessByte(PktConnT *conn, uint8_t byte) {
static bool rxProcessByte(PktConnT *conn, uint8_t byte) {
switch (conn->rxState) {
case RX_STATE_HUNT:
if (byte == FLAG_BYTE) {
@ -527,11 +565,15 @@ static void rxProcessByte(PktConnT *conn, uint8_t byte) {
case RX_STATE_ACTIVE:
if (byte == FLAG_BYTE) {
// End of frame (or start of next)
bool delivered = false;
if (conn->rxFrameLen >= MIN_FRAME_SIZE) {
processFrame(conn, conn->rxFrame, conn->rxFrameLen);
delivered = processFrame(conn, conn->rxFrame, conn->rxFrameLen);
}
// Reset for next frame
conn->rxFrameLen = 0;
return delivered;
} else if (byte == ESC_BYTE) {
conn->rxState = RX_STATE_ESCAPE;
} else {
@ -554,6 +596,8 @@ static void rxProcessByte(PktConnT *conn, uint8_t byte) {
}
break;
}
return false;
}

View file

@ -66,13 +66,8 @@
// Error codes
#define PKT_SUCCESS 0
#define PKT_ERR_INVALID_PORT -1
#define PKT_ERR_NOT_OPEN -2
#define PKT_ERR_WOULD_BLOCK -4
#define PKT_ERR_OVERFLOW -5
#define PKT_ERR_INVALID_PARAM -6
#define PKT_ERR_TX_FULL -7
#define PKT_ERR_NO_DATA -8
#define PKT_ERR_DISCONNECTED -9
// Callback for received packets

View file

@ -53,15 +53,12 @@
#define RX_BUFFER_MASK ((1U << RX_BUFFER_BITS) - 1)
#define TX_BUFFER_MASK ((1U << TX_BUFFER_BITS) - 1)
// Flow control watermarks (percentage of buffer)
// Flow control watermarks (percentage of buffer). Only the RX side has
// watermark consumers (RX_LOWATER/RX_HIWATER); the TX side has none.
#define RX_HIGH_PERCENT 80
#define RX_LOW_PERCENT 20
#define TX_HIGH_PERCENT 80
#define TX_LOW_PERCENT 20
#define RX_HIGH_WATER (RX_BUFFER_SIZE * RX_HIGH_PERCENT / 100UL)
#define RX_LOW_WATER (RX_BUFFER_SIZE * RX_LOW_PERCENT / 100UL)
#define TX_HIGH_WATER (TX_BUFFER_SIZE * TX_HIGH_PERCENT / 100UL)
#define TX_LOW_WATER (TX_BUFFER_SIZE * TX_LOW_PERCENT / 100UL)
// Ring buffer macros. These advance head/tail BEFORE accessing the slot
// (pre-increment style). The +1-before-mask pattern means slot 0 is never
@ -83,8 +80,6 @@
#define TX_EMPTY(C) ((C)->txHead == (C)->txTail)
#define TX_FULL(C) ((((C)->txHead + 1) & TX_BUFFER_MASK) == (C)->txTail)
#define TX_COUNT(C) (((C)->txHead - (C)->txTail) & TX_BUFFER_MASK)
#define TX_LOWATER(C) (TX_COUNT(C) < TX_LOW_WATER)
#define TX_HIWATER(C) (TX_COUNT(C) > TX_HIGH_WATER)
// XON/XOFF
#define XON 0x11
@ -123,6 +118,8 @@
#define PIC_SLAVE 0xA0
#define PIC_EOI 0x20
#define PIC_RR 0x02
#define PIC_MASTER_IRQ_MAX 7 // highest IRQ on the master 8259
#define SLAVE_IRQ_BASE 8 // IRQ8 is slave bit 0 (IRQ8..15 -> slave IMR bit 0..7)
#define PIC_WRITE_IMR(P, D) outportb((P) | 1, (D))
#define PIC_WRITE_OCW2(P, D) outportb((P), (D))
@ -131,19 +128,17 @@
#define PIC_END_IRQ(P) PIC_WRITE_OCW2((P), PIC_EOI)
#define PIC_ENABLE_IRQ(I) \
{ if ((I) <= 7) { PIC_WRITE_IMR(PIC_MASTER, PIC_READ_IMR(PIC_MASTER) & ~(1 << (I))); } \
else { PIC_WRITE_IMR(PIC_SLAVE, PIC_READ_IMR(PIC_SLAVE) & ~(1 << ((I) - 7))); } }
{ if ((I) <= PIC_MASTER_IRQ_MAX) { PIC_WRITE_IMR(PIC_MASTER, PIC_READ_IMR(PIC_MASTER) & ~(1 << (I))); } \
else { PIC_WRITE_IMR(PIC_SLAVE, PIC_READ_IMR(PIC_SLAVE) & ~(1 << ((I) - SLAVE_IRQ_BASE))); } }
#define PIC_DISABLE_IRQ(I) \
{ if ((I) <= 7) { PIC_WRITE_IMR(PIC_MASTER, PIC_READ_IMR(PIC_MASTER) | (1 << (I))); } \
else { PIC_WRITE_IMR(PIC_SLAVE, PIC_READ_IMR(PIC_SLAVE) | (1 << ((I) - 7))); } }
{ if ((I) <= PIC_MASTER_IRQ_MAX) { PIC_WRITE_IMR(PIC_MASTER, PIC_READ_IMR(PIC_MASTER) | (1 << (I))); } \
else { PIC_WRITE_IMR(PIC_SLAVE, PIC_READ_IMR(PIC_SLAVE) | (1 << ((I) - SLAVE_IRQ_BASE))); } }
// UART register offsets
#define UART_RX 0
#define UART_TX 0
#define UART_IER 1
#define UART_DLL 0 // divisor latch LSB (when DLAB=1)
#define UART_DLM 1 // divisor latch MSB (when DLAB=1)
#define UART_DLW 0 // divisor latch word (when DLAB=1)
#define UART_IIR 2
#define UART_FCR 2
@ -260,25 +255,6 @@
#define MSR_RI 0x40
#define MSR_DCD 0x80
// BPS divisors (1.8432 MHz crystal)
#define BPS_50 2304
#define BPS_75 1536
#define BPS_110 1047
#define BPS_150 768
#define BPS_300 384
#define BPS_600 192
#define BPS_1200 96
#define BPS_1800 64
#define BPS_2400 48
#define BPS_3800 32
#define BPS_4800 24
#define BPS_7200 16
#define BPS_9600 12
#define BPS_19200 6
#define BPS_38400 3
#define BPS_57600 2
#define BPS_115200 1
#define FIFO_SIZE 16
#define ISR_SIZE 2048
@ -313,6 +289,12 @@ typedef struct {
} Rs232StateT;
typedef struct {
int32_t bps;
uint16_t divisor;
} Rs232BpsMapT;
// ========================================================================
// Static globals
// ========================================================================
@ -329,12 +311,37 @@ static Rs232StateT sComPorts[COM_MAX + 1] = {
{ .port = 3, .defaultIrq = COM4_DEFAULT_IRQ, .irq = IRQ_NONE }
};
// BPS rate to divisor mapping (1.8432 MHz crystal)
static const Rs232BpsMapT sBpsMap[] = {
{ 115200, 1 },
{ 57600, 2 },
{ 38400, 3 },
{ 19200, 6 },
{ 9600, 12 },
{ 7200, 16 },
{ 4800, 24 },
{ 3600, 32 },
{ 2400, 48 },
{ 1800, 64 },
{ 1200, 96 },
{ 600, 192 },
{ 300, 384 },
{ 150, 768 },
{ 110, 1047 },
{ 75, 1536 },
{ 50, 2304 }
};
#define BPS_MAP_COUNT (sizeof(sBpsMap) / sizeof(sBpsMap[0]))
// ========================================================================
// Prototypes (alphabetical)
// ========================================================================
static int32_t bpsToDivisor(int32_t bps);
static void comGeneralIsr(void);
static int32_t divisorToBps(uint16_t divisor);
static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info);
static int dpmiLockMemory(void);
static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info);
@ -386,6 +393,30 @@ int rs232WriteBuf(int com, const char *data, int len);
// Interrupt service routine
// ========================================================================
static int32_t bpsToDivisor(int32_t bps) {
uint32_t i;
for (i = 0; i < BPS_MAP_COUNT; i++) {
if (sBpsMap[i].bps == bps) {
return sBpsMap[i].divisor;
}
}
return RS232_ERR_INVALID_BPS;
}
static int32_t divisorToBps(uint16_t divisor) {
uint32_t i;
for (i = 0; i < BPS_MAP_COUNT; i++) {
if (sBpsMap[i].divisor == divisor) {
return sBpsMap[i].bps;
}
}
return RS232_ERR_INVALID_BPS;
}
// Single shared ISR for all COM ports. ISR sharing is necessary because
// COM1/COM3 typically share IRQ4 and COM2/COM4 share IRQ3. Having one ISR
// that polls all ports avoids the complexity of per-IRQ handlers.
@ -649,9 +680,12 @@ static int findIrq(int comport) {
asm("STI");
// IRQs 0-2 are never valid serial lines (timer, keyboard, cascade), so a
// detected 0/1/2 is treated as detection failure; the caller then falls
// back to the port's default IRQ.
switch (irrM) {
case 0x01: return 0;
case 0x02: return 1;
case 0x01: return RS232_ERR_IRQ_NOT_FOUND;
case 0x02: return RS232_ERR_IRQ_NOT_FOUND;
case 0x04:
switch (irrS) {
case 0x01: return 8;
@ -662,7 +696,7 @@ static int findIrq(int comport) {
case 0x20: return 13;
case 0x40: return 14;
case 0x80: return 15;
default: return 2;
default: return RS232_ERR_IRQ_NOT_FOUND;
}
case 0x08: return 3;
case 0x10: return 4;
@ -696,7 +730,7 @@ static void freeIrq(int comport) {
// Clear the FIFO and RX registers
UART_WRITE_FCR(com, 0);
for (int i = 0; i < 14; i++) {
for (int i = 0; i < FIFO_DEFAULT_THRESHOLD; i++) {
UART_READ_DATA(com);
}
UART_READ_IIR(com);
@ -847,26 +881,7 @@ int32_t rs232GetBps(int com) {
return RS232_ERR_NOT_OPEN;
}
switch (UART_READ_BPS(port)) {
case BPS_115200: return 115200L;
case BPS_57600: return 57600L;
case BPS_38400: return 38400L;
case BPS_19200: return 19200L;
case BPS_9600: return 9600L;
case BPS_7200: return 7200L;
case BPS_4800: return 4800L;
case BPS_3800: return 3800L;
case BPS_2400: return 2400L;
case BPS_1800: return 1800L;
case BPS_1200: return 1200L;
case BPS_600: return 600L;
case BPS_300: return 300L;
case BPS_150: return 150L;
case BPS_110: return 110L;
case BPS_75: return 75L;
case BPS_50: return 50L;
}
return RS232_ERR_INVALID_BPS;
return divisorToBps((uint16_t)UART_READ_BPS(port));
}
@ -1139,6 +1154,11 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
// This is more reliable than hardcoding 0x3F8/0x2F8 because BIOS setup
// may have remapped ports or detected them in a different order.
if (rs232SetBase(com, _farpeekw(_dos_ds, BIOS_COM_DATA_AREA + (com << 1))) != RS232_SUCCESS) {
// Clear the open flag set above so a later open isn't rejected with
// ALREADY_OPEN against a port that never finished opening. No IRQ is
// hooked yet, so rs232Close (which would write to the bad base) is
// not needed.
port->isOpen = 0;
return RS232_ERR_NO_UART;
}
@ -1153,6 +1173,8 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
rc = rs232SetIrq(com, rc);
if (rc != RS232_SUCCESS) {
// Same as above: no IRQ hooked here either, so just clear the flag.
port->isOpen = 0;
return rc;
}
@ -1168,11 +1190,11 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
// Read initial status
UART_READ_LSR(port);
UART_READ_MSR(port);
}
// Enable interrupts
// Enable interrupts only after a successful configuration
UART_WRITE_IER(port, IER_DATA_READY | IER_MODEM_STATUS | IER_ERRORS);
PIC_ENABLE_IRQ(port->irq);
}
if (rc != RS232_SUCCESS) {
rs232Close(com);
@ -1246,7 +1268,7 @@ int rs232SetBase(int com, int base) {
if (com < COM_MIN || com > COM_MAX) {
return RS232_ERR_INVALID_PORT;
}
if (base < 1 || base > 0xFFF) {
if (base < 1 || base > UART_MAX_BASE) {
return RS232_ERR_INVALID_BASE;
}
@ -1257,6 +1279,7 @@ int rs232SetBase(int com, int base) {
int rs232SetBps(int com, int32_t bps) {
Rs232StateT *port = &sComPorts[com];
int32_t divisor = bpsToDivisor(bps);
if (com < COM_MIN || com > COM_MAX) {
return RS232_ERR_INVALID_PORT;
@ -1264,27 +1287,12 @@ int rs232SetBps(int com, int32_t bps) {
if (!port->isOpen) {
return RS232_ERR_NOT_OPEN;
}
switch (bps) {
case 115200L: UART_WRITE_BPS(port, BPS_115200); return RS232_SUCCESS;
case 57600L: UART_WRITE_BPS(port, BPS_57600); return RS232_SUCCESS;
case 38400L: UART_WRITE_BPS(port, BPS_38400); return RS232_SUCCESS;
case 19200L: UART_WRITE_BPS(port, BPS_19200); return RS232_SUCCESS;
case 9600L: UART_WRITE_BPS(port, BPS_9600); return RS232_SUCCESS;
case 7200L: UART_WRITE_BPS(port, BPS_7200); return RS232_SUCCESS;
case 4800L: UART_WRITE_BPS(port, BPS_4800); return RS232_SUCCESS;
case 3800L: UART_WRITE_BPS(port, BPS_3800); return RS232_SUCCESS;
case 2400L: UART_WRITE_BPS(port, BPS_2400); return RS232_SUCCESS;
case 1800L: UART_WRITE_BPS(port, BPS_1800); return RS232_SUCCESS;
case 1200L: UART_WRITE_BPS(port, BPS_1200); return RS232_SUCCESS;
case 600L: UART_WRITE_BPS(port, BPS_600); return RS232_SUCCESS;
case 300L: UART_WRITE_BPS(port, BPS_300); return RS232_SUCCESS;
case 150L: UART_WRITE_BPS(port, BPS_150); return RS232_SUCCESS;
case 110L: UART_WRITE_BPS(port, BPS_110); return RS232_SUCCESS;
case 75L: UART_WRITE_BPS(port, BPS_75); return RS232_SUCCESS;
case 50L: UART_WRITE_BPS(port, BPS_50); return RS232_SUCCESS;
if (divisor < 0) {
return divisor;
}
return RS232_ERR_INVALID_BPS;
UART_WRITE_BPS(port, (uint16_t)divisor);
return RS232_SUCCESS;
}
@ -1359,7 +1367,7 @@ int rs232SetFifoThreshold(int com, int threshold) {
}
// Clear any garbage in UART FIFO
for (int i = 0; i < 16; i++) {
for (int i = 0; i < FIFO_SIZE; i++) {
UART_READ_DATA(port);
}
UART_READ_LSR(port);

View file

@ -59,6 +59,7 @@
#define STATE_INIT 0
#define STATE_HANDSHAKE 1
#define STATE_READY 2
#define STATE_ERROR 3
#define TX_KEY_XOR 0xAA
#define RX_KEY_XOR 0x55
@ -137,6 +138,23 @@ static void completeHandshake(SecLinkT *link) {
secDhDestroy(link->dh);
link->dh = 0;
// If either cipher failed to allocate, abort rather than entering READY
// with a NULL cipher (which would silently transmit cleartext).
if (!link->txCipher || !link->rxCipher) {
if (link->txCipher) {
secCipherDestroy(link->txCipher);
link->txCipher = 0;
}
if (link->rxCipher) {
secCipherDestroy(link->rxCipher);
link->rxCipher = 0;
}
link->state = STATE_ERROR;
return;
}
link->state = STATE_READY;
}
@ -234,7 +252,12 @@ int secLinkHandshake(SecLinkT *link) {
return SECLINK_ERR_PARAM;
}
// Generate DH keypair
// Generate DH keypair (idempotent: free any context left by a prior call)
if (link->dh) {
secDhDestroy(link->dh);
link->dh = 0;
}
link->dh = secDhCreate();
if (!link->dh) {
return SECLINK_ERR_ALLOC;
@ -242,6 +265,8 @@ int secLinkHandshake(SecLinkT *link) {
rc = secDhGenerateKeys(link->dh);
if (rc != SEC_SUCCESS) {
secDhDestroy(link->dh);
link->dh = 0;
return SECLINK_ERR_HANDSHAKE;
}
@ -255,16 +280,26 @@ int secLinkHandshake(SecLinkT *link) {
rc = pktSend(link->pkt, link->myPub, SEC_DH_KEY_SIZE, true);
if (rc != PKT_SUCCESS) {
secDhDestroy(link->dh);
link->dh = 0;
return SECLINK_ERR_HANDSHAKE;
}
// Poll until the callback completes the handshake
while (link->state != STATE_READY) {
// Poll until the callback completes the handshake (or signals an error)
while (link->state != STATE_READY && link->state != STATE_ERROR) {
if (pktPoll(link->pkt) == PKT_ERR_DISCONNECTED) {
secDhDestroy(link->dh);
link->dh = 0;
return SECLINK_ERR_HANDSHAKE;
}
}
// completeHandshake sets STATE_ERROR (and frees the DH context) when a
// cipher could not be created; surface that as a handshake failure.
if (link->state == STATE_ERROR) {
return SECLINK_ERR_HANDSHAKE;
}
return SECLINK_SUCCESS;
}
@ -351,14 +386,14 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
}
// Copy payload after header
memcpy(buf + 1, data, len);
memcpy(buf + SECLINK_CHAN_HDR_SIZE, data, len);
// Encrypt the payload portion only (not the header)
if (encrypt) {
secCipherCrypt(link->txCipher, buf + 1, len);
secCipherCrypt(link->txCipher, buf + SECLINK_CHAN_HDR_SIZE, len);
}
rc = pktSend(link->pkt, buf, len + 1, block);
rc = pktSend(link->pkt, buf, len + SECLINK_CHAN_HDR_SIZE, block);
if (rc != PKT_SUCCESS) {
return SECLINK_ERR_SEND;
}
@ -374,7 +409,6 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
// expects the entire buffer to be sent when this returns.
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) {
int offset = 0;
int rc;
if (!link || !data) {
return SECLINK_ERR_PARAM;
@ -385,6 +419,8 @@ int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel
while (offset < len) {
int chunk = len - offset;
int rc;
if (chunk > SECLINK_MAX_PAYLOAD) {
chunk = SECLINK_MAX_PAYLOAD;
}

View file

@ -52,6 +52,7 @@
#include <stdint.h>
#include <stdbool.h>
#include "../packet/packet.h"
// Error codes
#define SECLINK_SUCCESS 0
@ -62,8 +63,11 @@
#define SECLINK_ERR_NOT_READY -5
#define SECLINK_ERR_SEND -6
// Max plaintext payload per send (packet max minus 1-byte channel header)
#define SECLINK_MAX_PAYLOAD 254
// Channel header is one byte: bit 7 = encrypted flag, bits 6..0 = channel
#define SECLINK_CHAN_HDR_SIZE 1
// Max plaintext payload per send (packet max minus the channel header)
#define SECLINK_MAX_PAYLOAD (PKT_MAX_PAYLOAD - SECLINK_CHAN_HDR_SIZE)
// Channel limits. MAX_CHANNEL is the highest valid channel index;
// NUM_CHANNELS is the array size needed to hold one slot per channel.

View file

@ -46,6 +46,9 @@
#define BN_BITS 1024
#define BN_WORDS (BN_BITS / 32)
#define BN_BYTES (BN_BITS / 8)
// DH wire size (SEC_DH_KEY_SIZE) and bignum byte width (BN_BYTES) must agree;
// they describe the same 1024-bit value and are used interchangeably.
typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1];
// R^2 mod m initialization uses repeated doubling (shift-left-by-1 + mod).
// Starting from 1, 2*BN_BITS doublings take us to 2^(2*BN_BITS) mod m = R^2.
@ -56,6 +59,19 @@
#define XTEA_ROUNDS 32
#define XTEA_DELTA 0x9E3779B9
#define XTEA_BLOCK_SIZE 8 // XTEA operates on 8-byte (two-uint32_t) blocks
// Newton iterations to get 32 correct bits of -m0^(-1) (1->2->4->8->16->32)
#define M0INV_NEWTON_ITERS 5
// DRBG seed warmup: bytes generated and discarded after seeding
#define DRBG_DISCARD_BYTES 64
// Hardware entropy sources for the DRBG seed
#define PIT_CMD_PORT 0x43 // PIT mode/command register
#define PIT_CH0_PORT 0x40 // PIT channel 0 data port
#define PIT_LATCH_CMD 0x00 // Latch counter command for channel 0
#define BIOS_TICK_ADDR 0x46C // BIOS tick count linear address (18.2 Hz)
// ========================================================================
@ -76,7 +92,6 @@ struct SecDhS {
struct SecCipherS {
uint32_t key[4];
uint32_t nonce[2];
uint32_t counter[2];
};
@ -91,13 +106,17 @@ typedef struct {
// Static globals
// ========================================================================
// RFC 2409 Group 2 (1024-bit MODP) prime, little-endian word order
// RFC 2409 Group 2 (1024-bit MODP) prime, little-endian word order.
// Verified against the RFC: the value is a safe prime (p and (p-1)/2
// both prime). A previous transcription of this constant had corrupted
// low words, making the modulus composite (factors 127 and 63463) and
// the discrete log breakable via Pohlig-Hellman; do not hand-edit.
static const BigNumT sDhPrime = { .w = {
0x39E38FAF, 0xCDB1CEDC, 0x51FF5DB8, 0x85E28A20,
0x1E9C284F, 0x2BB72AE0, 0x60F89D81, 0x4E664FD5,
0x45E6F3A1, 0x92F2129E, 0xB8E51B21, 0x35C7D431,
0x14A0C959, 0x137E2179, 0x5BE0CD19, 0x7A51F1D7,
0xF25F1468, 0x302B0A6D, 0xCD3A431B, 0xEF9519B3,
0xFFFFFFFF, 0xFFFFFFFF, 0xECE65381, 0x49286651,
0x7C4B1FE6, 0xAE9F2411, 0x5A899FA5, 0xEE386BFB,
0xF406B7ED, 0x0BFF5CB6, 0xA637ED6B, 0xF44C42E9,
0x625E7EC6, 0xE485B576, 0x6D51C245, 0x4FE1356D,
0xF25F1437, 0x302B0A6D, 0xCD3A431B, 0xEF9519B3,
0x8E3404DD, 0x514A0879, 0x3B139B22, 0x020BBEA6,
0x8A67CC74, 0x29024E08, 0x80DC1CD1, 0xC4C6628B,
0x2168C234, 0xC90FDAA2, 0xFFFFFFFF, 0xFFFFFFFF
@ -141,7 +160,6 @@ static void dhInit(void);
SecCipherT *secCipherCreate(const uint8_t *key);
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
void secCipherDestroy(SecCipherT *c);
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
SecDhT *secDhCreate(void);
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
@ -153,6 +171,7 @@ void secRngBytes(uint8_t *buf, int len);
int secRngGatherEntropy(uint8_t *buf, int len);
void secRngSeed(const uint8_t *entropy, int len);
static void secureZero(void *ptr, int len);
static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int len, bool xorMode);
static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]);
@ -281,14 +300,15 @@ static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp, c
// 'mod' sequentially in the inner loops.
static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b, const BigNumT *mod, uint32_t m0inv) {
uint32_t t[BN_WORDS + 1];
memset(t, 0, sizeof(t));
for (int i = 0; i < BN_WORDS; i++) {
uint32_t u;
uint64_t carry;
uint64_t prod;
uint64_t sum;
memset(t, 0, sizeof(t));
for (int i = 0; i < BN_WORDS; i++) {
// Step 1: t += a[i] * b
carry = 0;
for (int j = 0; j < BN_WORDS; j++) {
@ -378,7 +398,7 @@ static void bnToBytes(uint8_t *buf, const BigNumT *a) {
static uint32_t computeM0Inv(uint32_t m0) {
uint32_t x = 1;
for (int i = 0; i < 5; i++) {
for (int i = 0; i < M0INV_NEWTON_ITERS; i++) {
x = x * (2 - m0 * x);
}
@ -441,39 +461,11 @@ SecCipherT *secCipherCreate(const uint8_t *key) {
// with separate counters, and that the same counter value is never reused
// with the same key (which would be catastrophic for CTR mode security).
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) {
uint32_t block[2];
uint8_t *keystream;
int pos;
int take;
if (!c || !data || len <= 0) {
return;
}
keystream = (uint8_t *)block;
pos = 0;
while (pos < len) {
// Encrypt counter to generate keystream
block[0] = c->counter[0];
block[1] = c->counter[1];
xteaEncryptBlock(block, c->key);
// XOR keystream with data
take = len - pos;
if (take > 8) {
take = 8;
}
for (int i = 0; i < take; i++) {
data[pos + i] ^= keystream[i];
}
pos += take;
// Increment counter
if (++c->counter[0] == 0) {
c->counter[1]++;
}
}
xteaCtrKeystream(c->counter, c->key, data, len, true);
}
@ -485,18 +477,6 @@ void secCipherDestroy(SecCipherT *c) {
}
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi) {
if (!c) {
return;
}
c->nonce[0] = nonceLo;
c->nonce[1] = nonceHi;
c->counter[0] = nonceLo;
c->counter[1] = nonceHi;
}
// Compute the shared secret from the remote side's public key.
// Validates that the remote key is in [2, p-2] to prevent small-subgroup
// attacks (keys of 0, 1, or p-1 would produce trivially guessable secrets).
@ -639,7 +619,7 @@ int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len) {
// the entire key state.
void secRngAddEntropy(const uint8_t *data, int len) {
for (int i = 0; i < len; i++) {
((uint8_t *)sRng.key)[i % 16] ^= data[i];
((uint8_t *)sRng.key)[i % SEC_XTEA_KEY_SIZE] ^= data[i];
}
// Re-mix: encrypt the key with itself
@ -664,31 +644,12 @@ void secRngAddEntropy(const uint8_t *data, int len) {
// entropy. Auto-seeds from hardware entropy on first use as a safety net.
void secRngBytes(uint8_t *buf, int len) {
if (!sRng.seeded) {
uint8_t entropy[16];
uint8_t entropy[SEC_XTEA_KEY_SIZE];
int got = secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, got);
}
uint32_t block[2];
int pos = 0;
while (pos < len) {
block[0] = sRng.counter[0];
block[1] = sRng.counter[1];
xteaEncryptBlock(block, sRng.key);
int take = len - pos;
if (take > 8) {
take = 8;
}
memcpy(buf + pos, block, take);
pos += take;
// Increment counter
if (++sRng.counter[0] == 0) {
sRng.counter[1]++;
}
}
xteaCtrKeystream(sRng.counter, sRng.key, buf, len, false);
}
@ -701,12 +662,12 @@ void secRngBytes(uint8_t *buf, int len) {
// when supplemented by user interaction timing.
int secRngGatherEntropy(uint8_t *buf, int len) {
int out = 0;
outportb(0x43, 0x00);
uint8_t pitLo = inportb(0x40);
uint8_t pitHi = inportb(0x40);
outportb(PIT_CMD_PORT, PIT_LATCH_CMD);
uint8_t pitLo = inportb(PIT_CH0_PORT);
uint8_t pitHi = inportb(PIT_CH0_PORT);
// BIOS tick count (18.2 Hz)
uint32_t ticks = _farpeekl(_dos_ds, 0x46C);
uint32_t ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
if (out < len) { buf[out++] = pitLo; }
if (out < len) { buf[out++] = pitHi; }
@ -716,9 +677,9 @@ int secRngGatherEntropy(uint8_t *buf, int len) {
if (out < len) { buf[out++] = (uint8_t)(ticks >> 24); }
// Second PIT reading for jitter
outportb(0x43, 0x00);
pitLo = inportb(0x40);
pitHi = inportb(0x40);
outportb(PIT_CMD_PORT, PIT_LATCH_CMD);
pitLo = inportb(PIT_CH0_PORT);
pitHi = inportb(PIT_CH0_PORT);
if (out < len) { buf[out++] = pitLo; }
if (out < len) { buf[out++] = pitHi; }
@ -734,16 +695,15 @@ void secRngSeed(const uint8_t *entropy, int len) {
// XOR-fold entropy into the key
for (int i = 0; i < len; i++) {
((uint8_t *)sRng.key)[i % 16] ^= entropy[i];
((uint8_t *)sRng.key)[i % SEC_XTEA_KEY_SIZE] ^= entropy[i];
}
// Derive counter from key bits
sRng.counter[0] = sRng.key[2] ^ sRng.key[0];
sRng.counter[1] = sRng.key[3] ^ sRng.key[1];
sRng.seeded = true;
// Mix state by generating and discarding 64 bytes
uint8_t discard[64];
uint8_t discard[DRBG_DISCARD_BYTES];
sRng.seeded = true; // prevent recursion in secRngBytes
secRngBytes(discard, sizeof(discard));
secureZero(discard, sizeof(discard));
@ -762,6 +722,46 @@ static void secureZero(void *ptr, int len) {
}
// Generates XTEA-CTR keystream into out for len bytes, advancing the 64-bit
// counter in place. When xorMode is true the keystream is XOR'd into out
// (cipher); when false it is copied (DRBG output). Shared by secCipherCrypt
// and secRngBytes so the counter/keystream logic lives in one place.
static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int len, bool xorMode) {
uint32_t block[2];
int pos = 0;
while (pos < len) {
int take;
block[0] = counter[0];
block[1] = counter[1];
xteaEncryptBlock(block, key);
take = len - pos;
if (take > XTEA_BLOCK_SIZE) {
take = XTEA_BLOCK_SIZE;
}
if (xorMode) {
const uint8_t *keystream = (const uint8_t *)block;
for (int i = 0; i < take; i++) {
out[pos + i] ^= keystream[i];
}
} else {
memcpy(out + pos, block, take);
}
pos += take;
if (++counter[0] == 0) {
counter[1]++;
}
}
}
// 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
// shifts, adds, and XORs -- no S-boxes, no lookup tables, no key schedule.

View file

@ -61,7 +61,6 @@
#define SEC_SUCCESS 0
#define SEC_ERR_PARAM -1
#define SEC_ERR_NOT_READY -2
#define SEC_ERR_ALLOC -3
// Opaque types
typedef struct SecDhS SecDhT;
@ -91,6 +90,5 @@ int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
SecCipherT *secCipherCreate(const uint8_t *key);
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
void secCipherDestroy(SecCipherT *c);
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
#endif

View file

@ -323,13 +323,8 @@ Header: packet/packet.h
Constant Value Description
-------- ----- -----------
PKT_SUCCESS 0 Operation succeeded.
PKT_ERR_INVALID_PORT -1 Invalid COM port index.
PKT_ERR_NOT_OPEN -2 Connection is not open.
PKT_ERR_WOULD_BLOCK -4 Operation would block.
PKT_ERR_OVERFLOW -5 Buffer overflow.
PKT_ERR_INVALID_PARAM -6 Invalid parameter (NULL handle, bad data pointer, length out of range).
PKT_ERR_TX_FULL -7 Transmit window is full (non-blocking send only).
PKT_ERR_NO_DATA -8 No data available.
PKT_ERR_DISCONNECTED -9 Serial port returned a negative read (disconnect/error).
.endtable
@ -473,7 +468,6 @@ Header: security/security.h
SEC_SUCCESS 0 Operation succeeded.
SEC_ERR_PARAM -1 Invalid parameter.
SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).
SEC_ERR_ALLOC -3 Memory allocation failed.
.endtable
.h2 RNG Functions
@ -529,8 +523,6 @@ XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keyst
.code
SecCipherT *secCipherCreate(const uint8_t *key);
void secCipherSetNonce(SecCipherT *c,
uint32_t nonceLo, uint32_t nonceHi);
void secCipherCrypt(SecCipherT *c,
uint8_t *data, int len);
void secCipherDestroy(SecCipherT *c);
@ -540,7 +532,6 @@ void secCipherDestroy(SecCipherT *c);
Function Description
-------- -----------
secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
secCipherSetNonce Set the initial counter value (nonce). Must be unique per session. Both the stored nonce and the running counter are set to (nonceLo, nonceHi).
secCipherCrypt Encrypt or decrypt data in place. The counter increments by one every 8 bytes (one XTEA block).
secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.
.endtable

View file

@ -48,7 +48,6 @@ typedef struct {
sqlite3_stmt *stmt;
int32_t dbHandle; // which database owns this cursor
bool eof; // true after sqlite3_step returns SQLITE_DONE
bool started; // true after first dvxSqlNext call
} CursorEntryT;
// Dynamically grown handle tables. sDbsCount/sCursorsCount track allocated slots.
@ -289,12 +288,21 @@ bool dvxSqlNext(int32_t rs) {
}
int rc = sqlite3_step(cur->stmt);
cur->started = true;
if (rc == SQLITE_ROW) {
return true;
}
// Anything other than SQLITE_DONE is a real step error, not clean EOF;
// record it so callers don't mistake a failure for end-of-results.
if (rc != SQLITE_DONE) {
DbEntryT *db = getDb(cur->dbHandle);
if (db) {
snprintf(db->lastError, sizeof(db->lastError), "%s", sqlite3_errmsg(db->db));
}
}
cur->eof = true;
return false;
}
@ -393,7 +401,6 @@ int32_t dvxSqlQuery(int32_t db, const char *sql) {
sCursors[slot].stmt = stmt;
sCursors[slot].dbHandle = db;
sCursors[slot].eof = false;
sCursors[slot].started = false;
return slot + 1; // 1-based handle
}

View file

@ -74,6 +74,7 @@ static void updateStatusText(void);
// Per-row string storage for the list view (must outlive each refresh cycle)
typedef struct {
char name[SHELL_APP_NAME_MAX];
char title[MAX_TITLE_LEN];
char file[64];
char type[12];
@ -205,15 +206,17 @@ static void refreshTaskList(void) {
// Pass 1: collect all row data. Must finish before building cell
// pointers because arrput may reallocate, invalidating earlier
// pointers into the array.
static int32_t *appIds = NULL;
arrsetlen(appIds, 0);
for (int32_t i = 1; i < shellAppSlotCount(); i++) {
ShellAppT *app = shellGetApp(i);
if (app && app->state == AppStateRunningE) {
TmRowStringsT row = {0};
// Copy the name into stable per-row storage too: the raw
// app->name points into the reallocatable sApps array and would
// dangle once another app load grows it.
snprintf(row.name, sizeof(row.name), "%s", app->name);
for (int32_t w = 0; w < sCtx->stack.count; w++) {
WindowT *win = sCtx->stack.windows[w];
@ -225,7 +228,7 @@ static void refreshTaskList(void) {
char *sep = platformPathDirEnd(app->path);
const char *fname = sep ? sep + 1 : app->path;
snprintf(row.file, sizeof(row.file), "%.63s", fname);
snprintf(row.file, sizeof(row.file), "%s", fname);
snprintf(row.type, sizeof(row.type), "%s", app->hasMainLoop ? "Task" : "Callback");
uint32_t memKb = dvxMemGetAppUsage(app->appId) / 1024;
@ -237,7 +240,6 @@ static void refreshTaskList(void) {
}
arrput(sRowStrs, row);
arrput(appIds, i);
}
}
@ -245,9 +247,7 @@ static void refreshTaskList(void) {
int32_t rowCount = arrlen(sRowStrs);
for (int32_t r = 0; r < rowCount; r++) {
ShellAppT *app = shellGetApp(appIds[r]);
arrput(sCells, app->name);
arrput(sCells, sRowStrs[r].name);
arrput(sCells, sRowStrs[r].title);
arrput(sCells, sRowStrs[r].file);
arrput(sCells, sRowStrs[r].type);

View file

@ -49,6 +49,10 @@ static clock_t sCursorBlinkTime = 0;
// always initialize with textEditLineCacheInit, but guard defensively).
#define TEXT_EDIT_DEFAULT_TAB_WIDTH 3
// Maximum number of leading whitespace bytes copied when auto-indenting
// a new line; auto-indent silently truncates past this.
#define TEXT_EDIT_MAX_AUTO_INDENT 64
// Track the widget that last had an active selection so we can
// clear it in O(1) instead of walking every widget in every window.
static WidgetT *sLastSelectedWidget = NULL;
@ -58,7 +62,7 @@ static WidgetT *sLastSelectedWidget = NULL;
// ============================================================
void clearOtherSelections(WidgetT *except);
static bool clearSelectionOnWidget(WidgetT *w);
void textEditClearWidgetRef(WidgetT *w);
bool isWordChar(char c);
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
@ -82,6 +86,7 @@ int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_
int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len);
void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col);
int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col);
void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
@ -110,8 +115,9 @@ int32_t wordStart(const char *buf, int32_t pos);
// from the newly-focused one). Validates that the previous widget's
// window is still in the window stack before touching it -- the
// window may have been closed since sLastSelectedWidget was set.
// If the previous widget was in a different window, that window
// gets a full repaint to clear the stale selection highlight.
// If a selection was cleared, the previous widget is invalidated so
// the stale highlight is erased -- the deferred paint flush handles
// both same-window and cross-window widgets.
void clearOtherSelections(WidgetT *except) {
if (!except || !except->window || !except->window->widgetRoot) {
return;
@ -150,22 +156,28 @@ void clearOtherSelections(WidgetT *except) {
return;
}
if (clearSelectionOnWidget(prev) && prevWin != except->window) {
dvxInvalidateWindow(ctx, prevWin);
if (wclsClearSelection(prev)) {
wgtInvalidatePaint(prev);
}
}
static bool clearSelectionOnWidget(WidgetT *w) {
return wclsClearSelection(w);
}
bool isWordChar(char c) {
return isalnum((unsigned char)c) || c == '_';
}
// Nulls the last-selected tracker when its widget is being destroyed.
// Registered with libdvx's sWidgetDestroyFn hook so the static can never
// dangle past a wgtDestroy (this lives in a different TU than
// widgetClearReferences, which cannot reach this static directly).
void textEditClearWidgetRef(WidgetT *w) {
if (sLastSelectedWidget == w) {
sLastSelectedWidget = NULL;
}
}
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) {
int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd;
int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart;
@ -256,8 +268,17 @@ bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, cons
}
int32_t cursorByte = textEditRowColToOff(lc, buf, len, *pCursorRow, *pCursorCol);
int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
int32_t searchLen = len - needleLen + 1; // >= 1 given the needleLen > len check above
int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
// Backward: a match at pos requires pos + needleLen <= len, so the
// highest valid start is searchLen - 1. Without this clamp, a cursor
// within the last (needleLen - 1) bytes starts past every valid
// position and the loop breaks before testing anything.
if (!forward && startPos > searchLen - 1) {
startPos = searchLen - 1;
}
int32_t count = forward ? (searchLen - startPos) : (startPos + 1);
if (count <= 0) {
@ -507,10 +528,11 @@ void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int3
lc->lineVisLens[line] = vc;
if (vc > lc->cachedMaxLL) {
// cachedMaxLL == -1 is the "unknown/needs-recompute" sentinel. An insert
// can only grow the edited line's visual length, so only grow a KNOWN max
// and never clobber the sentinel with a single line's length.
if (lc->cachedMaxLL >= 0 && vc > lc->cachedMaxLL) {
lc->cachedMaxLL = vc;
} else {
lc->cachedMaxLL = -1;
}
}
@ -531,13 +553,28 @@ static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, in
if (needed > lc->lineOffsetCap) {
int32_t newCap = needed + TEXT_EDIT_LINE_CAP_GROWTH;
lc->lineOffsets = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t));
int32_t *tmp = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t));
if (!tmp) {
// Leave the cache marked invalid so a later call retries.
lc->cachedLines = -1;
return;
}
lc->lineOffsets = tmp;
lc->lineOffsetCap = newCap;
}
if (lineCount > lc->lineVisLenCap) {
int32_t newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH;
lc->lineVisLens = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t));
int32_t *tmp2 = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t));
if (!tmp2) {
lc->cachedLines = -1;
return;
}
lc->lineVisLens = tmp2;
lc->lineVisLenCap = newCap;
}
@ -779,6 +816,9 @@ int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, i
if (count > 0) {
*pSelAnchor = -1;
*pSelCursor = -1;
// Dirty the cache BEFORE converting the cursor: replaceAll changed the
// buffer, so the row/col conversions below must rebuild from it.
textEditLineCacheDirty(lc);
int32_t cursorOff = textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol);
@ -787,13 +827,84 @@ int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, i
}
*pDesiredCol = *pCursorCol;
textEditLineCacheDirty(lc);
}
return count;
}
// textEditReplaceSelectionMulti -- if the current selection exactly
// matches needle, splice replacement in its place, record a single
// undo snapshot, update cursor/selection/length, and dirty the line
// cache. Mirrors textEditReplaceAll's undo/cursor/line-cache policy.
// After a successful replace the cursor is left at the end of the
// inserted text when searching forward, or at its start when
// searching backward, so a subsequent textEditFindNext continues
// past the replacement in the chosen direction. Returns true only
// if a replacement was performed.
bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) {
if (!needle || !needle[0] || !replacement) {
return false;
}
if (*pSelAnchor < 0 || *pSelCursor < 0 || *pSelAnchor == *pSelCursor) {
return false;
}
int32_t selStart = *pSelAnchor < *pSelCursor ? *pSelAnchor : *pSelCursor;
int32_t selEnd = *pSelAnchor < *pSelCursor ? *pSelCursor : *pSelAnchor;
int32_t selLen = selEnd - selStart;
int32_t needleLen = (int32_t)strlen(needle);
if (selLen != needleLen) {
return false;
}
if (selStart < 0 || selEnd > *pLen) {
return false;
}
bool match;
if (caseSensitive) {
match = (memcmp(buf + selStart, needle, needleLen) == 0);
} else {
match = (strncasecmp(buf + selStart, needle, needleLen) == 0);
}
if (!match) {
return false;
}
int32_t replLen = (int32_t)strlen(replacement);
int32_t delta = replLen - needleLen;
if (*pLen + delta >= bufSize) {
return false;
}
textEditSaveUndo(buf, *pLen, textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol), undoBuf, pUndoLen, pUndoCursor, bufSize);
if (delta != 0) {
memmove(buf + selStart + replLen, buf + selStart + needleLen, *pLen - selStart - needleLen);
}
memcpy(buf + selStart, replacement, replLen);
*pLen += delta;
buf[*pLen] = '\0';
*pSelAnchor = -1;
*pSelCursor = -1;
int32_t cursorOff = forward ? selStart + replLen : selStart;
textEditLineCacheDirty(lc);
textEditOffToRowCol(lc, buf, *pLen, cursorOff, pCursorRow, pCursorCol);
*pDesiredCol = *pCursorCol;
return true;
}
int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col) {
int32_t start = textEditLineStart(lc, buf, len, row);
int32_t lineL = textEditLineLen(lc, buf, len, row);
@ -880,6 +991,7 @@ int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart,
static void textHelpInit(void) {
sCursorBlinkFn = wgtUpdateCursorBlink;
sWidgetDestroyFn = textEditClearWidgetRef;
}
@ -1181,6 +1293,10 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
memmove(buf + off + paste, buf + off, *pLen - off + 1);
memcpy(buf + off, clip, paste);
*pLen += paste;
// Buffer geometry changed; rebuild the line cache before
// converting the post-paste offset, otherwise a multi-line
// paste maps to a stale row/col.
textEditLineCacheDirty(lc);
FROM_OFF(off + paste);
*pDesiredCol = *pCol;
}
@ -1227,10 +1343,15 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
// Ctrl+Z -- undo (single-slot swap; gives you one level of redo)
if (key == KEY_CTRL_Z) {
if (undoBuf && *pUndoLen >= 0) {
char tmpBuf[*pLen + 1];
// Heap scratch rather than a ~64KB stack VLA (buf can be the full
// editor buffer). Abort the undo cleanly if allocation fails,
// falling through to the unconditional tail below.
char *tmpBuf = (char *)malloc((size_t)*pLen + 1);
if (tmpBuf) {
int32_t tmpLen = *pLen;
int32_t tmpCursor = CUR_OFF();
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;
int32_t copyLen = tmpLen;
memcpy(tmpBuf, buf, copyLen);
tmpBuf[copyLen] = '\0';
@ -1243,10 +1364,11 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
memcpy(undoBuf, tmpBuf, saveLen);
undoBuf[saveLen] = '\0';
*pUndoLen = saveLen;
*pUndoCursor = tmpCursor;
free(tmpBuf);
// Compute the restore offset from the snapshot cursor (still in
// *pUndoCursor) BEFORE overwriting it with the redo cursor.
int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
*pUndoLen = saveLen;
*pUndoCursor = tmpCursor;
FROM_OFF(restoreOff);
*pDesiredCol = *pCol;
@ -1259,6 +1381,7 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
textEditLineCacheDirty(lc);
}
}
ENSURE_VIS();
wgtInvalidatePaint(w);
@ -1282,12 +1405,12 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
int32_t off = CUR_OFF();
int32_t indent = 0;
char indentBuf[64];
char indentBuf[TEXT_EDIT_MAX_AUTO_INDENT];
if (opts->autoIndent) {
int32_t lineStart = textEditLineStart(lc, buf, *pLen, *pRow);
while (lineStart + indent < off && indent < 63 && (buf[lineStart + indent] == ' ' || buf[lineStart + indent] == '\t')) {
while (lineStart + indent < off && indent < (int32_t)sizeof(indentBuf) - 1 && (buf[lineStart + indent] == ' ' || buf[lineStart + indent] == '\t')) {
indentBuf[indent] = buf[lineStart + indent];
indent++;
}
@ -1602,7 +1725,12 @@ navigation: {
*pDesiredCol = *pCol;
}
} else {
int32_t spaces = tabW - (*pCol % tabW);
// *pCol is a byte column; tab-stop math must use the
// visual column so cursors on lines containing literal
// tabs still land on a tab stop.
int32_t lineStart = textEditLineStart(lc, buf, *pLen, *pRow);
int32_t visCol = textEditVisualCol(buf, lineStart, off, tabW);
int32_t spaces = tabW - (visCol % tabW);
for (int32_t s = 0; s < spaces && *pLen < bufSize - 1; s++) {
memmove(buf + off + 1, buf + off, *pLen - off + 1);
@ -2024,8 +2152,10 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
}
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) {
// Printable character
if (undoBuf) {
// Printable character. Only snapshot undo when the edit will
// actually happen: a full buffer with no selection is a no-op, and
// saving undo then would discard the real prior state.
if (undoBuf && (hasSel || *pLen < bufSize - 1)) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
@ -2452,12 +2582,12 @@ int32_t wordBoundaryLeft(const char *buf, int32_t pos) {
}
// Skip non-word characters
while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') {
while (pos > 0 && !isWordChar(buf[pos - 1])) {
pos--;
}
// Skip word characters
while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) {
while (pos > 0 && isWordChar(buf[pos - 1])) {
pos--;
}
@ -2471,12 +2601,12 @@ int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
}
// Skip word characters
while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) {
while (pos < len && isWordChar(buf[pos])) {
pos++;
}
// Skip non-word characters
while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') {
while (pos < len && !isWordChar(buf[pos])) {
pos++;
}

View file

@ -45,6 +45,7 @@ void wgtUpdateCursorBlink(void);
// ============================================================
void clearOtherSelections(WidgetT *except);
void textEditClearWidgetRef(WidgetT *w);
// ============================================================
// Word / character helpers
@ -182,10 +183,11 @@ void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf,
// handler needs to know about. Avoids a 20-argument entry point.
typedef struct TextEditMultiOptionsT {
// Fields are ordered by alignment to minimize struct padding.
int32_t tabWidth;
bool autoIndent;
bool captureTabs;
bool useTabChar;
int32_t tabWidth;
bool readOnly;
} TextEditMultiOptionsT;
@ -298,6 +300,12 @@ int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t
void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor);
int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
// textEditReplaceSelectionMulti: if the current selection exactly
// matches needle, replace it (single undo snapshot) and leave the
// cursor positioned to continue searching in direction forward.
// Returns true only if a replacement was performed.
bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
// ============================================================
// Single-line text editing engine
// ============================================================

View file

@ -365,11 +365,7 @@ static void hcfForEachEntry(const char *hcfPath, void (*cb)(const char *key, con
char line[512];
while (fgets(line, (int)sizeof(line), f)) {
int32_t len = (int32_t)strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r' || line[len - 1] == ' ')) {
line[--len] = '\0';
}
dvxTrimRight(line);
if (line[0] == '#' || line[0] == '\0') {
continue;
@ -644,7 +640,14 @@ static void logAndReadDeps(ModuleT *mods) {
int32_t pos = snprintf(line, sizeof(line), " %s deps:", mods[i].baseName);
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
pos += snprintf(line + pos, sizeof(line) - pos, " %s", mods[i].deps[d]);
// snprintf returns the would-be length on truncation; without
// this guard pos can exceed sizeof(line) and make the size_t
// 'sizeof(line) - pos' underflow into a huge write.
if (pos < 0 || (size_t)pos >= sizeof(line)) {
break;
}
pos += snprintf(line + pos, sizeof(line) - (size_t)pos, " %s", mods[i].deps[d]);
}
dvxLog("%s", line);
@ -801,12 +804,21 @@ static void readDeps(ModuleT *mod) {
char line[64];
while (fgets(line, sizeof(line), f)) {
// Strip \r and \n
int32_t len = strlen(line);
// A physical line longer than the buffer is split across fgets
// calls. Drain the remainder so an over-long comment's tail (which
// does not start with '#') is not mistaken for a separate dep name.
// fgetc returns EOF immediately at end of file, so this is safe even
// for an unterminated final line.
if (strchr(line, '\n') == NULL) {
int32_t ch;
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
while ((ch = fgetc(f)) != '\n' && ch != EOF) {
// Discard the rest of the over-long physical line.
}
}
// Strip trailing whitespace (CR/LF/space/tab)
int32_t len = dvxTrimRight(line);
// Skip empty lines and comments
if (len == 0 || line[0] == '#') {
@ -923,6 +935,14 @@ static void validateDeps(const ModuleT *libs, const ModuleT *widgets) {
if (errors > 0) {
fprintf(stderr, "%d unresolved dependency reference(s); aborting.\n", (int)errors);
// Restore text mode first or the message prints invisibly under the
// graphics-mode splash.
if (sSplashActive) {
platformSplashShutdown();
sSplashActive = 0;
}
exit(1);
}
}
@ -969,6 +989,11 @@ int main(int argc, char *argv[]) {
void **handles = loadAllModules();
if (!handles || arrlen(handles) == 0) {
if (sSplashActive) {
platformSplashShutdown();
sSplashActive = 0;
}
fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR);
arrfree(handles);
return 1;
@ -984,6 +1009,11 @@ int main(int argc, char *argv[]) {
if (!shellMain) {
dvxLog("ERROR: No module exports shellMain");
if (sSplashActive) {
platformSplashShutdown();
sSplashActive = 0;
}
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
dlclose(handles[i]);
}

View file

@ -24,7 +24,10 @@
#
# Builds native (host) utilities and cross-compiled DOS tools.
CC = gcc
# Host compiler for native build-time utilities. Kept separate from CC so an
# analyze.sh / CI override of CC (to a cross/analyzer compiler) cannot
# cross-compile the host tools or contaminate bin/host.
HOSTCC = gcc
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-format-truncation -I../libs/kpunch/libdvx -I../libs/kpunch/libdvx/platform
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
@ -46,22 +49,22 @@ PLATFORM_UTIL = ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
STB_DS_IMPL = ../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
$(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTCC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ mkicon.c bmpDraw.c -lm
$(HOSTCC) $(CFLAGS) -o $@ mkicon.c bmpDraw.c -lm
$(HOSTDIR)/mktbicon: mktbicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ mktbicon.c bmpDraw.c
$(HOSTCC) $(CFLAGS) -o $@ mktbicon.c bmpDraw.c
$(HOSTDIR)/mkwgticon: mkwgticon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ mkwgticon.c bmpDraw.c
$(HOSTCC) $(CFLAGS) -o $@ mkwgticon.c bmpDraw.c
$(HOSTDIR)/bmp2raw: bmp2raw.c | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ bmp2raw.c
$(HOSTCC) $(CFLAGS) -o $@ bmp2raw.c
$(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTCC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTDIR):
mkdir -p $(HOSTDIR)

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