diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index 3b6432b..89627ba 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -1235,16 +1235,18 @@ name$ = "Hello" ' String ---------- -------- ----------- 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

Arithmetic Operators

  Operator   Description
   --------   -----------
@@ -2829,11 +2831,15 @@ End Sub
  Property     Type      Description
   ----------   -------   -------------------------------------------
   DataSource   String    Name of the Data control that supplies records.
-  GridLines    Boolean   Show or hide grid lines between cells (default: True).
+ GridLines Boolean Show or hide grid lines between cells (default: True). + SelectedRow Integer Index of the currently selected row (-1 = no selection).

Type-Specific Methods

-
  Method    Parameters   Description
-  -------   ----------   -------------------------------------------
-  Refresh   (none)       Reload and redraw the grid from the Data control.
+
  Method             Parameters                        Description
+  ----------------   -------------------------------   -------------------------------------------
+  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.

Type-Specific Events

  Event      Parameters   Description
   --------   ----------   -------------------------------------------
diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html
index 2505119..8e05319 100644
--- a/docs/dvx_system_reference.html
+++ b/docs/dvx_system_reference.html
@@ -579,11 +579,13 @@ img { max-width: 100%; }
 
  • shellReapApp
  • shellReapApps
  • shellRegisterDesktopUpdate
  • +
  • shellRegisterIdle
  • shellRunningAppCount
  • shellTaskMgrOpen
  • shellTaskMgrRefresh
  • shellTerminateAllApps
  • shellUnregisterDesktopUpdate
  • +
  • shellUnregisterIdle
  • Slider
  • Spacer
  • Spinner
  • @@ -783,9 +785,11 @@ img { max-width: 100%; }
  • wgtTabControlSetActive
  • wgtTabPage
  • wgtTextArea
  • +
  • wgtTextAreaCursorToEnd
  • wgtTextAreaFindNext
  • wgtTextAreaGoToLine
  • wgtTextAreaReplaceAll
  • +
  • wgtTextAreaReplaceSelection
  • wgtTextAreaSetAutoIndent
  • wgtTextAreaSetColorize
  • wgtTextAreaSetShowLineNumbers
  • @@ -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
    + SCROLLBAR_WIDTH = 16px

    Hit Test Regions

    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).

    Menu System

    @@ -1710,8 +1713,7 @@ prefsClose(h); 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 + CHROME_TOTAL_BOTTOM 6 Total inset from bottom of frame to content

    Hit Test Constants

      Define          Value   Description
       ------          -----   -----------
    @@ -2211,11 +2213,12 @@ prefsClose(h);
    win Window to add menu bar to

    Returns: Pointer to the new MenuBarT.

    wmDestroyMenuBar

    -
    void wmDestroyMenuBar(WindowT *win);
    -

    Free the menu bar and reclaim the content area.

    +
    void wmDestroyMenuBar(WindowT *win, const DisplayT *d);
    +

    Free the menu bar and reclaim the content area. The content buffer is reallocated to fit the grown content area.

      Parameter   Description
       ---------   -----------
    -  win         Window to remove menu bar from
    + win Window to remove menu bar from + d Display context; pass NULL only during window teardown

    wmAddMenu

    MenuT *wmAddMenu(MenuBarT *bar, const char *label);

    Append a dropdown menu to the menu bar. The label supports & accelerator markers (e.g. "&File").

    @@ -3875,7 +3878,6 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch 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)

    Core Widget Functions

    @@ -3928,9 +3930,6 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch

    clipboardGet

    const char *clipboardGet(int32_t *outLen);

    Retrieve clipboard text.

    -

    clipboardMaxLen

    -
    int32_t clipboardMaxLen(void);
    -

    Return the fixed capacity of the clipboard buffer in bytes.

    Scrollbar Helpers

    ScrollHitE

    Scrollbar hit-test result.

    @@ -4464,7 +4463,7 @@ shellConfigPath(ctx, "settings.ini", path, sizeof(path));

    Shell Event Hooks

      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.
    @@ -4474,6 +4473,12 @@ shellConfigPath(ctx, "settings.ini", path, sizeof(path));

    shellUnregisterDesktopUpdate

    void shellUnregisterDesktopUpdate(void (*updateFn)(void));

    Remove a previously registered callback. Call this before app shutdown to avoid dangling function pointers.

    +

    shellRegisterIdle

    +
    void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
    +

    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.

    +

    shellUnregisterIdle

    +
    void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
    +

    Remove a previously registered idle handler. No-op if the (fn, ctx) pair is absent, so a double-unregister is harmless.

    shellDesktopUpdate

    void shellDesktopUpdate(void);

    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.

    @@ -5241,13 +5246,8 @@ dvxSqlExec(db, sql);
      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).

    PktRecvCallbackT

    Callback type for received packets.

    @@ -5314,8 +5314,7 @@ dvxSqlExec(db, sql); -------- ----- ----------- 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. + SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).

    RNG Functions

    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.

    void secRngSeed(const uint8_t *entropy, int len);
    @@ -5349,15 +5348,12 @@ void    secDhDestroy(SecDhT *dh);

    XTEA-CTR Cipher

    XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.

    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);
      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.

    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 <= 0.

    @@ -6988,11 +6984,13 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced"); 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. + 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).

    API Struct (wgtRegisterApi "textinput")

    The combined "textinput" API exposes all four constructors plus the TextArea functions. The designer uses per-type APIs registered under the names "textbox" (single-line) and "textarea" (multi-line), each exposing only a create slot.

    Properties (BASIC Interface)

    diff --git a/src/apps/kpunch/clock/clock.c b/src/apps/kpunch/clock/clock.c index b0ef1ab..f184a8a 100644 --- a/src/apps/kpunch/clock/clock.c +++ b/src/apps/kpunch/clock/clock.c @@ -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; } diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c index 92bf13b..5bd09b5 100644 --- a/src/apps/kpunch/cpanel/cpanel.c +++ b/src/apps/kpunch/cpanel/cpanel.c @@ -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; } diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile index cf36ffa..48e594b 100644 --- a/src/apps/kpunch/dvxbasic/Makefile +++ b/src/apps/kpunch/dvxbasic/Makefile @@ -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) diff --git a/src/apps/kpunch/dvxbasic/README.md b/src/apps/kpunch/dvxbasic/README.md index f2b6a9f..d7e2742 100644 --- a/src/apps/kpunch/dvxbasic/README.md +++ b/src/apps/kpunch/dvxbasic/README.md @@ -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`. diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c index 4df26a6..d6d154b 100644 --- a/src/apps/kpunch/dvxbasic/basBuild.c +++ b/src/apps/kpunch/dvxbasic/basBuild.c @@ -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; } diff --git a/src/apps/kpunch/dvxbasic/basRes.h b/src/apps/kpunch/dvxbasic/basRes.h index 3d30ae8..f2e82dd 100644 --- a/src/apps/kpunch/dvxbasic/basRes.h +++ b/src/apps/kpunch/dvxbasic/basRes.h @@ -31,6 +31,18 @@ #ifndef BAS_RES_H #define BAS_RES_H +#include +#include // NULL +#include +#include // 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 " 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 diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h index 646bea0..3ffaf67 100644 --- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h +++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h @@ -32,8 +32,14 @@ #ifndef BAS_EVENTS_H #define BAS_EVENTS_H +#include + // 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 diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.c b/src/apps/kpunch/dvxbasic/compiler/codegen.c index 55659f2..309ef27 100644 --- a/src/apps/kpunch/dvxbasic/compiler/codegen.c +++ b/src/apps/kpunch/dvxbasic/compiler/codegen.c @@ -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,9 +162,10 @@ 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; } - - mod->formVarInfoCount = cg->formVarInfoCount; } // Copy debug variable info @@ -163,9 +174,8 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) { if (mod->debugVars) { memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT)); + mod->debugVarCount = cg->debugVarCount; } - - mod->debugVarCount = cg->debugVarCount; } // Copy UDT type definitions for debugger @@ -187,9 +197,9 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) { } } } - } - mod->debugUdtDefCount = cg->debugUdtDefCount; + 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)); } diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.h b/src/apps/kpunch/dvxbasic/compiler/codegen.h index 402bcb0..01c318d 100644 --- a/src/apps/kpunch/dvxbasic/compiler/codegen.h +++ b/src/apps/kpunch/dvxbasic/compiler/codegen.h @@ -37,6 +37,14 @@ #include #include +// 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(). diff --git a/src/apps/kpunch/dvxbasic/compiler/compact.c b/src/apps/kpunch/dvxbasic/compiler/compact.c index c8c9f26..b0ec3f4 100644 --- a/src/apps/kpunch/dvxbasic/compiler/compact.c +++ b/src/apps/kpunch/dvxbasic/compiler/compact.c @@ -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 #include +#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; } diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.c b/src/apps/kpunch/dvxbasic/compiler/lexer.c index 29eff57..2afdaab 100644 --- a/src/apps/kpunch/dvxbasic/compiler/lexer.c +++ b/src/apps/kpunch/dvxbasic/compiler/lexer.c @@ -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'; @@ -645,8 +659,8 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) { maxDigit = 15; } - int32_t idx = 0; - int32_t value = 0; + int32_t idx = 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,11 +726,11 @@ 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; - lex->token.text[idx] = '\0'; - lex->token.textLen = idx; + 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 @@ -764,48 +776,56 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { static BasTokenTypeE tokenizeNumber(BasLexerT *lex) { - int32_t idx = 0; - bool hasDecimal = false; - bool hasExp = false; + int32_t idx = 0; + bool hasDecimal = false; + bool hasExp = false; // 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)); } - while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { - if (idx < BAS_MAX_TOKEN_LEN - 1) { - lex->token.text[idx++] = advance(lex); - } else { - advance(lex); + if (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { + hasExp = true; + + while (!atEnd(lex) && isdigit((unsigned char)peek(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; @@ -862,14 +876,18 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) { static BasTokenTypeE tokenizeString(BasLexerT *lex) { advance(lex); // skip opening quote - int32_t idx = 0; + 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; } diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.h b/src/apps/kpunch/dvxbasic/compiler/lexer.h index 6f875d7..144bb4d 100644 --- a/src/apps/kpunch/dvxbasic/compiler/lexer.h +++ b/src/apps/kpunch/dvxbasic/compiler/lexer.h @@ -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). diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c index 905295e..1321149 100644 --- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c +++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c @@ -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; } diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c index f4c38ed..2067f6b 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parser.c +++ b/src/apps/kpunch/dvxbasic/compiler/parser.c @@ -35,14 +35,6 @@ #include #include -// ============================================================ -// Forward jump list (for EXIT FOR / EXIT DO backpatching) -// ============================================================ - -typedef struct { - int32_t *patchAddr; // stb_ds dynamic array -} ExitListT; - // ============================================================ // Built-in function table // ============================================================ @@ -115,15 +107,12 @@ static const BuiltinFuncT builtinFuncs[] = { {NULL, 0, 0, 0, 0} }; -// ============================================================ -// Exit list helpers -// ============================================================ - -static ExitListT exitForList; -static ExitListT exitDoList; -static ExitListT exitSubList; -static ExitListT exitFuncList; - +// Single source of truth for BASIC type-suffix characters. Both +// suffixToType (maps a trailing suffix to its BAS_TYPE_*) and +// nameHasTypeSuffix (reports whether a trailing suffix is present) +// are driven by this string so the two never disagree. +// '%' = INTEGER, '&' = LONG, '!' = SINGLE, '#' = DOUBLE, '$' = STRING +static const char suffixChars[] = "%&!#$"; // ============================================================ // Module-scope declarations @@ -144,6 +133,7 @@ static bool check(BasParserT *p, BasTokenTypeE type); static bool checkCtrlArrayAccess(BasParserT *p); static bool checkKeyword(BasParserT *p, const char *kw); static bool checkKeywordText(const char *text, const char *kw); +static bool clampParamCount(BasParserT *p, int32_t paramCount); static void collectDebugGlobals(BasParserT *p); static void collectDebugLocals(BasParserT *p, int32_t procIndex); static void emitByRefArg(BasParserT *p); @@ -151,6 +141,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym); static int32_t emitJump(BasParserT *p, uint8_t opcode); static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName); static void emitLoad(BasParserT *p, BasSymbolT *sym); +static void emitSelectPops(BasParserT *p, int32_t count); static void emitStore(BasParserT *p, BasSymbolT *sym); static void emitUdtInit(BasParserT *p, int32_t udtTypeId); static BasSymbolT *ensureVariable(BasParserT *p, const char *name); @@ -165,6 +156,7 @@ static const BuiltinFuncT *findBuiltin(const char *name); static BasSymbolT *findTypeDef(BasParserT *p, const char *name); static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId); static bool match(BasParserT *p, BasTokenTypeE type); +static bool nameHasTypeSuffix(const char *name); static void parseAddExpr(BasParserT *p); static void parseAndExpr(BasParserT *p); static void parseAssignOrCall(BasParserT *p); @@ -195,6 +187,7 @@ static void parseFunction(BasParserT *p); static void parseGet(BasParserT *p); static void parseGosub(BasParserT *p); static void parseGoto(BasParserT *p); +static void parseIdivExpr(BasParserT *p); static void parseIf(BasParserT *p); static void parseImpExpr(BasParserT *p); static void parseInput(BasParserT *p); @@ -203,6 +196,7 @@ static void parseLineInput(BasParserT *p); static void parseMkDir(BasParserT *p); static void parseModule(BasParserT *p); static void prescanSignatures(BasParserT *p); +static void parseMulDivExpr(BasParserT *p); static void parseMulExpr(BasParserT *p); static void parseName(BasParserT *p); static void parseNotExpr(BasParserT *p); @@ -212,6 +206,7 @@ static void parseOpen(BasParserT *p); static void parseOption(BasParserT *p); static void parseOrExpr(BasParserT *p); static void parsePowExpr(BasParserT *p); +static void parsePowOperand(BasParserT *p); static void parsePrimary(BasParserT *p); static void parsePrint(BasParserT *p); static void parsePut(BasParserT *p); @@ -241,6 +236,7 @@ static void patchJump(BasParserT *p, int32_t addr); static void patchLabelRefs(BasParserT *p, BasSymbolT *sym); static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName); static uint8_t resolveTypeName(BasParserT *p); +static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta); static void skipNewlines(BasParserT *p); static uint8_t suffixToType(const char *name); @@ -302,6 +298,20 @@ static void advance(BasParserT *p) { bool basParse(BasParserT *p) { parseModule(p); + + // The code generator silently caps the constant pool at 16-bit + // indices; if it overflowed, surface it as a compile error so we + // never emit a module with truncated constant references. + if (p->cg.overflow) { + error(p, "Too many string constants (constant pool index exceeds 16 bits)"); + } + + // Jumps and call addresses are 16-bit; a larger module would silently + // miscompile, so reject it cleanly instead. + if (p->cg.codeLen > BAS_MAX_CODE_SIZE) { + error(p, "Module too large (code size exceeds 16-bit limit)"); + } + return !p->hasError; } @@ -322,16 +332,20 @@ BasModuleT *basParserBuildModule(BasParserT *p) { void basParserFree(BasParserT *p) { basCodeGenFree(&p->cg); - // Free per-symbol dynamic arrays, then the symbol struct itself - for (int32_t i = 0; i < p->sym.count; i++) { - arrfree(p->sym.symbols[i]->patchAddrs); - arrfree(p->sym.symbols[i]->fields); - free(p->sym.symbols[i]); - } + // Free all symbols and the symbol array. + basSymTabFree(&p->sym); - arrfree(p->sym.symbols); - p->sym.symbols = NULL; - p->sym.count = 0; + // EXIT-target backpatch lists are normally arrfree'd by exitListPatch at + // the close of each loop/proc; an error early-return can skip that, so + // release the top-level lists here too. + arrfree(p->exitForList.patchAddr); + arrfree(p->exitDoList.patchAddr); + arrfree(p->exitSubList.patchAddr); + arrfree(p->exitFuncList.patchAddr); + p->exitForList.patchAddr = NULL; + p->exitDoList.patchAddr = NULL; + p->exitSubList.patchAddr = NULL; + p->exitFuncList.patchAddr = NULL; } @@ -340,14 +354,6 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) { basLexerInit(&p->lex, source, sourceLen); basCodeGenInit(&p->cg); basSymTabInit(&p->sym); - p->hasError = false; - p->errorLine = 0; - p->error[0] = '\0'; - - exitListInit(&exitForList); - exitListInit(&exitDoList); - exitListInit(&exitSubList); - exitListInit(&exitFuncList); p->formInitJmpAddr = -1; p->formInitCodeStart = -1; @@ -439,6 +445,22 @@ static bool checkKeywordText(const char *text, const char *kw) { } +// Guard against declaring more than BAS_MAX_PARAMS parameters. The +// typed param arrays on a symbol are fixed at BAS_MAX_PARAMS, so a +// declaration may not exceed that bound. Returns true (and reports an +// error) when paramCount has reached the limit, signalling the caller +// to stop the parameter loop. +static bool clampParamCount(BasParserT *p, int32_t paramCount) { + if (paramCount >= BAS_MAX_PARAMS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many parameters (maximum is %d)", (int)BAS_MAX_PARAMS); + error(p, buf); + return true; + } + return false; +} + + // Snapshot global variables for the debugger at the end of compilation. static void collectDebugGlobals(BasParserT *p) { for (int32_t i = 0; i < p->sym.count; i++) { @@ -462,6 +484,10 @@ static void collectDebugGlobals(BasParserT *p) { snprintf(def.fields[f].name, BAS_MAX_PROC_NAME, "%s", s->fields[f].name); def.fields[f].dataType = s->fields[f].dataType; } + } else { + // Allocation failed: record the type name but no fields so + // the debugger never walks a NULL field array. + def.fieldCount = 0; } arrput(p->cg.debugUdtDefs, def); @@ -623,7 +649,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { if (minArgs == 0 && sym->paramCount > 0) { // No optional params declared -- all are required bool hasOptional = false; - for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < sym->paramCount; i++) { if (sym->paramOptional[i]) { hasOptional = true; break; @@ -666,6 +692,13 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { // External library function: emit OP_CALL_EXTERN if (sym->isExtern) { + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } + basEmit8(&p->cg, OP_CALL_EXTERN); basEmitU16(&p->cg, sym->externLibIdx); basEmitU16(&p->cg, sym->externFuncIdx); @@ -685,7 +718,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { basEmit8(&p->cg, baseSlot); // If not yet defined, record the address for backpatching - if (!sym->isDefined && true) { + if (!sym->isDefined) { arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -770,8 +803,19 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) { static void emitStore(BasParserT *p, BasSymbolT *sym) { - // Fixed-length string: pad/truncate before storing - if (sym->fixedLen > 0) { + // Storing into a CONST would emit OP_STORE_GLOBAL at its index 0 + // (CONSTs never allocate a slot), corrupting the first real global. + // Reject here so every store path (FOR/READ/INPUT/SWAP/MID$/GET/SET) + // is covered, not just the plain-assignment path. + if (sym->kind == SYM_CONST) { + error(p, "Cannot assign to a constant"); + return; + } + // Fixed-length string: pad/truncate before storing. Skip for arrays: + // OP_STR_FIXLEN would convert the array reference being stored at DIM + // time into a fixed-width string, destroying the array (per-element + // fixed length is not modelled, so the '* n' is simply ignored here). + if (sym->fixedLen > 0 && !sym->isArray) { basEmit8(&p->cg, OP_STR_FIXLEN); basEmitU16(&p->cg, (uint16_t)sym->fixedLen); } @@ -788,6 +832,16 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) { } +// Emit OP_POP `count` times. Used to discard the live SELECT CASE test +// values from the eval stack when EXIT/GOTO jumps out of one or more +// enclosing SELECT constructs (see selectDepth). +static void emitSelectPops(BasParserT *p, int32_t count) { + for (int32_t i = 0; i < count; i++) { + basEmit8(&p->cg, OP_POP); + } +} + + // emitUdtInit -- emit code to initialize nested UDT fields after a UDT // has been created and is on top of the stack. For each field that is // itself a UDT, we DUP the parent, allocate the child UDT, and store it @@ -847,7 +901,7 @@ static BasSymbolT *ensureVariable(BasParserT *p, const char *name) { // OPTION EXPLICIT: require explicit DIM if (p->optionExplicit) { - char buf[320]; + char buf[BAS_PARSE_ERR_SCRATCH]; snprintf(buf, sizeof(buf), "Variable not declared: %s (OPTION EXPLICIT is on)", name); error(p, buf); return NULL; @@ -857,14 +911,11 @@ static BasSymbolT *ensureVariable(BasParserT *p, const char *name) { // Use suffix type if present, otherwise defType for the first letter uint8_t dt = suffixToType(name); - if (dt == BAS_TYPE_SINGLE && name[0] != '\0') { - // suffixToType returns SINGLE as the default when no suffix. - // Check if defType overrides it. - char firstLetter = name[0]; - - if (firstLetter >= 'a' && firstLetter <= 'z') { - firstLetter -= 32; - } + if (dt == BAS_TYPE_SINGLE && !nameHasTypeSuffix(name) && name[0] != '\0') { + // suffixToType returns SINGLE both for an explicit '!' suffix and + // for no suffix at all. Only apply the DEFxxx override when there + // is NO explicit suffix; an explicit '!' must stay SINGLE. + char firstLetter = (char)toupper((unsigned char)name[0]); if (firstLetter >= 'A' && firstLetter <= 'Z') { uint8_t defDt = p->defType[firstLetter - 'A']; @@ -883,7 +934,7 @@ static BasSymbolT *ensureVariable(BasParserT *p, const char *name) { } sym->scope = SCOPE_GLOBAL; - sym->index = basSymTabAllocSlot(&p->sym); + sym->index = basSymTabAllocGlobalSlot(&p->sym); sym->isDefined = true; return sym; } @@ -1025,6 +1076,13 @@ static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId) { static bool match(BasParserT *p, BasTokenTypeE type) { + // Once an error is set, advance() is a no-op (the token is frozen), + // so a true return here would spin any 'while (match(...))' loop + // forever. Report no match so error unwinding terminates the loop. + if (p->hasError) { + return false; + } + if (p->lex.token.type == type) { advance(p); return true; @@ -1033,6 +1091,19 @@ static bool match(BasParserT *p, BasTokenTypeE type) { } +// Report whether a name carries an explicit BASIC type-suffix character +// (one of suffixChars). Lets callers distinguish an explicitly typed +// name like "a!" from a bare "a", which suffixToType cannot do alone +// because it returns BAS_TYPE_SINGLE for both. +static bool nameHasTypeSuffix(const char *name) { + int32_t len = (int32_t)strlen(name); + if (len == 0) { + return false; + } + return strchr(suffixChars, name[len - 1]) != NULL; +} + + static void parseAddExpr(BasParserT *p) { parseMulExpr(p); while (!p->hasError) { @@ -1311,7 +1382,7 @@ static void parseAssignOrCall(BasParserT *p) { // Parse arguments (space-separated, like VB) int32_t argc = 0; - while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) { if (argc > 0) { if (check(p, TOK_COMMA)) { advance(p); @@ -1321,6 +1392,13 @@ static void parseAssignOrCall(BasParserT *p) { argc++; } + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } + basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, OP_POP); // discard return value (statement form) @@ -1378,7 +1456,7 @@ static void parseAssignOrCall(BasParserT *p) { basEmitU16(&p->cg, methodNameIdx); int32_t argc = 0; - while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) { if (argc > 0 && check(p, TOK_COMMA)) { advance(p); } @@ -1386,6 +1464,13 @@ static void parseAssignOrCall(BasParserT *p) { argc++; } + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } + basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, OP_POP); // discard return value @@ -1510,7 +1595,7 @@ static void parseAssignOrCall(BasParserT *p) { int32_t minArgs = sym->requiredParams; bool hasOptional = false; - for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < sym->paramCount; i++) { if (sym->paramOptional[i]) { hasOptional = true; break; @@ -1553,6 +1638,13 @@ static void parseAssignOrCall(BasParserT *p) { // External library SUB: emit OP_CALL_EXTERN if (sym->isExtern) { + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } + basEmit8(&p->cg, OP_CALL_EXTERN); basEmitU16(&p->cg, sym->externLibIdx); basEmitU16(&p->cg, sym->externFuncIdx); @@ -1569,7 +1661,7 @@ static void parseAssignOrCall(BasParserT *p) { basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, baseSlot); - if (!sym->isDefined && true) { + if (!sym->isDefined) { arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -1913,6 +2005,10 @@ static void parseDeclare(BasParserT *p) { return; } + if (clampParamCount(p, paramCount)) { + return; + } + char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; @@ -1924,10 +2020,8 @@ static void parseDeclare(BasParserT *p) { pdt = resolveTypeName(p); } - if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; - } + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; paramCount++; } @@ -1947,7 +2041,8 @@ static void parseDeclare(BasParserT *p) { } // Add to symbol table as forward declaration - BasSymbolT *sym = basSymTabAdd(&p->sym, name, kind, returnType); + BasSymbolT *sym = basSymTabAdd(&p->sym, name, kind, returnType); + bool added = (sym != NULL); if (sym == NULL) { // Might already be declared -- look it up @@ -1959,12 +2054,16 @@ static void parseDeclare(BasParserT *p) { } } - sym->scope = SCOPE_GLOBAL; - sym->isDefined = false; - sym->codeAddr = 0; + sym->scope = SCOPE_GLOBAL; + + if (added || !sym->isDefined) { + sym->isDefined = false; + sym->codeAddr = 0; + } + sym->paramCount = paramCount; - for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < paramCount; i++) { sym->paramTypes[i] = paramTypes[i]; sym->paramByVal[i] = paramByVal[i]; } @@ -2048,7 +2147,7 @@ static void parseDeclareLibrary(BasParserT *p) { if (enLen > 0) { char last = externName[enLen - 1]; - if (last == '$' || last == '%' || last == '&' || last == '!' || last == '#') { + if (basIsTypeSuffixChar(last)) { externName[enLen - 1] = '\0'; } } @@ -2073,6 +2172,10 @@ static void parseDeclareLibrary(BasParserT *p) { return; } + if (clampParamCount(p, paramCount)) { + return; + } + char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; @@ -2084,10 +2187,8 @@ static void parseDeclareLibrary(BasParserT *p) { pdt = resolveTypeName(p); } - if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; - } + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; paramCount++; } @@ -2125,7 +2226,7 @@ static void parseDeclareLibrary(BasParserT *p) { sym->externFuncIdx = funcNameIdx; sym->paramCount = paramCount; - for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < paramCount; i++) { sym->paramTypes[i] = paramTypes[i]; sym->paramByVal[i] = paramByVal[i]; } @@ -2174,6 +2275,10 @@ static void parseDef(BasParserT *p) { return; } + if (clampParamCount(p, paramCount)) { + return; + } + char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; @@ -2198,10 +2303,8 @@ static void parseDef(BasParserT *p) { paramSym->isDefined = true; paramSym->udtTypeId = pUdtTypeId; - if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = true; - } + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = true; paramCount++; } expect(p, TOK_RPAREN); @@ -2211,7 +2314,11 @@ static void parseDef(BasParserT *p) { parseExpression(p); basEmit8(&p->cg, OP_RET_VAL); - collectDebugLocals(p, p->cg.debugProcCount++); + // Do NOT emit debug locals (or consume a debug proc index) for DEF FN. + // The proc table orders DEF FNs at its tail (symbol-table order), while + // debugProcCount runs in source order; consuming an index here would + // misattribute this DEF FN's -- and every later proc's -- debug locals. + // DEF FN params are tiny/transient, so skipping their debug info is safe. basSymTabLeaveLocal(&p->sym); uint8_t returnType = suffixToType(name); @@ -2229,7 +2336,7 @@ static void parseDef(BasParserT *p) { funcSym->isDefined = true; funcSym->paramCount = paramCount; funcSym->scope = SCOPE_GLOBAL; - for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < paramCount; i++) { funcSym->paramTypes[i] = paramTypes[i]; funcSym->paramByVal[i] = paramByVal[i]; } @@ -2252,11 +2359,7 @@ static void parseDefType(BasParserT *p, uint8_t dataType) { return; } - char startLetter = p->lex.token.text[0]; - - if (startLetter >= 'a' && startLetter <= 'z') { - startLetter -= 32; - } + char startLetter = (char)toupper((unsigned char)p->lex.token.text[0]); if (startLetter < 'A' || startLetter > 'Z') { error(p, "Expected letter A-Z"); @@ -2273,11 +2376,7 @@ static void parseDefType(BasParserT *p, uint8_t dataType) { return; } - endLetter = p->lex.token.text[0]; - - if (endLetter >= 'a' && endLetter <= 'z') { - endLetter -= 32; - } + endLetter = (char)toupper((unsigned char)p->lex.token.text[0]); if (endLetter < 'A' || endLetter > 'Z') { error(p, "Expected letter A-Z"); @@ -2359,9 +2458,23 @@ static void parseDim(BasParserT *p) { return; } - // Check for duplicate + // Determine the scope this DIM would create (local > form > global) + // so duplicate detection only rejects a name already declared in the + // SAME scope. basSymTabFind falls back to form/global from a SUB, so a + // plain isDefined check wrongly blocked a local DIM from shadowing a + // module-level variable (standard QB semantics, see dvx_basic_reference). + BasScopeE newScope; + if (p->sym.inLocalScope) { + newScope = SCOPE_LOCAL; + } else if (p->sym.inFormScope) { + newScope = SCOPE_FORM; + } else { + newScope = SCOPE_GLOBAL; + } + + // Check for duplicate in the same scope only BasSymbolT *existing = basSymTabFind(&p->sym, name); - if (existing != NULL && existing->isDefined) { + if (existing != NULL && existing->isDefined && existing->scope == newScope) { char buf[BAS_PARSE_ERR_SCRATCH]; snprintf(buf, sizeof(buf), "Variable '%s' already declared", name); error(p, buf); @@ -2380,13 +2493,7 @@ static void parseDim(BasParserT *p) { sym->udtTypeId = udtTypeId; sym->fixedLen = fixedLen; - if (p->sym.inLocalScope) { - sym->scope = SCOPE_LOCAL; - } else if (p->sym.inFormScope) { - sym->scope = SCOPE_FORM; - } else { - sym->scope = SCOPE_GLOBAL; - } + sym->scope = newScope; if (isArray) { if (dt == BAS_TYPE_UDT && udtTypeId >= 0) { @@ -2448,8 +2555,9 @@ static void parseDimBounds(BasParserT *p, int32_t *outDims) { } else { // Single value = ubound, lbound defaults to optionBase. // Ubound expression already emitted. Insert PUSH_INT16 before it. + int16_t lbound = (int16_t)p->optionBase; int32_t exprLen = basCodePos(&p->cg) - exprStart; - int32_t insertLen = 3; // OP_PUSH_INT16 + 2 bytes + int32_t insertLen = (int32_t)(1 + sizeof(lbound)); // opcode byte + int16 operand { // Grow the array to make room for the insertion @@ -2459,9 +2567,14 @@ static void parseDimBounds(BasParserT *p, int32_t *outDims) { memmove(&p->cg.code[exprStart + insertLen], &p->cg.code[exprStart], exprLen); p->cg.code[exprStart] = OP_PUSH_INT16; - int16_t lbound = (int16_t)p->optionBase; - memcpy(&p->cg.code[exprStart + 1], &lbound, 2); + memcpy(&p->cg.code[exprStart + 1], &lbound, sizeof(lbound)); p->cg.codeLen = (int32_t)arrlen(p->cg.code); + + // The ubound expression was just moved forward by insertLen. + // Any forward-CALL or label-ref patch address that landed + // inside it now points at stale bytes; shift them so they + // patch the relocated operand instead of corrupting code. + shiftBackpatchAddrs(p, exprStart, insertLen); } } @@ -2479,8 +2592,11 @@ static void parseDo(BasParserT *p) { // LOOP [WHILE|UNTIL cond] advance(p); // consume DO - ExitListT savedExitDo = exitDoList; - exitListInit(&exitDoList); + ExitListT savedExitDo = p->exitDoList; + exitListInit(&p->exitDoList); + + int32_t savedDoSelectBase = p->doSelectBase; + p->doSelectBase = p->selectDepth; int32_t loopTop = basCodePos(&p->cg); @@ -2543,8 +2659,9 @@ static void parseDo(BasParserT *p) { } // Patch all EXIT DO jumps to here - exitListPatch(&exitDoList, p); - exitDoList = savedExitDo; + exitListPatch(&p->exitDoList, p); + p->exitDoList = savedExitDo; + p->doSelectBase = savedDoSelectBase; } @@ -2583,17 +2700,16 @@ static void parseEndForm(BasParserT *p) { p->formInitJmpAddr = -1; p->formInitCodeStart = -1; - // Record form variable info (even if varCount is 0 but init code exists) - if (varCount > 0 || initAddr >= 0) { - BasFormVarInfoT info; - memset(&info, 0, sizeof(info)); - snprintf(info.formName, sizeof(info.formName), "%s", formName); - info.varCount = varCount; - info.initCodeAddr = initAddr; - info.initCodeLen = initLen; - arrput(p->cg.formVarInfo, info); - p->cg.formVarInfoCount = (int32_t)arrlen(p->cg.formVarInfo); - } + // Record form variable info (init code always exists once BEGINFORM ran, + // so initAddr >= 0 here -- the entry is recorded unconditionally). + BasFormVarInfoT info; + memset(&info, 0, sizeof(info)); + snprintf(info.formName, sizeof(info.formName), "%s", formName); + info.varCount = varCount; + info.initCodeAddr = initAddr; + info.initCodeLen = initLen; + arrput(p->cg.formVarInfo, info); + p->cg.formVarInfoCount = (int32_t)arrlen(p->cg.formVarInfo); } @@ -2638,21 +2754,28 @@ static void parseExit(BasParserT *p) { if (check(p, TOK_FOR)) { advance(p); + // Discard the test values of any SELECT CASE bodies this EXIT + // jumps out of, then drop the VM for-frame and jump. + emitSelectPops(p, p->selectDepth - p->forSelectBase); basEmit8(&p->cg, OP_FOR_POP); int32_t addr = emitJump(p, OP_JMP); - exitListAdd(&exitForList, addr); + exitListAdd(&p->exitForList, addr); } else if (check(p, TOK_DO)) { advance(p); + emitSelectPops(p, p->selectDepth - p->doSelectBase); int32_t addr = emitJump(p, OP_JMP); - exitListAdd(&exitDoList, addr); + exitListAdd(&p->exitDoList, addr); } else if (check(p, TOK_SUB)) { advance(p); + // Leaving the procedure entirely: pop every open SELECT value. + emitSelectPops(p, p->selectDepth); int32_t addr = emitJump(p, OP_JMP); - exitListAdd(&exitSubList, addr); + exitListAdd(&p->exitSubList, addr); } else if (check(p, TOK_FUNCTION)) { advance(p); + emitSelectPops(p, p->selectDepth); int32_t addr = emitJump(p, OP_JMP); - exitListAdd(&exitFuncList, addr); + exitListAdd(&p->exitFuncList, addr); } else { error(p, "Expected FOR, DO, SUB, or FUNCTION after EXIT"); } @@ -2680,8 +2803,13 @@ static void parseFor(BasParserT *p) { // NEXT [var] advance(p); // consume FOR - ExitListT savedExitFor = exitForList; - exitListInit(&exitForList); + ExitListT savedExitFor = p->exitForList; + exitListInit(&p->exitForList); + + // Record SELECT depth at loop entry so EXIT FOR knows how many + // enclosing SELECT test values to pop on the way out. + int32_t savedForSelectBase = p->forSelectBase; + p->forSelectBase = p->selectDepth; // Loop variable if (!check(p, TOK_IDENT)) { @@ -2763,8 +2891,9 @@ static void parseFor(BasParserT *p) { p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF); // Patch all EXIT FOR jumps to here - exitListPatch(&exitForList, p); - exitForList = savedExitFor; + exitListPatch(&p->exitForList, p); + p->exitForList = savedExitFor; + p->forSelectBase = savedForSelectBase; } @@ -2796,8 +2925,8 @@ static void parseFunction(BasParserT *p) { // Enter local scope basSymTabEnterLocal(&p->sym); - ExitListT savedExitFunc = exitFuncList; - exitListInit(&exitFuncList); + ExitListT savedExitFunc = p->exitFuncList; + exitListInit(&p->exitFuncList); // Allocate slot 0 for return value basSymTabAllocSlot(&p->sym); @@ -2835,6 +2964,10 @@ static void parseFunction(BasParserT *p) { return; } + if (clampParamCount(p, paramCount)) { + return; + } + char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; @@ -2859,11 +2992,9 @@ static void parseFunction(BasParserT *p) { paramSym->isDefined = true; paramSym->udtTypeId = pUdtTypeId; - if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; - paramOptional[paramCount] = optional; - } + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + paramOptional[paramCount] = optional; if (!optional) { requiredCount = paramCount + 1; @@ -2906,7 +3037,7 @@ static void parseFunction(BasParserT *p) { funcSym->paramCount = paramCount; funcSym->requiredParams = requiredCount; funcSym->scope = SCOPE_GLOBAL; - for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < paramCount; i++) { funcSym->paramTypes[i] = paramTypes[i]; funcSym->paramByVal[i] = paramByVal[i]; funcSym->paramOptional[i] = paramOptional[i]; @@ -2943,8 +3074,8 @@ static void parseFunction(BasParserT *p) { } // Patch EXIT FUNCTION jumps - exitListPatch(&exitFuncList, p); - exitFuncList = savedExitFunc; + exitListPatch(&p->exitFuncList, p); + p->exitFuncList = savedExitFunc; // Load return value from slot 0 and return basEmit8(&p->cg, OP_LOAD_LOCAL); @@ -3054,6 +3185,12 @@ static void parseGoto(BasParserT *p) { labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); + // A GOTO out of one or more SELECT CASE blocks must discard their + // live test values. Labels are resolved at the procedure level, so + // a GOTO target is treated as the proc base (selectDepth 0); pop all + // currently-open SELECT values before jumping. + emitSelectPops(p, p->selectDepth); + emitJumpToLabel(p, OP_JMP, labelName); } @@ -3075,18 +3212,27 @@ static void parseIf(BasParserT *p) { return; } - // Check for single-line IF: IF cond THEN stmt + // Check for single-line IF: IF cond THEN stmt [: stmt]... if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) { - // Single-line IF + // Single-line IF. QuickBASIC semantics: ALL colon-separated + // statements after THEN (and after ELSE) belong to that branch. + // parseStatement consumes a trailing colon, so loop until the + // branch terminator (ELSE/NEWLINE/EOF) is reached. int32_t falseJump = emitJump(p, OP_JMP_FALSE); - parseStatement(p); + while (!p->hasError && !check(p, TOK_ELSE) && !check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) { + parseStatement(p); + } if (check(p, TOK_ELSE)) { advance(p); int32_t endJump = emitJump(p, OP_JMP); patchJump(p, falseJump); - parseStatement(p); + + while (!p->hasError && !check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) { + parseStatement(p); + } + patchJump(p, endJump); } else { patchJump(p, falseJump); @@ -3456,6 +3602,13 @@ static void prescanSignatures(BasParserT *p) { break; } + // Prescan is best-effort: too-many-parameters is surfaced + // with full location info by the real parse pass, so here + // we simply stop scanning this signature's parameters. + if (clampParamCount(p, paramCount)) { + break; + } + char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; @@ -3475,11 +3628,9 @@ static void prescanSignatures(BasParserT *p) { } } - if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; - paramOptional[paramCount] = optional; - } + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + paramOptional[paramCount] = optional; if (!optional) { requiredCount = paramCount + 1; @@ -3520,7 +3671,7 @@ static void prescanSignatures(BasParserT *p) { sym->paramCount = paramCount; sym->requiredParams = requiredCount; - for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < paramCount; i++) { sym->paramTypes[i] = paramTypes[i]; sym->paramByVal[i] = paramByVal[i]; sym->paramOptional[i] = paramOptional[i]; @@ -3591,11 +3742,24 @@ static void parseModule(BasParserT *p) { } -// VB precedence (high to low): ^, unary -, *, /, \, MOD, +, - -// So parseMulExpr calls parseUnaryExpr, which calls parsePowExpr, -// which calls parsePrimary. This makes -2^2 = -(2^2) = -4. +// QuickBASIC precedence (high to low): ^, unary -, (* /), \, MOD, +, -. +// The multiplicative operators occupy THREE distinct levels, so the chain is +// parseAddExpr -> parseMulExpr (MOD) -> parseIdivExpr (\) -> +// parseMulDivExpr (* /) -> parseUnaryExpr -> parsePowExpr -> parsePrimary. +// This makes 8 MOD 3 * 2 == 2 and 10 \ 2 * 3 == 1 (matching QB), and keeps +// -2^2 = -(2^2) = -4. -static void parseMulExpr(BasParserT *p) { +static void parseIdivExpr(BasParserT *p) { + parseMulDivExpr(p); + while (!p->hasError && check(p, TOK_BACKSLASH)) { + advance(p); + parseMulDivExpr(p); + basEmit8(&p->cg, OP_IDIV_INT); + } +} + + +static void parseMulDivExpr(BasParserT *p) { parseUnaryExpr(p); while (!p->hasError) { if (check(p, TOK_STAR)) { @@ -3606,14 +3770,6 @@ static void parseMulExpr(BasParserT *p) { advance(p); parseUnaryExpr(p); basEmit8(&p->cg, OP_DIV_FLT); - } else if (check(p, TOK_BACKSLASH)) { - advance(p); - parseUnaryExpr(p); - basEmit8(&p->cg, OP_IDIV_INT); - } else if (check(p, TOK_MOD)) { - advance(p); - parseUnaryExpr(p); - basEmit8(&p->cg, OP_MOD_INT); } else { break; } @@ -3621,6 +3777,16 @@ static void parseMulExpr(BasParserT *p) { } +static void parseMulExpr(BasParserT *p) { + parseIdivExpr(p); + while (!p->hasError && check(p, TOK_MOD)) { + advance(p); + parseIdivExpr(p); + basEmit8(&p->cg, OP_MOD_INT); + } +} + + static void parseName(BasParserT *p) { // NAME oldname AS newname advance(p); @@ -3906,12 +4072,10 @@ static void parseOption(BasParserT *p) { advance(p); // consume COMPARE if (check(p, TOK_BINARY)) { - p->optionCompareText = false; advance(p); basEmit8(&p->cg, OP_COMPARE_MODE); basEmit8(&p->cg, 0); } else if (checkKeyword(p, "TEXT")) { - p->optionCompareText = true; advance(p); basEmit8(&p->cg, OP_COMPARE_MODE); basEmit8(&p->cg, 1); @@ -3946,12 +4110,31 @@ static void parsePowExpr(BasParserT *p) { parsePrimary(p); while (!p->hasError && check(p, TOK_CARET)) { advance(p); - parsePrimary(p); + // The exponent operand may carry a leading sign (QB: 2 ^ -3 == 0.125). + // A leading sign on the BASE is handled higher up by parseUnaryExpr, + // so only the RHS needs parsePowOperand. + parsePowOperand(p); basEmit8(&p->cg, OP_POW); } } +static void parsePowOperand(BasParserT *p) { + if (check(p, TOK_MINUS)) { + advance(p); + parsePowOperand(p); + basEmit8(&p->cg, OP_NEG_INT); + return; + } + if (check(p, TOK_PLUS)) { + advance(p); // unary plus is a no-op + parsePowOperand(p); + return; + } + parsePrimary(p); +} + + static void parsePrimary(BasParserT *p) { if (p->hasError) { return; @@ -4587,6 +4770,12 @@ static void parsePrimary(BasParserT *p) { } } expect(p, TOK_RPAREN); + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, (uint8_t)argc); } else { @@ -4633,6 +4822,13 @@ static void parsePrint(BasParserT *p) { bool trailingSep = false; for (;;) { + // Bail on error: parseExpression leaves the token frozen, and + // a stuck ';'/',' would otherwise re-satisfy the separator + // check below and spin (advance() is a no-op under error). + if (p->hasError) { + break; + } + // Duplicate the channel for this OP_FILE_PRINT. basEmit8(&p->cg, OP_DUP); parseExpression(p); @@ -4676,6 +4872,12 @@ static void parsePrint(BasParserT *p) { // Parse values, each one gets formatted with PRINT_USING for (;;) { + // Bail on error: a stuck ';' would re-satisfy the separator + // check below and spin (advance() is a no-op under error). + if (p->hasError) { + break; + } + parseExpression(p); basEmit8(&p->cg, OP_PRINT_USING); basEmit8(&p->cg, OP_PRINT); @@ -4691,6 +4893,10 @@ static void parsePrint(BasParserT *p) { break; } + // The format string pushed at the top of this branch is re-pushed by + // each OP_PRINT_USING and left on the stack after the final OP_PRINT; + // pop it so PRINT USING doesn't leak one eval slot per execution. + basEmit8(&p->cg, OP_POP); basEmit8(&p->cg, OP_PRINT_NL); return; } @@ -4947,6 +5153,10 @@ static void parseSelectCase(BasParserT *p) { // Evaluate the test expression -- stays on stack throughout parseExpression(p); + // The test value is now live on the eval stack for the whole + // construct; track it so EXIT/GOTO can pop it on the way out. + p->selectDepth++; + expectEndOfStatement(p); skipNewlines(p); @@ -4959,11 +5169,15 @@ static void parseSelectCase(BasParserT *p) { advance(p); if (check(p, TOK_SELECT)) { advance(p); - basEmit8(&p->cg, OP_POP); // pop test expression + // Patch the per-case end jumps to land ON the pop (not + // after it) so every matched-case body pops the test + // value exactly once, then fall through to the pop. for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { patchJump(p, endJumps[i]); } + basEmit8(&p->cg, OP_POP); // pop test expression arrfree(endJumps); + p->selectDepth--; return; } p->lex = savedLex; @@ -4972,6 +5186,7 @@ static void parseSelectCase(BasParserT *p) { if (!check(p, TOK_CASE)) { error(p, "Expected CASE or END SELECT"); arrfree(endJumps); + p->selectDepth--; return; } @@ -4990,11 +5205,12 @@ static void parseSelectCase(BasParserT *p) { advance(p); if (check(p, TOK_SELECT)) { advance(p); - basEmit8(&p->cg, OP_POP); for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { patchJump(p, endJumps[i]); } + basEmit8(&p->cg, OP_POP); arrfree(endJumps); + p->selectDepth--; return; } p->lex = savedLex; @@ -5038,6 +5254,7 @@ static void parseSelectCase(BasParserT *p) { error(p, "Expected comparison operator after IS"); arrfree(bodyJumps); arrfree(endJumps); + p->selectDepth--; return; } @@ -5101,12 +5318,15 @@ static void parseSelectCase(BasParserT *p) { advance(p); if (check(p, TOK_SELECT)) { advance(p); - basEmit8(&p->cg, OP_POP); + // Both the no-match fall-through (nextCaseJump) and + // earlier matched bodies (endJumps) land ON the pop. patchJump(p, nextCaseJump); for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { patchJump(p, endJumps[i]); } + basEmit8(&p->cg, OP_POP); arrfree(endJumps); + p->selectDepth--; return; } p->lex = savedLex; @@ -5123,15 +5343,17 @@ static void parseSelectCase(BasParserT *p) { } // Reached if EOF hit without END SELECT -- patch pending end jumps - // and clean up. - basEmit8(&p->cg, OP_POP); - + // and clean up. End jumps land on the pop, then fall through to it. for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) { patchJump(p, endJumps[i]); } + basEmit8(&p->cg, OP_POP); + arrfree(endJumps); + p->selectDepth--; + if (!p->hasError) { error(p, "Expected END SELECT"); } @@ -5459,7 +5681,32 @@ static void parseStatement(BasParserT *p) { if (check(p, TOK_LPAREN)) { emitFunctionCall(p, sym); } else { - // CALL with no arguments + // CALL with no arguments. For an already-defined target, + // verify it does not require arguments. Forward references + // (isDefined == false) have no param info yet, so they are + // not checked here -- this mirrors the arity logic in + // emitFunctionCall() and the bare-sub-call path. + int32_t minArgs = sym->requiredParams; + if (minArgs == 0 && sym->paramCount > 0) { + bool hasOptional = false; + for (int32_t i = 0; i < sym->paramCount; i++) { + if (sym->paramOptional[i]) { + hasOptional = true; + break; + } + } + if (!hasOptional) { + minArgs = sym->paramCount; + } + } + + if (sym->isDefined && minArgs > 0) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got 0", sym->name, (int)minArgs); + error(p, buf); + break; + } + uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; basEmit8(&p->cg, OP_CALL); int32_t addrPos = basCodePos(&p->cg); @@ -5467,7 +5714,7 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, 0); basEmit8(&p->cg, baseSlot); - if (!sym->isDefined && true) { + if (!sym->isDefined) { arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -5717,13 +5964,19 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_PUSH_STR); basEmitU16(&p->cg, methodIdx); int32_t argc = 0; - while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) { if (argc > 0 && check(p, TOK_COMMA)) { advance(p); } parseExpression(p); argc++; } + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, OP_POP); @@ -5744,13 +5997,19 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_PUSH_STR); basEmitU16(&p->cg, methodIdx); int32_t argc = 0; - while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) { if (argc > 0 && check(p, TOK_COMMA)) { advance(p); } parseExpression(p); argc++; } + if (argc > BAS_VM_MAX_CALL_ARGS) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS); + error(p, buf); + return; + } basEmit8(&p->cg, OP_CALL_METHOD); basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, OP_POP); @@ -5811,7 +6070,7 @@ static void parseStatement(BasParserT *p) { } // After the label, there may be a statement on the same line // which will be parsed on the next iteration - break; + return; } // Not a label -- restore and parse as assignment/call @@ -5870,6 +6129,15 @@ static void parseStatic(BasParserT *p) { return; } + // STATIC of a user-defined type is not supported: parseStatic neither + // records the UDT typeId nor emits the one-time allocation, so field + // access would fault at runtime. Reject it cleanly instead of silently + // mis-compiling. + if (dt == BAS_TYPE_UDT) { + error(p, "STATIC of a user type is not supported"); + return; + } + // Create a mangled global name: "procName$varName" // Truncation is intentional -- symbol names are clamped to BAS_MAX_SYMBOL_NAME. char mangledName[BAS_MAX_SYMBOL_NAME * 2 + 1]; @@ -5889,7 +6157,7 @@ static void parseStatic(BasParserT *p) { return; } globalSym->scope = SCOPE_GLOBAL; - globalSym->index = p->sym.nextGlobalIdx++; + globalSym->index = basSymTabAllocGlobalSlot(&p->sym); globalSym->isDefined = true; // Create a local alias that maps to this global's index @@ -5932,8 +6200,8 @@ static void parseSub(BasParserT *p) { // Enter local scope basSymTabEnterLocal(&p->sym); - ExitListT savedExitSub = exitSubList; - exitListInit(&exitSubList); + ExitListT savedExitSub = p->exitSubList; + exitListInit(&p->exitSubList); // Parse parameter list int32_t paramCount = 0; @@ -5968,6 +6236,10 @@ static void parseSub(BasParserT *p) { return; } + if (clampParamCount(p, paramCount)) { + return; + } + char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; @@ -5992,11 +6264,9 @@ static void parseSub(BasParserT *p) { paramSym->isDefined = true; paramSym->udtTypeId = pUdtTypeId; - if (paramCount < BAS_MAX_PARAMS) { - paramTypes[paramCount] = pdt; - paramByVal[paramCount] = byVal; - paramOptional[paramCount] = optional; - } + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + paramOptional[paramCount] = optional; if (!optional) { requiredCount = paramCount + 1; @@ -6030,7 +6300,7 @@ static void parseSub(BasParserT *p) { subSym->paramCount = paramCount; subSym->requiredParams = requiredCount; subSym->scope = SCOPE_GLOBAL; - for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + for (int32_t i = 0; i < paramCount; i++) { subSym->paramTypes[i] = paramTypes[i]; subSym->paramByVal[i] = paramByVal[i]; subSym->paramOptional[i] = paramOptional[i]; @@ -6068,8 +6338,8 @@ static void parseSub(BasParserT *p) { } // Patch EXIT SUB jumps - exitListPatch(&exitSubList, p); - exitSubList = savedExitSub; + exitListPatch(&p->exitSubList, p); + p->exitSubList = savedExitSub; basEmit8(&p->cg, OP_RET); @@ -6227,8 +6497,11 @@ static void parseWhile(BasParserT *p) { // WEND advance(p); // consume WHILE - ExitListT savedExitDo = exitDoList; - exitListInit(&exitDoList); + ExitListT savedExitDo = p->exitDoList; + exitListInit(&p->exitDoList); + + int32_t savedDoSelectBase = p->doSelectBase; + p->doSelectBase = p->selectDepth; int32_t loopTop = basCodePos(&p->cg); @@ -6258,8 +6531,9 @@ static void parseWhile(BasParserT *p) { patchJump(p, falseJump); // Patch EXIT DO jumps (WHILE/WEND uses the DO exit list) - exitListPatch(&exitDoList, p); - exitDoList = savedExitDo; + exitListPatch(&p->exitDoList, p); + p->exitDoList = savedExitDo; + p->doSelectBase = savedDoSelectBase; } @@ -6418,6 +6692,48 @@ static uint8_t resolveTypeName(BasParserT *p) { } +// Add delta to every recorded backpatch ADDRESS >= from. Used when +// emitted code is moved (e.g. parseDimBounds prepending a default +// lbound), so that pending forward-CALL and label-reference patch +// positions still point at the operand bytes after the shift. The +// operands themselves are computed at patch time from the (already +// shifted) stored address, so only the stored address needs adjusting. +static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta) { + // Per-symbol forward-CALL and label-reference patch addresses. + // Only the live patch set (patchCount) matters: entries already + // consumed by patchCallAddrs/patchLabelRefs leave patchCount at 0 + // and are never re-applied, so they need no shifting. + for (int32_t i = 0; i < p->sym.count; i++) { + BasSymbolT *s = p->sym.symbols[i]; + + for (int32_t j = 0; j < s->patchCount; j++) { + if (s->patchAddrs[j] >= from) { + s->patchAddrs[j] += delta; + } + } + } + + // Active EXIT FOR / EXIT DO / EXIT SUB / EXIT FUNCTION lists. + ExitListT *exitLists[] = {&p->exitForList, &p->exitDoList, &p->exitSubList, &p->exitFuncList}; + int32_t exitListCount = (int32_t)(sizeof(exitLists) / sizeof(exitLists[0])); + + for (int32_t i = 0; i < exitListCount; i++) { + int32_t n = (int32_t)arrlen(exitLists[i]->patchAddr); + + for (int32_t j = 0; j < n; j++) { + if (exitLists[i]->patchAddr[j] >= from) { + exitLists[i]->patchAddr[j] += delta; + } + } + } + + // Pending JMP over a form's init block. + if (p->formInitJmpAddr >= from) { + p->formInitJmpAddr += delta; + } +} + + static void skipNewlines(BasParserT *p) { while (!p->hasError && check(p, TOK_NEWLINE)) { advance(p); diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.h b/src/apps/kpunch/dvxbasic/compiler/parser.h index b958932..27ea9ce 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parser.h +++ b/src/apps/kpunch/dvxbasic/compiler/parser.h @@ -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; diff --git a/src/apps/kpunch/dvxbasic/compiler/strip.c b/src/apps/kpunch/dvxbasic/compiler/strip.c index 8ca8048..162e969 100644 --- a/src/apps/kpunch/dvxbasic/compiler/strip.c +++ b/src/apps/kpunch/dvxbasic/compiler/strip.c @@ -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; +} diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.c b/src/apps/kpunch/dvxbasic/compiler/symtab.c index 3d3b661..58e96d2 100644 --- a/src/apps/kpunch/dvxbasic/compiler/symtab.c +++ b/src/apps/kpunch/dvxbasic/compiler/symtab.c @@ -29,17 +29,26 @@ #include #include +// 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; diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.h b/src/apps/kpunch/dvxbasic/compiler/symtab.h index 7952eb9..b9731d6 100644 --- a/src/apps/kpunch/dvxbasic/compiler/symtab.h +++ b/src/apps/kpunch/dvxbasic/compiler/symtab.h @@ -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); diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c index e7b15d1..cd0ca79 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c @@ -65,6 +65,11 @@ #define MAX_EVENT_NAME_LEN 128 #define MENU_ID_BASE 10000 +// Work-buffer / item-count caps for the BASIC dialog wrappers. +#define BAS_CHOICE_MAX_ITEMS 64 +#define BAS_CHOICE_BUF_LEN 1024 +#define BAS_FILTER_BUF_LEN 1024 + // Module-level form runtime pointer for onFormClose callback static BasFormRtT *sFormRt = NULL; @@ -178,6 +183,18 @@ static ShellLoadAppFnT sShellLoadApp = NULL; static bool sShellLoadResolved = false; +// Shell idle-handler registry (resolved lazily; mirrors sShellLoadApp). +// The secLink/serial pollers register here instead of overwriting the +// context's single idleCallback slot -- the old scheme clobbered the +// shell's cooperative-yield handler and never restored it. +typedef void (*ShellIdleFnT)(void (*)(void *), void *); + +static ShellIdleFnT sShellRegisterIdle = NULL; + +static ShellIdleFnT sShellUnregisterIdle = NULL; + +static bool sShellIdleResolved = false; + // Maximum number of simultaneously open resource handles #define RES_MAX_HANDLES 8 @@ -238,6 +255,7 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName); BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName); void basFormRtHideForm(void *ctx, void *formRef); +static void basFormRtInitFormVars(BasFormRtT *rt, BasFormT *form, bool runInit); static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText); void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName); static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen); @@ -252,11 +270,15 @@ void basFormRtRunSimple(BasFormRtT *rt); void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName); void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); void basFormRtShowForm(void *ctx, void *formRef, bool modal); +static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form); void basFormRtUnloadForm(void *ctx, void *formRef); const char *basInputBox2(const char *title, const char *prompt, const char *defaultText); int32_t basInputCancelled(void); int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal); static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); +static uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult); +static bool menuContainsMenu(const MenuT *tree, const MenuT *target); +static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId); static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc); int32_t basPromptSave(const char *title); static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); @@ -279,7 +301,11 @@ static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len); WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData); +static WidgetT *findCtrlWidgetByName(const char *name); static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount); +static void formRtCommIdleSync(void); +static void formRtSerIdleSync(void); +static void freeControl(BasControlT *ctrl); static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name); static void frmLoad_onCtrlEnd(void *userData); static void frmLoad_onCtrlProp(void *userData, const char *key, const char *value); @@ -292,6 +318,7 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); int32_t HelpCompile(const char *inputFile, const char *outputFile); void HelpView(const char *hlpFile); +static bool ifaceHasProp(const WgtIfaceT *iface, const char *propName); static void onFormActivate(WindowT *win); static void onFormClose(WindowT *win); static void onFormDeactivate(WindowT *win); @@ -312,6 +339,7 @@ static void onWidgetScroll(WidgetT *w, int32_t delta); static bool onWidgetValidate(WidgetT *w); static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize); static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl); +static bool resolveShellIdle(void); const char *resolveTypeName(const char *typeName); int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); int32_t ResAddText(const char *path, const char *name, const char *text); @@ -324,6 +352,8 @@ int32_t ResOpen(const char *path); int32_t ResRemove(const char *path, const char *name); int32_t ResSize(int32_t handle, int32_t index); int32_t ResType(int32_t handle, int32_t index); +static void rtEventEnter(BasFormRtT *rt); +static void rtEventLeave(BasFormRtT *rt); void SerAttach(int32_t com, const char *termCtrlName); int32_t SerAvailable(int32_t com); void SerClose(int32_t com); @@ -358,9 +388,11 @@ void SQLFreeResult(int32_t rs); int32_t SQLNext(int32_t rs); int32_t SQLOpen(const char *path); int32_t SQLQuery(int32_t db, const char *sql); +static void unloadFormNow(BasFormRtT *rt, BasFormT *form); static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl); bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val); bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); +static void wireWidgetEvents(WidgetT *w, BasControlT *ctrl); static BasValueT zeroValue(void); int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx) { @@ -369,14 +401,14 @@ int32_t basChoiceDialog(const char *title, const char *prompt, const char *items } // Split pipe-delimited items string into an array - static char itemBuf[1024]; + static char itemBuf[BAS_CHOICE_BUF_LEN]; snprintf(itemBuf, sizeof(itemBuf), "%s", items); - const char *ptrs[64]; + const char *ptrs[BAS_CHOICE_MAX_ITEMS]; int32_t count = 0; char *tok = itemBuf; - while (*tok && count < 64) { + while (*tok && count < BAS_CHOICE_MAX_ITEMS) { ptrs[count++] = tok; char *sep = strchr(tok, '|'); @@ -407,12 +439,15 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha (void)libName; (void)funcName; - uint32_t nativeArgs[32]; - char *tempStrings[16]; + // Each DOUBLE/SINGLE arg consumes two 32-bit slots, so the marshalled + // word array must hold 2x the max call-arg count. The VM rejects + // argc > BAS_VM_MAX_CALL_ARGS before reaching here. + uint32_t nativeArgs[BAS_VM_MAX_CALL_ARGS * 2]; + char *tempStrings[BAS_VM_MAX_CALL_ARGS]; int32_t tempStringCount = 0; int32_t nativeCount = 0; - for (int32_t i = 0; i < argc && i < 16; i++) { + for (int32_t i = 0; i < argc && i < BAS_VM_MAX_CALL_ARGS; i++) { switch (args[i].type) { case BAS_TYPE_STRING: { BasStringT *s = basValFormatString(args[i]); @@ -423,8 +458,15 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha break; } - case BAS_TYPE_DOUBLE: case BAS_TYPE_SINGLE: { + union { double d; uint32_t u[2]; } conv; + conv.d = (double)args[i].sngVal; + nativeArgs[nativeCount++] = conv.u[0]; + nativeArgs[nativeCount++] = conv.u[1]; + break; + } + + case BAS_TYPE_DOUBLE: { union { double d; uint32_t u[2]; } conv; conv.d = args[i].dblVal; nativeArgs[nativeCount++] = conv.u[0]; @@ -442,73 +484,17 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha BasValueT result; memset(&result, 0, sizeof(result)); - if (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE) { - double dblResult = 0.0; + double dblResult = 0.0; + bool fpReturn = (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE); - __asm__ __volatile__( - "movl %%esp, %%ebx\n\t" - ::: "ebx" - ); - - for (int32_t i = nativeCount - 1; i >= 0; i--) { - uint32_t val = nativeArgs[i]; - __asm__ __volatile__("pushl %0" :: "r"(val)); - } - - __asm__ __volatile__( - "call *%1\n\t" - "fstpl %0\n\t" - "movl %%ebx, %%esp\n\t" - : "=m"(dblResult) - : "r"(funcPtr) - : "eax", "ecx", "edx", "memory" - ); + uint32_t rawResult = basNativeCall(funcPtr, nativeArgs, nativeCount, fpReturn, &dblResult); + if (fpReturn) { result = basValDouble(dblResult); } else if (retType == BAS_TYPE_STRING) { - uint32_t rawResult = 0; - - __asm__ __volatile__( - "movl %%esp, %%ebx\n\t" - ::: "ebx" - ); - - for (int32_t i = nativeCount - 1; i >= 0; i--) { - uint32_t val = nativeArgs[i]; - __asm__ __volatile__("pushl %0" :: "r"(val)); - } - - __asm__ __volatile__( - "call *%1\n\t" - "movl %%ebx, %%esp\n\t" - : "=a"(rawResult) - : "r"(funcPtr) - : "ecx", "edx", "memory" - ); - const char *str = (const char *)(uintptr_t)rawResult; result = basValStringFromC(str ? str : ""); } else { - uint32_t rawResult = 0; - - __asm__ __volatile__( - "movl %%esp, %%ebx\n\t" - ::: "ebx" - ); - - for (int32_t i = nativeCount - 1; i >= 0; i--) { - uint32_t val = nativeArgs[i]; - __asm__ __volatile__("pushl %0" :: "r"(val)); - } - - __asm__ __volatile__( - "call *%1\n\t" - "movl %%ebx, %%esp\n\t" - : "=a"(rawResult) - : "r"(funcPtr) - : "ecx", "edx", "memory" - ); - result = basValLong((int32_t)rawResult); } @@ -537,7 +523,7 @@ const char *basFileOpen(const char *title, const char *filter) { } FileFilterT *filters = NULL; - char filterBuf[1024]; + char filterBuf[BAS_FILTER_BUF_LEN]; int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf)); static char path[DVX_MAX_PATH]; @@ -561,7 +547,7 @@ const char *basFileSave(const char *title, const char *filter) { } FileFilterT *filters = NULL; - char filterBuf[1024]; + char filterBuf[BAS_FILTER_BUF_LEN]; int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf)); static char path[DVX_MAX_PATH]; @@ -921,14 +907,10 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const arrput(form->controls, ctrl); - // Wire up event callbacks - widget->userData = ctrl; - widget->onClick = onWidgetClick; - widget->onDblClick = onWidgetDblClick; - widget->onChange = onWidgetChange; - widget->onFocus = onWidgetFocus; - widget->onBlur = onWidgetBlur; - widget->onValidate = onWidgetValidate; + // Wire up event callbacks (key/mouse/scroll included so dynamically + // created controls match the .frm-load path; dispatch no-ops when + // BASIC has no matching handler). + wireWidgetEvents(widget, ctrl); return ctrl; } @@ -999,13 +981,7 @@ void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, cons arrput(form->controls, ctrl); - widget->userData = ctrl; - widget->onClick = onWidgetClick; - widget->onDblClick = onWidgetDblClick; - widget->onChange = onWidgetChange; - widget->onFocus = onWidgetFocus; - widget->onBlur = onWidgetBlur; - widget->onValidate = onWidgetValidate; + wireWidgetEvents(widget, ctrl); return ctrl; } @@ -1044,7 +1020,7 @@ void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_ BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT)); arrput(rt->forms, form); - snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); + snprintf(form->name, BAS_MAX_FORM_NAME, "%s", formName); snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox"); form->frmWidth = width; form->frmHeight = height; @@ -1068,30 +1044,7 @@ void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_ form->formCtrl.form = form; // Allocate per-form variable storage from module metadata - if (rt->module && rt->module->formVarInfo) { - for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { - if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) { - int32_t vc = rt->module->formVarInfo[j].varCount; - - if (vc > 0) { - form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); - form->formVarCount = vc; - } - - int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; - - if (initAddr >= 0 && rt->vm) { - basVmSetCurrentForm(rt->vm, form); - basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); - basVmCallSub(rt->vm, initAddr); - basVmSetCurrentForm(rt->vm, NULL); - basVmSetCurrentFormVars(rt->vm, NULL, 0); - } - - break; - } - } - } + basFormRtInitFormVars(rt, form, true); return form; } @@ -1147,34 +1100,15 @@ void basFormRtDestroy(BasFormRtT *rt) { sFormRt = NULL; } + // Close serial/secLink and drop the idle pollers before tearing down the + // forms whose terminal widgets they reference. Also invoked from the + // stub's _appShutdown hook so a force-killed app gets the same cleanup. + basFormRtSerialShutdown(); + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { BasFormT *form = rt->forms[i]; - if (form->formVars) { - for (int32_t j = 0; j < form->formVarCount; j++) { - basValRelease(&form->formVars[j]); - } - - free(form->formVars); - } - - for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { - free(form->controls[j]); - } - - arrfree(form->controls); - - for (int32_t j = 0; j < form->menuIdMapCount; j++) { - free(form->menuIdMap[j].proxy); - } - - arrfree(form->menuIdMap); - - for (int32_t j = 0; j < form->popupMenuCount; j++) { - wmFreeMenu(form->popupMenus[j].menu); - } - - arrfree(form->popupMenus); + basFormRtTeardownForm(rt, form); if (form->window) { dvxDestroyWindow(rt->ctx, form->window); @@ -1192,10 +1126,50 @@ void basFormRtDestroy(BasFormRtT *rt) { } arrfree(rt->frmCache); + + // Free compiled form cache + for (int32_t i = 0; i < rt->cfmCacheCount; i++) { + free(rt->cfmCache[i].data); + } + + arrfree(rt->cfmCache); free(rt); } +// Release all serial/secLink resources and unregister both idle pollers from +// the shell. Safe to call repeatedly. The pollers live in this shared +// runtime DXE but iterate per-app slots/terminals, so they MUST be dropped +// when a BASIC app goes away -- on normal exit via basFormRtDestroy, and on +// force-kill (where basFormRtDestroy never runs) via the stub's _appShutdown. +// NOTE: closes every open connection/port, which is correct for the single +// foreground BASIC app; concurrent BASIC instances sharing these module-scope +// slots is a separate, pre-existing limitation. +void basFormRtSerialShutdown(void) { + for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) { + if (sCommSlots[i]) { + CommClose(i + 1); + } + } + + arrfree(sCommSlots); + sCommSlots = NULL; + + if (sSerApiResolved && sSerApi.close) { + for (int32_t i = 0; i < RS232_NUM_PORTS; i++) { + if (sSerAttach[i].attached) { + SerClose(i + 1); + } + } + } + + if (sShellUnregisterIdle) { + sShellUnregisterIdle(commIdlePoll, NULL); + sShellUnregisterIdle(serIdlePoll, NULL); + } +} + + void basFormRtEventLoop(BasFormRtT *rt) { if (!rt || !rt->ctx || !rt->vm) { return; @@ -1243,10 +1217,13 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { // Create proxy on first access if (!form->menuIdMap[i].proxy) { BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT)); - snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); - proxy->form = form; - proxy->menuId = form->menuIdMap[i].id; - form->menuIdMap[i].proxy = proxy; + + if (proxy) { + snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + proxy->form = form; + proxy->menuId = form->menuIdMap[i].id; + form->menuIdMap[i].proxy = proxy; + } } return form->menuIdMap[i].proxy; } @@ -1280,10 +1257,13 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { if (strcasecmp(other->menuIdMap[j].name, ctrlName) == 0) { if (!other->menuIdMap[j].proxy) { BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT)); - snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); - proxy->form = other; - proxy->menuId = other->menuIdMap[j].id; - other->menuIdMap[j].proxy = proxy; + + if (proxy) { + snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + proxy->form = other; + proxy->menuId = other->menuIdMap[j].id; + other->menuIdMap[j].proxy = proxy; + } } return other->menuIdMap[j].proxy; } @@ -1362,6 +1342,8 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName bool ok; + rtEventEnter(rt); + if (argCount > 0 && args) { ok = basVmCallSubWithArgs(rt->vm, proc->codeAddr, args, argCount); } else { @@ -1371,6 +1353,7 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName rt->currentForm = prevForm; basVmSetCurrentForm(rt->vm, prevForm); basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); + rtEventLeave(rt); return ok; } @@ -1414,6 +1397,8 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c bool cancelled = false; + rtEventEnter(rt); + if (proc->paramCount == 1) { BasValueT args[1]; args[0] = basValLong(0); // Cancel = 0 (don't cancel) @@ -1432,6 +1417,7 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c rt->currentForm = prevForm; basVmSetCurrentForm(rt->vm, prevForm); basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); + rtEventLeave(rt); return cancelled; } @@ -1449,27 +1435,31 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { return zeroValue(); } - // Menu item properties - if (ctrl->menuId > 0 && ctrl->form && ctrl->form->window && ctrl->form->window->menuBar) { - MenuBarT *bar = ctrl->form->window->menuBar; + // Menu item properties. resolveMenuItem handles both menu-bar items and + // popup-only items (a Visible=False menu with no bar entry), so + // Checked/Enabled read the item's real state in either case. + if (ctrl->menuId > 0 && ctrl->form) { + MenuItemT *item = resolveMenuItem(ctrl->form, ctrl->menuId); - if (strcasecmp(propName, "Checked") == 0) { - return basValBool(wmMenuItemIsChecked(bar, ctrl->menuId)); + if (item) { + if (strcasecmp(propName, "Checked") == 0) { + return basValBool(item->checked); + } + + if (strcasecmp(propName, "Enabled") == 0) { + return basValBool(item->enabled); + } + + if (strcasecmp(propName, "Name") == 0) { + return basValStringFromC(ctrl->name); + } + + basFormRtRuntimeError(rt, + "Unknown menu property", + "Menu: %s\nProperty: %s\nMenu items only expose Checked, Enabled, and Name.", + ctrl->name, propName ? propName : "?"); + return zeroValue(); } - - if (strcasecmp(propName, "Enabled") == 0) { - return basValBool(true); // TODO: wmMenuItemIsEnabled not exposed yet - } - - if (strcasecmp(propName, "Name") == 0) { - return basValStringFromC(ctrl->name); - } - - basFormRtRuntimeError(rt, - "Unknown menu property", - "Menu: %s\nProperty: %s\nMenu items only expose Checked, Enabled, and Name.", - ctrl->name, propName ? propName : "?"); - return zeroValue(); } if (!ctrl->widget) { @@ -1583,21 +1573,63 @@ void basFormRtHideForm(void *ctx, void *formRef) { return; } + // A hidden form must not stay registered as the modal window, or + // the modal input gate would drop all clicks and deadlock input. + if (rt->ctx->modalWindow == form->window) { + rt->ctx->modalWindow = NULL; + } + dvxHideWindow(rt->ctx, form->window); } +// Allocate per-form variable storage from module metadata and, when +// runInit is true, run the form's init code. Allocation is idempotent: +// if form->formVars is already set (e.g. a bare form created by the +// nested .frm LoadForm), it is left untouched. Single source of truth +// for the four formerly-duplicated init blocks. +static void basFormRtInitFormVars(BasFormRtT *rt, BasFormT *form, bool runInit) { + if (!rt || !form || !rt->module || !rt->module->formVarInfo) { + return; + } + + for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { + if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) != 0) { + continue; + } + + if (!form->formVars) { + int32_t vc = rt->module->formVarInfo[j].varCount; + + if (vc > 0) { + form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); + form->formVarCount = vc; + } + } + + int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; + + if (runInit && initAddr >= 0 && rt->vm) { + rtEventEnter(rt); + basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmCallSub(rt->vm, initAddr); + basVmSetCurrentForm(rt->vm, NULL); + basVmSetCurrentFormVars(rt->vm, NULL, 0); + rtEventLeave(rt); + } + + break; + } +} + + static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText) { BasFormRtT *rt = (BasFormRtT *)ctx; char buf[256]; buf[0] = '\0'; - if (defaultText && defaultText[0]) { - strncpy(buf, defaultText, sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - } - if (!dvxInputBox(rt->ctx, title, prompt, defaultText, buf, sizeof(buf))) { return NULL; } @@ -1644,71 +1676,6 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) { static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen) { (void)rt; (void)data; (void)dataLen; return NULL; - // Unreachable past here; left to satisfy the rest of the function body - // which assumed a live formcfm implementation. - BasFormT *form = NULL; - - if (!form || !form->window) { - return form; - } - - // Set window callbacks (same as basFormRtLoadForm) - form->window->onClose = onFormClose; - form->window->onResize = onFormResize; - form->window->onFocus = onFormActivate; - form->window->onBlur = onFormDeactivate; - - // Wire event callbacks on all controls - for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { - BasControlT *ctrl = form->controls[i]; - - if (ctrl->widget) { - ctrl->widget->onClick = onWidgetClick; - ctrl->widget->onDblClick = onWidgetDblClick; - ctrl->widget->onChange = onWidgetChange; - ctrl->widget->onFocus = onWidgetFocus; - ctrl->widget->onBlur = onWidgetBlur; - ctrl->widget->onValidate = onWidgetValidate; - ctrl->widget->onKeyPress = onWidgetKeyPress; - ctrl->widget->onKeyDown = onWidgetKeyDown; - ctrl->widget->onKeyUp = onWidgetKeyUp; - ctrl->widget->onMouseDown = onWidgetMouseDown; - ctrl->widget->onMouseUp = onWidgetMouseUp; - ctrl->widget->onMouseMove = onWidgetMouseMove; - ctrl->widget->onScroll = onWidgetScroll; - } - } - - // Allocate per-form variable storage - if (rt->module && rt->module->formVarInfo) { - for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { - if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) == 0) { - int32_t vc = rt->module->formVarInfo[j].varCount; - - if (vc > 0) { - form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); - form->formVarCount = vc; - } - - int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; - - if (initAddr >= 0 && rt->vm) { - basVmSetCurrentForm(rt->vm, form); - basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); - basVmCallSub(rt->vm, initAddr); - basVmSetCurrentForm(rt->vm, NULL); - basVmSetCurrentFormVars(rt->vm, NULL, 0); - } - - break; - } - } - } - - // Fire Form_Load event - basFormRtFireEvent(rt, form, form->name, "Load"); - - return form; } @@ -1764,7 +1731,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT)); arrput(rt->forms, form); - snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); + snprintf(form->name, BAS_MAX_FORM_NAME, "%s", formName); snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox"); win->onClose = onFormClose; win->onResize = onFormResize; @@ -1790,30 +1757,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { // against an empty controls list. // - Otherwise this is a genuine bare form (no cached .frm); we // still need to run init to DIM arrays and UDT fields. - if (rt->module && rt->module->formVarInfo) { - for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { - if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) { - int32_t vc = rt->module->formVarInfo[j].varCount; - - if (vc > 0) { - form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); - form->formVarCount = vc; - } - - int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; - - if (!sLoadingFrm && initAddr >= 0 && rt->vm) { - basVmSetCurrentForm(rt->vm, form); - basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); - basVmCallSub(rt->vm, initAddr); - basVmSetCurrentForm(rt->vm, NULL); - basVmSetCurrentFormVars(rt->vm, NULL, 0); - } - - break; - } - } - } + basFormRtInitFormVars(rt, form, !sLoadingFrm); return form; } @@ -1863,8 +1807,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Apply accumulated form-level properties if (form) { - // Ensure content box exists even if form has no controls - if (!form->contentBox && form->root) { + // Ensure content box exists even if form has no controls. contentBox + // starts as root, so test for the default root box (not NULL) or a + // form-level Layout would be ignored. + if (form->contentBox == form->root && form->root) { form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout); } @@ -1916,7 +1862,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen BasFrmPopupMenuT entry; memset(&entry, 0, sizeof(entry)); snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", mi->name); - entry.menu = curTopPopup; + entry.menu = curTopPopup; + entry.ownsMenu = true; // resource-built popup root arrput(form->popupMenus, entry); form->popupMenuCount = (int32_t)arrlen(form->popupMenus); } @@ -2019,7 +1966,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen if (!cached) { BasFrmCacheT entry; memset(&entry, 0, sizeof(entry)); - snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", form->name); + snprintf(entry.formName, BAS_MAX_FORM_NAME, "%s", form->name); entry.frmSource = (char *)malloc(sourceLen + 1); entry.frmSourceLen = sourceLen; @@ -2036,33 +1983,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Allocate per-form variable storage and run init code. // This must happen AFTER controls are created (so init code can // reference controls by name) but BEFORE Form_Load fires. - if (form && rt->module && rt->module->formVarInfo) { - for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { - if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) != 0) { - continue; - } - - if (!form->formVars) { - int32_t vc = rt->module->formVarInfo[j].varCount; - - if (vc > 0) { - form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); - form->formVarCount = vc; - } - } - - int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; - - if (initAddr >= 0 && rt->vm) { - basVmSetCurrentForm(rt->vm, form); - basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); - basVmCallSub(rt->vm, initAddr); - basVmSetCurrentForm(rt->vm, NULL); - basVmSetCurrentFormVars(rt->vm, NULL, 0); - } - - break; - } + if (form) { + basFormRtInitFormVars(rt, form, true); } // Fire the Load event now that the form and controls are ready @@ -2095,9 +2017,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen continue; } + // wgtDataCtrlRefresh now fires onChange (updateBoundControls + + // Reposition + refreshDetailControls) itself, so the manual + // follow-up calls would double-run them. wgtDataCtrlRefresh(dc->widget); - updateBoundControls(form, dc); - refreshDetailControls(form, dc); } } @@ -2235,8 +2158,13 @@ void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *d } BasCfmCacheT entry; - snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", formName); - entry.data = (uint8_t *)malloc(dataLen); + snprintf(entry.formName, BAS_MAX_FORM_NAME, "%s", formName); + entry.data = (uint8_t *)malloc(dataLen); + + if (!entry.data) { + return; + } + entry.dataLen = dataLen; memcpy(entry.data, data, dataLen); arrput(rt->cfmCache, entry); @@ -2250,8 +2178,13 @@ void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *sour } BasFrmCacheT entry; - snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", formName); - entry.frmSource = (char *)malloc(sourceLen + 1); + snprintf(entry.formName, BAS_MAX_FORM_NAME, "%s", formName); + entry.frmSource = (char *)malloc(sourceLen + 1); + + if (!entry.frmSource) { + return; + } + entry.frmSourceLen = sourceLen; memcpy(entry.frmSource, source, sourceLen); entry.frmSource[sourceLen] = '\0'; @@ -2277,7 +2210,9 @@ void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) { wgtDestroy(ctrl->widget); } - free(ctrl); + // freeControl also releases ctrl->tooltip; a bare free(ctrl) + // leaked it. + freeControl(ctrl); arrdel(form->controls, i); return; @@ -2367,25 +2302,46 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } - // Menu item properties - if (ctrl->menuId > 0 && ctrl->form && ctrl->form->window && ctrl->form->window->menuBar) { - MenuBarT *bar = ctrl->form->window->menuBar; + // Menu item properties. Bar items route through wmMenuItemSetChecked so + // the menu-bar radio-group walk runs; popup-only items route through + // wmMenuItemSetCheckedInMenu, which does the same walk on a standalone + // menu. Enabled writes the item's flag directly in both cases. + if (ctrl->menuId > 0 && ctrl->form) { + MenuBarT *bar = (ctrl->form->window) ? ctrl->form->window->menuBar : NULL; + MenuItemT *barItem = NULL; - if (strcasecmp(propName, "Checked") == 0) { - wmMenuItemSetChecked(bar, ctrl->menuId, basValIsTruthy(value)); - return; + if (bar) { + for (int32_t i = 0; i < bar->menuCount && !barItem; i++) { + barItem = wmMenuFindItemInMenu(bar->menus[i], ctrl->menuId); + } } - if (strcasecmp(propName, "Enabled") == 0) { - wmMenuItemSetEnabled(bar, ctrl->menuId, basValIsTruthy(value)); + MenuItemT *item = barItem ? barItem : resolveMenuItem(ctrl->form, ctrl->menuId); + + if (item) { + if (strcasecmp(propName, "Checked") == 0) { + if (barItem) { + wmMenuItemSetChecked(bar, ctrl->menuId, basValIsTruthy(value)); + } else { + for (int32_t i = 0; i < ctrl->form->popupMenuCount; i++) { + wmMenuItemSetCheckedInMenu(ctrl->form->popupMenus[i].menu, ctrl->menuId, basValIsTruthy(value)); + } + } + + return; + } + + if (strcasecmp(propName, "Enabled") == 0) { + item->enabled = basValIsTruthy(value); + return; + } + + basFormRtRuntimeError(rt, + "Unknown menu property", + "Menu: %s\nProperty: %s\nMenu items only support Checked and Enabled.", + ctrl->name, propName ? propName : "?"); return; } - - basFormRtRuntimeError(rt, - "Unknown menu property", - "Menu: %s\nProperty: %s\nMenu items only support Checked and Enabled.", - ctrl->name, propName ? propName : "?"); - return; } if (!ctrl->widget) { @@ -2584,22 +2540,15 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { } -void basFormRtUnloadForm(void *ctx, void *formRef) { - BasFormRtT *rt = (BasFormRtT *)ctx; - BasFormT *form = (BasFormT *)formRef; +// Free ALL per-form heap resources: form variables, controls (with +// tooltips), menuIdMap proxies + array, and popup menus. Does NOT +// destroy the window, does NOT remove the form from rt->forms, and does +// NOT free the BasFormT itself -- callers differ on those. Single +// source of truth for the three formerly-diverged teardown sites +// (basFormRtDestroy, basFormRtUnloadForm, onFormClose). +static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) { + (void)rt; - if (!form) { - return; - } - - // QueryUnload: give the form a chance to cancel - if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) { - return; - } - - basFormRtFireEvent(rt, form, form->name, "Unload"); - - // Release per-form variables if (form->formVars) { for (int32_t i = 0; i < form->formVarCount; i++) { basValRelease(&form->formVars[i]); @@ -2611,46 +2560,82 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { } for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { - free(form->controls[i]->tooltip); - free(form->controls[i]); + freeControl(form->controls[i]); } arrfree(form->controls); form->controls = NULL; + for (int32_t i = 0; i < form->menuIdMapCount; i++) { + freeControl(form->menuIdMap[i].proxy); + } + + arrfree(form->menuIdMap); + form->menuIdMap = NULL; + form->menuIdMapCount = 0; + + // Free popup menus with first-occurrence ownership (mirrors the + // DestroyMenu isSubmenu dedupe). AddSubMenu requires its parent to + // already be registered, so a given MenuT* always appears first as + // its owning root; later duplicate submenu entries are skipped to + // avoid the double-free that wmFreeMenu's recursion would cause. + // Free only the entries flagged as owning roots. AddSubMenu stores a + // non-owning alias (ownsMenu=false) whose MenuT is freed by its root's + // wmFreeMenu recursion; the old exact-pointer dedupe never matched those + // distinct child pointers and double-freed them. for (int32_t i = 0; i < form->popupMenuCount; i++) { - wmFreeMenu(form->popupMenus[i].menu); + if (form->popupMenus[i].ownsMenu) { + wmFreeMenu(form->popupMenus[i].menu); + } } arrfree(form->popupMenus); form->popupMenus = NULL; form->popupMenuCount = 0; +} - if (form->window) { - if (rt->ctx->modalWindow == form->window) { - rt->ctx->modalWindow = NULL; + +void basFormRtUnloadForm(void *ctx, void *formRef) { + BasFormRtT *rt = (BasFormRtT *)ctx; + BasFormT *form = (BasFormT *)formRef; + + if (!form || form->unloading) { + return; + } + + // Block re-entrant Unload of this form from its own QueryUnload / + // Unload handlers; cleared below if QueryUnload cancels. + form->unloading = true; + + // QueryUnload: give the form a chance to cancel + if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) { + form->unloading = false; + return; + } + + basFormRtFireEvent(rt, form, form->name, "Unload"); + + // If a VM event handler is still on the stack (Unload Me inside a + // Click handler), the handler's form variables and the firing + // BasControlT live on this form -- freeing them now is a guaranteed + // use-after-free when the handler resumes. Hide the window for + // immediate visual feedback and defer the frees to rtEventLeave. + if (rt->eventDepth > 0) { + if (form->window) { + dvxHideWindow(rt->ctx, form->window); } - dvxDestroyWindow(rt->ctx, form->window); - form->window = NULL; - form->root = NULL; - form->contentBox = NULL; - } - - int32_t idx = -1; - - for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { - if (rt->forms[i] == form) { - idx = i; - break; + if (rt->pendingUnloadCount < BAS_MAX_PENDING_UNLOAD) { + rt->pendingUnload[rt->pendingUnloadCount] = form; + rt->pendingUnloadCount++; + return; } + + // Queue full: fall through to immediate teardown (re-risks the + // UAF, but only past 8 unloads from one handler). } - if (idx >= 0 && idx < (int32_t)arrlen(rt->forms)) { - free(form); - arrdel(rt->forms, idx); - - } + unloadFormNow(rt, form); } @@ -2711,6 +2696,120 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char } +// Push the already-marshalled cdecl argument words and call a native +// function via funcPtr. Returns the EAX result word; when fpReturn is +// true the x87 ST(0) result is also stored through fpResult via fstpl. +// +// The ESP save, the entire push loop, the call, and the ESP restore +// live inside ONE __asm__ block per return path: a saved-ESP value held +// in a scratch register cannot survive across separate asm statements +// (GCC is free to reuse the register), so everything that depends on it +// must stay in the same block. EDI holds the saved ESP, ESI the args +// base, ECX the down-counter; all are clobber-listed so GCC reloads +// anything it kept there. "memory" stops GCC caching the string args +// the callee may dereference. fstpl is gated strictly to fpReturn so +// the integer path never pops the x87 stack. +static uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) { + uint32_t rawResult = 0; + + if (fpReturn) { + __asm__ __volatile__( + "movl %%esp, %%edi\n\t" // save ESP + "movl %[cnt], %%ecx\n\t" // ECX = arg count + "1:\n\t" + "testl %%ecx, %%ecx\n\t" + "jle 2f\n\t" + "decl %%ecx\n\t" + "pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX] + "jmp 1b\n\t" + "2:\n\t" + "call *%[fn]\n\t" + "fstpl %[fpres]\n\t" // pop x87 ST(0) -> *fpResult + "movl %%edi, %%esp\n\t" // restore ESP + : "=a"(rawResult), [fpres] "=m"(*fpResult) + : [fn] "r"(funcPtr), "S"(nativeArgs), [cnt] "r"(nativeCount) + : "ecx", "edi", "edx", "cc", "memory" + ); + } else { + __asm__ __volatile__( + "movl %%esp, %%edi\n\t" // save ESP + "movl %[cnt], %%ecx\n\t" // ECX = arg count + "1:\n\t" + "testl %%ecx, %%ecx\n\t" + "jle 2f\n\t" + "decl %%ecx\n\t" + "pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX] + "jmp 1b\n\t" + "2:\n\t" + "call *%[fn]\n\t" + "movl %%edi, %%esp\n\t" // restore ESP + : "=a"(rawResult) + : [fn] "r"(funcPtr), "S"(nativeArgs), [cnt] "r"(nativeCount) + : "ecx", "edi", "edx", "cc", "memory" + ); + } + + return rawResult; +} + + +// True if target is tree itself or any submenu nested within it (any depth). +// Used by DestroyMenu to purge every popupMenus alias and contextMenu pointer +// that references a tree about to be freed. +static bool menuContainsMenu(const MenuT *tree, const MenuT *target) { + if (!tree || !target) { + return false; + } + + if (tree == target) { + return true; + } + + for (int32_t i = 0; i < tree->itemCount; i++) { + if (menuContainsMenu(tree->items[i].subMenu, target)) { + return true; + } + } + + return false; +} + + +// Resolve a menu item by command id across both the form's menu bar AND its +// popup-only menus, so .Checked/.Enabled work on Visible=False popup items +// (which have no menu-bar entry) as well as bar items. +static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId) { + if (!form || menuId <= 0) { + return NULL; + } + + // Menu-bar items: walk every top-level menu (and its submenus) on the + // form's window menu bar. + if (form->window && form->window->menuBar) { + MenuBarT *bar = form->window->menuBar; + + for (int32_t i = 0; i < bar->menuCount; i++) { + MenuItemT *item = wmMenuFindItemInMenu(bar->menus[i], menuId); + + if (item) { + return item; + } + } + } + + // Popup-only items: search each named popup/context menu the form owns. + for (int32_t i = 0; i < form->popupMenuCount; i++) { + MenuItemT *item = wmMenuFindItemInMenu(form->popupMenus[i].menu, menuId); + + if (item) { + return item; + } + } + + return NULL; +} + + // Find the loaded form whose .frm declared this SUB. Returns NULL if // the SUB is module-global (no BEGINFORM scope) or the owning form // isn't currently loaded -- callers should fall back to ctrl->form. @@ -2731,7 +2830,7 @@ static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) { int32_t basPromptSave(const char *title) { if (!sFormRt) { - return 2; + return DVX_SAVE_NO; } return dvxPromptSave(sFormRt->ctx, title); @@ -2866,7 +2965,8 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas BasFrmPopupMenuT entry; memset(&entry, 0, sizeof(entry)); snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", s->data); - entry.menu = wmCreateMenu(); + entry.menu = wmCreateMenu(); + entry.ownsMenu = true; // CreateMenu root arrput(menuForm->popupMenus, entry); menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus); } @@ -3107,28 +3207,37 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas if (foundIdx >= 0) { MenuT *m = menuForm->popupMenus[foundIdx].menu; - // Only free if this is a root (not a submenu whose - // parent also has a pointer). Heuristic: check if any - // other popup entry contains m as a descendant. Safe - // alternative: if m appears elsewhere in the list, treat - // the first occurrence as the root. For now we free - // only if no other popup has the same MenuT*, which - // handles the common case where a submenu is registered - // under a separate name but still owned by its parent. - bool isSubmenu = false; + if (menuForm->popupMenus[foundIdx].ownsMenu) { + // Freeing an owning root: first null every widget/window + // contextMenu that points anywhere into this tree, then drop + // ALL popupMenus entries (root + non-owning child aliases) + // that reference it, then free the tree once. Otherwise those + // aliases and contextMenu pointers would dangle. + for (int32_t i = 0; i < (int32_t)arrlen(menuForm->controls); i++) { + WidgetT *cw = menuForm->controls[i]->widget; - for (int32_t i = 0; i < menuForm->popupMenuCount; i++) { - if (i != foundIdx && menuForm->popupMenus[i].menu == m) { - isSubmenu = true; - break; + if (cw && menuContainsMenu(m, cw->contextMenu)) { + cw->contextMenu = NULL; + } + } + + if (menuForm->window && menuContainsMenu(m, menuForm->window->contextMenu)) { + menuForm->window->contextMenu = NULL; + } + + for (int32_t i = menuForm->popupMenuCount - 1; i >= 0; i--) { + if (menuContainsMenu(m, menuForm->popupMenus[i].menu)) { + arrdel(menuForm->popupMenus, i); + } } - } - if (!isSubmenu) { wmFreeMenu(m); + } else { + // Non-owning alias: drop just this entry; the root owns the + // tree and will free it. + arrdel(menuForm->popupMenus, foundIdx); } - arrdel(menuForm->popupMenus, foundIdx); menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus); } @@ -3159,38 +3268,27 @@ void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32 } // Find the terminal widget by control name on any loaded form - WidgetT *termWidget = NULL; - - for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { - BasFormT *form = sFormRt->forms[i]; - - for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { - if (strcasecmp(form->controls[j]->name, termCtrlName) == 0) { - termWidget = form->controls[j]->widget; - break; - } - } - - if (termWidget) { - break; - } - } + WidgetT *termWidget = findCtrlWidgetByName(termCtrlName); if (!termWidget) { return; } + // commTermRead/commTermWrite index channels[] by termChannel; an + // out-of-range channel would read/write past the slot. + if (channel < 0 || channel >= SECLINK_NUM_CHANNELS) { + return; + } + slot->termChannel = channel; slot->termEncrypt = (encrypt != 0); wgtAnsiTermSetComm(termWidget, slot, commTermRead, commTermWrite); slot->attachedTerm = termWidget; - // Set idle callback for automatic polling - if (sFormRt->ctx) { - sFormRt->ctx->idleCallback = commIdlePoll; - sFormRt->ctx->idleCtx = NULL; - } + // Register the secLink poller with the shell's idle registry so this + // connection is pumped every idle frame regardless of foreground app. + formRtCommIdleSync(); } @@ -3214,6 +3312,9 @@ void CommClose(int32_t handle) { // connection until a fresh CommOpen fills the hole. free(slot); sCommSlots[handle - 1] = NULL; + + // Drop the secLink poller if that was the last open connection. + formRtCommIdleSync(); } @@ -3266,6 +3367,33 @@ static void commIdlePoll(void *ctx) { } +// Register or unregister the secLink idle poller with the shell based on +// whether any connection is still open. Idempotent on both sides, so this +// is safe to call from every CommAttach and CommClose. commIdlePoll keeps +// every open link pumped (handshake/retransmit) regardless of which app is +// foreground, so it stays registered until the last connection closes. +static void formRtCommIdleSync(void) { + bool needed = false; + + for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) { + if (sCommSlots[i] && sCommSlots[i]->active) { + needed = true; + break; + } + } + + if (needed) { + if (resolveShellIdle()) { + sShellRegisterIdle(commIdlePoll, NULL); + } else { + dvxLog("BASIC: shell idle registry unavailable; secLink polling disabled"); + } + } else if (sShellUnregisterIdle) { + sShellUnregisterIdle(commIdlePoll, NULL); + } +} + + int32_t CommIsReady(int32_t handle) { CommSlotT *slot = commGetSlot(handle); @@ -3526,6 +3654,27 @@ WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *p } +// Finds a control's live widget by name across every loaded form. Shared by +// CommAttach/SerAttach to locate the terminal widget to bind a serial channel. +static WidgetT *findCtrlWidgetByName(const char *name) { + if (!sFormRt || !name) { + return NULL; + } + + for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { + BasFormT *form = sFormRt->forms[i]; + + for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { + if (strcasecmp(form->controls[j]->name, name) == 0) { + return form->controls[j]->widget; + } + } + } + + return NULL; +} + + static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) { // Re-entrancy guard: reject only same-event re-entry (runSubLoop // pumps events during long handlers; without this, a lingering @@ -3573,6 +3722,7 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa // Override found: call the named SUB directly if (!rt->vm || !rt->module) { + free(allArgs); return; } @@ -3612,6 +3762,8 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa rt->vm->errorMsg[0] = '\0'; rt->vm->errorNumber = 0; + rtEventEnter(rt); + if (finalArgCount > 0 && finalArgs) { basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount); } else { @@ -3627,6 +3779,7 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa basVmSetCurrentForm(rt->vm, prevForm); basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); free(allArgs); + rtEventLeave(rt); return; } @@ -3638,6 +3791,10 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : ""); } + // Bracket the nested fire with our own depth so a deferred unload + // cannot flush (and free ctrl) before the eventFiring writes below. + rtEventEnter(rt); + if (finalArgCount > 0 && finalArgs) { basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount); } else { @@ -3650,6 +3807,20 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa } free(allArgs); + rtEventLeave(rt); +} + + +// Free a control and its heap-owned tooltip. NULL-safe: also used for +// menuIdMap proxies, which are calloc'd with no tooltip (free(NULL) is +// a no-op), so this is the single owner of control teardown. +static void freeControl(BasControlT *ctrl) { + if (!ctrl) { + return; + } + + free(ctrl->tooltip); + free(ctrl); } @@ -3664,8 +3835,15 @@ static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char return; } - // Lazy-create the content box once we know the form layout. - if (!ctx->form->contentBox && ctx->form->root) { + // Clear any stale pointer first: if this control fails to create (unknown + // type or NULL widget) ctx->current stays NULL, so frmLoad_onCtrlProp's + // guard drops its props instead of overwriting the previous control's. + ctx->current = NULL; + + // Lazy-create the content box once we know the form layout. contentBox + // is initialized to root, so test for "still the default root box" rather + // than NULL -- otherwise a form-level Layout (HBox/WrapBox) never fires. + if (ctx->form->contentBox == ctx->form->root && ctx->form->root) { ctx->form->contentBox = basFormRtCreateContentBox(ctx->form->root, ctx->form->frmLayout); ctx->parentStack[0] = ctx->form->contentBox; } @@ -3707,20 +3885,7 @@ static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char ctx->current = ctrlEntry; - widget->userData = ctrlEntry; - widget->onClick = onWidgetClick; - widget->onDblClick = onWidgetDblClick; - widget->onChange = onWidgetChange; - widget->onFocus = onWidgetFocus; - widget->onBlur = onWidgetBlur; - widget->onValidate = onWidgetValidate; - widget->onKeyPress = onWidgetKeyPress; - widget->onKeyDown = onWidgetKeyDown; - widget->onKeyUp = onWidgetKeyUp; - widget->onMouseDown = onWidgetMouseDown; - widget->onMouseUp = onWidgetMouseUp; - widget->onMouseMove = onWidgetMouseMove; - widget->onScroll = onWidgetScroll; + wireWidgetEvents(widget, ctrlEntry); } const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); @@ -3948,11 +4113,11 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha return basValLong(ctrl->widget->weight); } - if (strcasecmp(propName, "Visible") == 0) { + if (strcasecmp(propName, "Visible") == 0 && !ifaceHasProp(ctrl->iface, "Visible")) { return basValBool(ctrl->widget->visible); } - if (strcasecmp(propName, "Enabled") == 0) { + if (strcasecmp(propName, "Enabled") == 0 && !ifaceHasProp(ctrl->iface, "Enabled")) { return basValBool(ctrl->widget->enabled); } @@ -4001,6 +4166,11 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr return basValStringFromC(s ? s : ""); } + case WGT_IFACE_ENUM: { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); + return basValLong(v); + } + case WGT_IFACE_INT: { int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); return basValLong(v); @@ -4068,7 +4238,31 @@ void HelpView(const char *hlpFile) { } +// True if the widget's interface descriptor registers a property of this name. +// The generic Visible/Enabled common-property handlers defer to such a property +// so a widget can give those names its own meaning (e.g. the Timer's "Enabled" +// drives its running state, VB-style) consistently at runtime and design time. +static bool ifaceHasProp(const WgtIfaceT *iface, const char *propName) { + if (!iface) { + return false; + } + + for (int32_t i = 0; i < iface->propCount; i++) { + if (strcasecmp(iface->props[i].name, propName) == 0) { + return true; + } + } + + return false; +} + + static void onFormActivate(WindowT *win) { + // Chain the default widget focus handler: this overrides + // wgtInitWindow's win->onFocus, so without this the widget focus + // bridge (icon refresh, focus bookkeeping) never runs for forms. + widgetOnFocus(win); + if (!sFormRt) { return; } @@ -4103,24 +4297,7 @@ static void onFormClose(WindowT *win) { basFormRtFireEvent(sFormRt, form, form->name, "Unload"); - // Release per-form variables - if (form->formVars) { - for (int32_t j = 0; j < form->formVarCount; j++) { - basValRelease(&form->formVars[j]); - } - - free(form->formVars); - form->formVars = NULL; - form->formVarCount = 0; - } - - // Free control resources - for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { - free(form->controls[j]); - } - - arrfree(form->controls); - form->controls = NULL; + basFormRtTeardownForm(sFormRt, form); // Destroy the window if (sFormRt->ctx->modalWindow == win) { @@ -4145,6 +4322,11 @@ static void onFormClose(WindowT *win) { static void onFormDeactivate(WindowT *win) { + // Chain the default widget focus handler: it clears sFocusedWidget + // and fires the focused widget's onBlur (BASIC LostFocus event + + // data-binding write-back), which this override otherwise suppresses. + widgetOnBlur(win); + if (!sFormRt) { return; } @@ -4466,12 +4648,19 @@ static bool onWidgetValidate(WidgetT *w) { BasValueT *prevVars = rt->vm->currentFormVars; int32_t prevVarCount = rt->vm->currentFormVarCount; + // Bind form-scope vars to the SUB's owning form (from BEGINFORM), + // not to the control's form. + BasFormT *owningForm = resolveOwningForm(rt, proc); + BasFormT *varsForm = owningForm ? owningForm : ctrl->form; + rt->currentForm = ctrl->form; basVmSetCurrentForm(rt->vm, ctrl->form); - basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount); + basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount); BasValueT outCancel = basValLong(0); + rtEventEnter(rt); + if (proc->paramCount == 1) { basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, &cancelArg, 1, &outCancel, 1); } else { @@ -4481,9 +4670,12 @@ static bool onWidgetValidate(WidgetT *w) { rt->currentForm = prevForm; basVmSetCurrentForm(rt->vm, prevForm); basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); + rtEventLeave(rt); // Non-zero Cancel means abort the write - return !basValIsTruthy(outCancel); + bool cancelled = basValIsTruthy(outCancel); + basValRelease(&outCancel); + return !cancelled; } @@ -4570,6 +4762,12 @@ static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { BasControlT *ctrl = form->controls[i]; + // Never treat the master as its own detail: a control naming itself + // in MasterSource would otherwise re-refresh itself and recurse. + if (ctrl == masterCtrl) { + continue; + } + if (strcasecmp(ctrl->typeName, "Data") != 0 || !ctrl->widget || !ctrl->iface) { continue; } @@ -4603,12 +4801,11 @@ static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { val = wgtDataCtrlGetField(masterCtrl->widget, mf); } - // Set the filter value and refresh the detail + // Set the filter value and refresh the detail. wgtDataCtrlRefresh + // fires onChange, which already runs updateBoundControls for this + // detail, so a manual follow-up call would double it. wgtDataCtrlSetMasterValue(ctrl->widget, val); wgtDataCtrlRefresh(ctrl->widget); - - // Update bound controls for this detail - updateBoundControls(form, ctrl); } } @@ -4834,22 +5031,7 @@ void SerAttach(int32_t com, const char *termCtrlName) { } // Find the terminal widget by control name - WidgetT *termWidget = NULL; - - for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) { - BasFormT *form = sFormRt->forms[i]; - - for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) { - if (strcasecmp(form->controls[j]->name, termCtrlName) == 0) { - termWidget = form->controls[j]->widget; - break; - } - } - - if (termWidget) { - break; - } - } + WidgetT *termWidget = findCtrlWidgetByName(termCtrlName); if (!termWidget) { return; @@ -4861,10 +5043,9 @@ void SerAttach(int32_t com, const char *termCtrlName) { wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite); - if (sFormRt->ctx) { - sFormRt->ctx->idleCallback = serIdlePoll; - sFormRt->ctx->idleCtx = NULL; - } + // Register the serial poller with the shell's idle registry so this + // terminal is fed every idle frame regardless of foreground app. + formRtSerIdleSync(); } @@ -4889,6 +5070,7 @@ void SerClose(int32_t com) { wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL); sSerAttach[idx].attached = false; sSerAttach[idx].term = NULL; + formRtSerIdleSync(); } sSerApi.close(com - 1); @@ -4905,6 +5087,7 @@ void SerDetach(int32_t com) { wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL); sSerAttach[idx].attached = false; sSerAttach[idx].term = NULL; + formRtSerIdleSync(); } @@ -4944,6 +5127,33 @@ int32_t SerGetUart(int32_t com) { } +// Tracks entry into a VM event handler. Paired with rtEventLeave +// around every basVmCallSub* site so basFormRtUnloadForm knows when a +// handler is on the stack and form teardown must be deferred. +static void rtEventEnter(BasFormRtT *rt) { + rt->eventDepth++; +} + + +// Tracks exit from a VM event handler. When the outermost handler +// returns, flushes every form whose Unload was deferred -- at this +// point no VM frame references their form vars or controls. Must be +// called AFTER the caller restores currentForm/currentFormVars and +// finishes writing to the firing control, since the flush frees both. +static void rtEventLeave(BasFormRtT *rt) { + rt->eventDepth--; + + if (rt->eventDepth > 0) { + return; + } + + while (rt->pendingUnloadCount > 0) { + rt->pendingUnloadCount--; + unloadFormNow(rt, rt->pendingUnload[rt->pendingUnloadCount]); + } +} + + // Idle callback for raw serial: polls all attached ports static void serIdlePoll(void *ctx) { (void)ctx; @@ -4956,6 +5166,32 @@ static void serIdlePoll(void *ctx) { } +// Register or unregister the raw-serial idle poller with the shell based on +// whether any port is still attached to a terminal. Idempotent, so it is +// safe to call from SerAttach, SerDetach, and SerClose. serIdlePoll only +// services attached terminals, so it is dropped as soon as none remain. +static void formRtSerIdleSync(void) { + bool needed = false; + + for (int32_t i = 0; i < RS232_NUM_PORTS; i++) { + if (sSerAttach[i].attached) { + needed = true; + break; + } + } + + if (needed) { + if (resolveShellIdle()) { + sShellRegisterIdle(serIdlePoll, NULL); + } else { + dvxLog("BASIC: shell idle registry unavailable; serial polling disabled"); + } + } else if (sShellUnregisterIdle) { + sShellUnregisterIdle(serIdlePoll, NULL); + } +} + + int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) { if (!serResolveApi()) { return 0; @@ -4984,6 +5220,21 @@ const char *SerRead(int32_t com) { } +// Lazily resolve the shell's idle-handler registry (mirrors the +// _shellLoadAppWithArgs resolution used by HelpView). Returns false when +// the host does not export the registry -- e.g. a headless/proxy build with +// no shell event loop -- in which case polling is simply not driven. +static bool resolveShellIdle(void) { + if (!sShellIdleResolved) { + sShellIdleResolved = true; + sShellRegisterIdle = dlsym(NULL, "_shellRegisterIdle"); + sShellUnregisterIdle = dlsym(NULL, "_shellUnregisterIdle"); + } + + return sShellRegisterIdle != NULL; +} + + static bool serResolveApi(void) { if (sSerApiResolved) { return sSerApi.open != NULL; @@ -5135,12 +5386,12 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val return true; } - if (strcasecmp(propName, "Visible") == 0) { + if (strcasecmp(propName, "Visible") == 0 && !ifaceHasProp(ctrl->iface, "Visible")) { wgtSetVisible(ctrl->widget, basValIsTruthy(value)); return true; } - if (strcasecmp(propName, "Enabled") == 0) { + if (strcasecmp(propName, "Enabled") == 0 && !ifaceHasProp(ctrl->iface, "Enabled")) { wgtSetEnabled(ctrl->widget, basValIsTruthy(value)); return true; } @@ -5405,6 +5656,40 @@ int32_t SQLQuery(int32_t db, const char *sql) { // updateBoundControls -- sync bound controls from Data control's current record +// The teardown half of basFormRtUnloadForm: frees the form's controls, +// variables, window, and the BasFormT itself. QueryUnload/Unload have +// already fired by the time this runs. Called directly when no VM +// handler is on the stack, or from rtEventLeave's deferred-unload flush. +static void unloadFormNow(BasFormRtT *rt, BasFormT *form) { + basFormRtTeardownForm(rt, form); + + if (form->window) { + if (rt->ctx->modalWindow == form->window) { + rt->ctx->modalWindow = NULL; + } + + dvxDestroyWindow(rt->ctx, form->window); + form->window = NULL; + form->root = NULL; + form->contentBox = NULL; + } + + int32_t idx = -1; + + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + if (rt->forms[i] == form) { + idx = i; + break; + } + } + + if (idx >= 0 && idx < (int32_t)arrlen(rt->forms)) { + free(form); + arrdel(rt->forms, idx); + } +} + + static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { if (!dataCtrl->widget) { return; @@ -5520,6 +5805,27 @@ bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, in } +// Wires the full set of widget event callbacks to the runtime dispatchers and +// links the widget back to its BasControlT. Shared by the two dynamic +// create-control paths and the .frm loader so every control behaves the same. +static void wireWidgetEvents(WidgetT *w, BasControlT *ctrl) { + w->userData = ctrl; + w->onClick = onWidgetClick; + w->onDblClick = onWidgetDblClick; + w->onChange = onWidgetChange; + w->onFocus = onWidgetFocus; + w->onBlur = onWidgetBlur; + w->onValidate = onWidgetValidate; + w->onKeyPress = onWidgetKeyPress; + w->onKeyDown = onWidgetKeyDown; + w->onKeyUp = onWidgetKeyUp; + w->onMouseDown = onWidgetMouseDown; + w->onMouseUp = onWidgetMouseUp; + w->onMouseMove = onWidgetMouseMove; + w->onScroll = onWidgetScroll; +} + + static BasValueT zeroValue(void) { BasValueT v; memset(&v, 0, sizeof(v)); diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h index 227dbd0..e73107f 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h @@ -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); diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.c b/src/apps/kpunch/dvxbasic/formrt/frmParser.c index ee4e664..1ee3b9e 100644 --- a/src/apps/kpunch/dvxbasic/formrt/frmParser.c +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.c @@ -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) { - if (cb->onFormBegin && !cb->onFormBegin(cb->userData, ctrlName)) { - return false; - } - - inForm = true; - - if (blockDepth < FRM_MAX_NESTING) { - blockStack[blockDepth++] = BLK_FORM; - } + blkType = BLK_FORM; + doPush = true; } else if (strcasecmp(typeName, "Menu") == 0 && inForm) { - if (cb->onMenuBegin) { - cb->onMenuBegin(cb->userData, ctrlName, menuNestDepth); - } - - menuNestDepth++; - - if (blockDepth < FRM_MAX_NESTING) { - blockStack[blockDepth++] = BLK_MENU; - } + blkType = BLK_MENU; + doPush = true; } else if (inForm) { - if (cb->onCtrlBegin) { - cb->onCtrlBegin(cb->userData, typeName, ctrlName); - } - - if (blockDepth < FRM_MAX_NESTING) { - blockStack[blockDepth++] = BLK_CTRL; - } + 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; + + case BLK_MENU: + if (cb->onMenuBegin) { + cb->onMenuBegin(cb->userData, ctrlName, menuNestDepth); + } + + menuNestDepth++; + break; + + case BLK_CTRL: + if (cb->onCtrlBegin) { + cb->onCtrlBegin(cb->userData, typeName, ctrlName); + } + + 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++; } diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c index 14d1b77..d231716 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c @@ -35,6 +35,7 @@ #include "stb_ds_wrap.h" #include +#include #include #include #include @@ -44,11 +45,15 @@ // Constants // ============================================================ -#define DEFAULT_FORM_W 400 -#define DEFAULT_FORM_H 300 -#define DEFAULT_CTRL_W 100 -#define DEFAULT_CTRL_H 30 -#define MIN_CTRL_SIZE 8 +#define DEFAULT_FORM_W 400 +#define DEFAULT_FORM_H 300 +#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; } @@ -440,7 +505,9 @@ static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const cha return; } - cp->index = -1; + 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)); - cp->index = -1; + + 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,33 +1032,22 @@ 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->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. + ds->mode = DSGN_REORDERING; + ds->dragStartY = y; + ds->form->dirty = true; } @@ -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; diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h index 1563af6..16cb53f 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h @@ -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); diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c index 32d1804..cee7666 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c @@ -80,6 +80,7 @@ #include #include #include +#include #include #include #include @@ -89,8 +90,13 @@ // Constants // ============================================================ -#define IDE_MAX_SOURCE 65536 -#define IDE_MAX_OUTPUT 32768 +#define IDE_MAX_SOURCE 65536 +#define IDE_MAX_OUTPUT 32768 +#define IDE_MAX_EVT_LABELS 64 // rows in event-dropdown label buffer +#define IDE_EVT_LABEL_LEN 32 // bytes per event label +#define IDE_NAME_BUF 64 // object/event/proc/status/value name buffer +#define IDE_WORD_BUF 32 // single-word scratch buffer +#define IDE_SOURCE_MARGIN 64 // Menu command IDs #define CMD_OPEN 100 @@ -151,6 +157,9 @@ #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 +#define IDE_KEY_CTRL_A 0x01 // Ctrl+A (select all); KEY_CTRL_A not in dvxTypes.h +#define IDE_OUTPUT_WIN_H 120 // Output/Immediate window height +#define IDE_OUTPUT_WIN_GAP 2 // gap between code window and output strip // Syntax color indices (used by basicColorize / classifyWord) #define SYNTAX_DEFAULT 0 @@ -203,12 +212,14 @@ static void debugUpdateWindows(void); static bool doEventsCallback(void *ctx); static void dsgnCopySelected(void); static void dsgnPasteControl(void); +static int32_t editorLineToCodeLine(int32_t editorLine); +static int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); static void ensureProject(const char *filePath); static void evaluateImmediate(const char *expr); static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize); static char *extractNewProcs(const char *buf); static const BasDebugVarT *findDebugVar(const char *name); -static bool findInProject(const char *needle, bool caseSensitive); +static bool findInProject(const char *needle, bool caseSensitive, bool forward); static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen); static void formatValue(const BasValueT *v, char *buf, int32_t bufSize); static void freeProcBufs(void); @@ -333,7 +344,7 @@ static void showLocalsWindow(void); static void showOutputWindow(void); static void showPreferencesDialog(void); static void showProc(int32_t procIdx); -static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive); +static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive, bool forward); static void showWatchWindow(void); static void stashCurrentFile(void); static void stashDesignerState(void); @@ -628,8 +639,8 @@ static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back // Procedure table for Object/Event dropdowns typedef struct { - char objName[64]; - char evtName[64]; + char objName[IDE_NAME_BUF]; + char evtName[IDE_NAME_BUF]; int32_t lineNum; // line of the SUB / FUNCTION declaration int32_t endLineNum; // line of the END SUB / END FUNCTION } IdeProcEntryT; @@ -1000,12 +1011,12 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo // Operators if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') { - colors[i++] = 5; // SYNTAX_OPERATOR + colors[i++] = SYNTAX_OPERATOR; continue; } // Default (whitespace, parens, etc.) - colors[i++] = 0; + colors[i++] = SYNTAX_DEFAULT; } } @@ -1031,12 +1042,6 @@ static void buildVmBreakpoints(void) { if (sProject.sourceMap[m].fileIdx == fileIdx) { int32_t base = sProject.sourceMap[m].startLine; - // .frm files have an injected BEGINFORM line - if (fileIdx >= 0 && fileIdx < sProject.fileCount && - sProject.files[fileIdx].isForm) { - base++; - } - int32_t vmLine = base + codeLine - 1; arrput(sVmBreakpoints, vmLine); break; @@ -1172,7 +1177,9 @@ static void buildWindow(void) { // File group WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open"); tbOpen->onClick = onTbOpen; - wgtSetTooltip(tbOpen, "Open (Ctrl+O)"); + // Button opens the project; Ctrl+O is bound to Add File, so the tooltip + // must not advertise a shortcut the button doesn't perform. + wgtSetTooltip(tbOpen, "Open Project..."); WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save"); tbSave->onClick = onTbSave; @@ -1251,9 +1258,9 @@ static void buildWindow(void) { // Converts to uppercase once, then does a single hash lookup. static uint8_t classifyWord(const char *word, int32_t wordLen) { - char upper[32]; + char upper[IDE_WORD_BUF]; - if (wordLen <= 0 || wordLen >= 32) { + if (wordLen <= 0 || wordLen >= IDE_WORD_BUF) { return SYNTAX_DEFAULT; } @@ -1714,7 +1721,13 @@ static void ideBuildCtrlMap(IdeValidatorCtxT *ctx) { if (!src) { int32_t dlen = 0; - diskBuf = platformReadFile(sProject.files[i].path, &dlen); + char fullPath[DVX_MAX_PATH]; + + // PrjFileT.path is project-relative; read via the full path so + // the control map isn't silently empty when the project dir + // differs from the process CWD. + prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); + diskBuf = platformReadFile(fullPath, &dlen); src = diskBuf; len = dlen; } @@ -1854,6 +1867,11 @@ static bool compileProject(void) { fileSrc = diskBuf; tmpDs.form->code = NULL; } else { + // No code section: the 'continue' below skips the + // tail free(diskBuf), so release the file buffer + // here to avoid leaking it. + free(diskBuf); + diskBuf = NULL; fileSrc = NULL; } @@ -1868,9 +1886,7 @@ static bool compileProject(void) { // Inject BEGINFORM directive for .frm code sections if (sProject.files[i].isForm && sProject.files[i].formName[0]) { - int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, - "BEGINFORM \"%s\"\n", sProject.files[i].formName); - pos += dirLen; + pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "BEGINFORM \"%s\"\n", sProject.files[i].formName); line++; } @@ -1881,8 +1897,12 @@ static bool compileProject(void) { int32_t fileLen = (int32_t)strlen(fileSrc); int32_t copyLen = fileLen; - if (pos + copyLen >= IDE_MAX_SOURCE - 64) { - copyLen = IDE_MAX_SOURCE - 64 - pos; + if (pos + copyLen >= IDE_MAX_SOURCE - IDE_SOURCE_MARGIN) { + copyLen = IDE_MAX_SOURCE - IDE_SOURCE_MARGIN - pos; + } + + if (copyLen < 0) { + copyLen = 0; } memcpy(concatBuf + pos, fileSrc, copyLen); @@ -1915,8 +1935,7 @@ static bool compileProject(void) { // Inject ENDFORM directive if (sProject.files[i].isForm && sProject.files[i].formName[0]) { - int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n"); - pos += dirLen; + pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "ENDFORM\n"); line++; } @@ -1965,17 +1984,17 @@ static bool compileProject(void) { // DXE. Those would silently drop the control at runtime and // produce a misleading "control not found" error on first access. if (arrlen(validatorCtx.badTypes) > 0) { - int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); + int32_t n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - 256; i++) { IdeBadTypeT *b = &validatorCtx.badTypes[i]; - n += snprintf(sOutputBuf + n, IDE_MAX_OUTPUT - n, - " Form '%s' control '%s': type '%s' is not registered.\n", - b->formName, b->ctrlName, b->typeName); + n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, + " Form '%s' control '%s': type '%s' is not registered.\n", + b->formName, b->ctrlName, b->typeName); } - n += snprintf(sOutputBuf + n, IDE_MAX_OUTPUT - n, - "\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n"); + n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, + "\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n"); sOutputLen = n; setOutputText(sOutputBuf); showOutputWindow(); @@ -2047,11 +2066,11 @@ static bool compileProject(void) { // Show the error with procedure name and proc-relative line int32_t n; if (procName[0]) { - n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n", - procName, (int)procLine, msg); + n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", + procName, (int)procLine, msg); } else { - n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n", - errFile, (int)errLocalLine, msg); + n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", + errFile, (int)errLocalLine, msg); } sOutputLen = n; setOutputText(sOutputBuf); @@ -2123,11 +2142,7 @@ static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void AppContextT *ac = (AppContextT *)ctx; // Convert editor line to file code line - int32_t codeLine = lineNum; - - if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { - codeLine = sProcTable[sCurProcIdx].lineNum + lineNum - 1; - } + int32_t codeLine = editorLineToCodeLine(lineNum); int32_t fileIdx = sProject.activeFileIdx; @@ -2357,37 +2372,45 @@ static void dsgnCopySelected(void) { return; } - // Serialize the selected control to FRM text - char buf[2048]; + // Serialize the selected control to FRM text. Sized to hold the + // worst case (DSGN_MAX_PROPS props x DSGN_MAX_TEXT values + chrome); + // emitClamped below guarantees no overflow even past this. Heap- + // allocated so this ~10KB buffer doesn't sit on the task stack. + int32_t bufSize = DSGN_MAX_PROPS * (DSGN_MAX_NAME + DSGN_MAX_TEXT + 8) + 512; + char *buf = (char *)malloc((size_t)bufSize); + + if (!buf) { + return; + } int32_t pos = 0; DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; - pos += snprintf(buf + pos, sizeof(buf) - pos, "Begin %s %s\n", ctrl->typeName, ctrl->name); + pos = emitClamped(buf, bufSize, pos,"Begin %s %s\n", ctrl->typeName, ctrl->name); if (ctrl->index >= 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " Index = %d\n", (int)ctrl->index); + pos = emitClamped(buf, bufSize, pos," Index = %d\n", (int)ctrl->index); } - pos += snprintf(buf + pos, sizeof(buf) - pos, " Caption = \"%s\"\n", wgtGetText(ctrl->widget) ? wgtGetText(ctrl->widget) : ""); + pos = emitClamped(buf, bufSize, pos," Caption = \"%s\"\n", wgtGetText(ctrl->widget) ? wgtGetText(ctrl->widget) : ""); if (ctrl->width > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " MinWidth = %d\n", (int)ctrl->width); + pos = emitClamped(buf, bufSize, pos," MinWidth = %d\n", (int)ctrl->width); } if (ctrl->height > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " MinHeight = %d\n", (int)ctrl->height); + pos = emitClamped(buf, bufSize, pos," MinHeight = %d\n", (int)ctrl->height); } if (ctrl->maxWidth > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " MaxWidth = %d\n", (int)ctrl->maxWidth); + pos = emitClamped(buf, bufSize, pos," MaxWidth = %d\n", (int)ctrl->maxWidth); } if (ctrl->maxHeight > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " MaxHeight = %d\n", (int)ctrl->maxHeight); + pos = emitClamped(buf, bufSize, pos," MaxHeight = %d\n", (int)ctrl->maxHeight); } if (ctrl->weight > 0) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " Weight = %d\n", (int)ctrl->weight); + pos = emitClamped(buf, bufSize, pos," Weight = %d\n", (int)ctrl->weight); } for (int32_t i = 0; i < ctrl->propCount; i++) { @@ -2395,7 +2418,7 @@ static void dsgnCopySelected(void) { continue; } - pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = \"%s\"\n", ctrl->props[i].name, ctrl->props[i].value); + pos = emitClamped(buf, bufSize, pos," %s = \"%s\"\n", ctrl->props[i].name, ctrl->props[i].value); } // Save interface properties @@ -2432,14 +2455,15 @@ static void dsgnCopySelected(void) { char valBuf[DSGN_MAX_TEXT]; if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, valBuf); + pos = emitClamped(buf, bufSize, pos," %s = %s\n", p->name, valBuf); } } } } - pos += snprintf(buf + pos, sizeof(buf) - pos, "End\n"); + pos = emitClamped(buf, bufSize, pos,"End\n"); dvxClipboardCopy(buf, pos); + free(buf); } @@ -2685,6 +2709,51 @@ static void dsgnPasteControl(void) { } +// Converts a 1-based editor line (within the current proc's buffer) to its +// 1-based line in the file's full code. For (General)/no-proc context +// (sCurProcIdx out of range) the editor line is the code line. +static int32_t editorLineToCodeLine(int32_t editorLine) { + if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { + return sProcTable[sCurProcIdx].lineNum + editorLine - 1; + } + + return editorLine; +} + + +// Clamped formatted append into a fixed buffer. Returns pos unchanged once +// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and +// returns the new position, capped at bufSize-1 on truncation. This avoids +// the snprintf-accumulation overrun where bufSize-pos wraps negative. + +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; + } + + 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; +} + + // Auto-create an implicit project when opening a file without one. // Derives project name and directory from the file path. Also adds // a matching .frm file if one exists alongside the .bas. @@ -3163,106 +3232,140 @@ static const BasDebugVarT *findDebugVar(const char *name) { } -static bool findInProject(const char *needle, bool caseSensitive) { +// procBufContains -- true if needle occurs anywhere in a proc buffer. +static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive) { + if (!hay) { + return false; + } + + if (caseSensitive) { + return strstr(hay, needle) != NULL; + } + + return findSubstrNoCase(hay, needle, needleLen) != NULL; +} + + +static bool findInProject(const char *needle, bool caseSensitive, bool forward) { if (!needle || !needle[0] || sProject.fileCount == 0) { return false; } int32_t needleLen = (int32_t)strlen(needle); + int32_t step = forward ? 1 : -1; // Stash current editor state so all buffers are up-to-date stashCurrentFile(); - // Start from the active file, searching from after the current selection - int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0; + // Start from the active file, searching from the current selection in + // the chosen direction. + int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0; + bool searchedEditorFile = false; // If the editor is open on the current file, try the current proc - // first (no wrap — returns false if no more matches ahead). + // first (no wrap -- returns false if no more matches that way). if (sEditor && sEditorFileIdx == startFile) { - if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) { + searchedEditorFile = true; + + if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward)) { return true; } - // No more matches in current proc — search remaining procs int32_t procCount = (int32_t)arrlen(sProcBufs); - for (int32_t p = sCurProcIdx + 1; p < procCount; p++) { - if (!sProcBufs[p]) { - continue; + // Procs on the far side of the current one, in scan order. + // Forward: procs after the current one, then General, then procs + // before it (wrap within file). Backward mirrors this exactly: + // procs before the current one (descending), then General, then + // procs after it (descending). + if (forward) { + for (int32_t p = sCurProcIdx + 1; p < procCount; p++) { + if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { + showProcAndFind(p, needle, caseSensitive, forward); + return true; + } } - const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); - - if (found) { - showProcAndFind(p, needle, caseSensitive); + if (sCurProcIdx >= 0 && procBufContains(sGeneralBuf, needle, needleLen, caseSensitive)) { + showProcAndFind(-1, needle, caseSensitive, forward); return true; } + + for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) { + if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { + showProcAndFind(p, needle, caseSensitive, forward); + return true; + } + } + } else { + for (int32_t p = sCurProcIdx - 1; p >= 0; p--) { + if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { + showProcAndFind(p, needle, caseSensitive, forward); + return true; + } + } + + if (sCurProcIdx != -1 && procBufContains(sGeneralBuf, needle, needleLen, caseSensitive)) { + showProcAndFind(-1, needle, caseSensitive, forward); + return true; + } + + for (int32_t p = procCount - 1; p > sCurProcIdx; p--) { + if (p < 0) { + break; + } + + if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { + showProcAndFind(p, needle, caseSensitive, forward); + return true; + } + } } - // Search General section if we started in a proc - if (sCurProcIdx >= 0 && sGeneralBuf) { - const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen); - - if (found) { - showProcAndFind(-1, needle, caseSensitive); - return true; - } - } - - // Search procs before the current one (wrap within file) - for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) { - if (!sProcBufs[p]) { - continue; - } - - const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); - - if (found) { - showProcAndFind(p, needle, caseSensitive); - return true; - } - } - - // Move to next file - startFile = (startFile + 1) % sProject.fileCount; + // Move to the adjacent file in scan order. + startFile = (startFile + step + sProject.fileCount) % sProject.fileCount; } - // Search remaining files, proc by proc. + // Search remaining files, proc by proc, in scan order. // startFile was advanced past the current file if we already searched it. int32_t filesToSearch = sProject.fileCount; - if (sEditor && sEditorFileIdx >= 0) { + if (searchedEditorFile) { filesToSearch--; // skip the file we already searched above } for (int32_t attempt = 0; attempt < filesToSearch; attempt++) { - int32_t fileIdx = (startFile + attempt) % sProject.fileCount; + int32_t fileIdx = (startFile + step * attempt + sProject.fileCount * sProject.fileCount) % sProject.fileCount; // Activate the file to load its proc buffers activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE); - // Search General section - if (sGeneralBuf) { - const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen); - - if (found) { - showProcAndFind(-1, needle, caseSensitive); - return true; - } - } - - // Search each procedure int32_t procCount = (int32_t)arrlen(sProcBufs); - for (int32_t p = 0; p < procCount; p++) { - if (!sProcBufs[p]) { - continue; + // Within each file, General precedes the numbered procs forward; + // backward visits the numbered procs (descending) then General. + if (forward) { + if (procBufContains(sGeneralBuf, needle, needleLen, caseSensitive)) { + showProcAndFind(-1, needle, caseSensitive, forward); + return true; } - const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); + for (int32_t p = 0; p < procCount; p++) { + if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { + showProcAndFind(p, needle, caseSensitive, forward); + return true; + } + } + } else { + for (int32_t p = procCount - 1; p >= 0; p--) { + if (procBufContains(sProcBufs[p], needle, needleLen, caseSensitive)) { + showProcAndFind(p, needle, caseSensitive, forward); + return true; + } + } - if (found) { - showProcAndFind(p, needle, caseSensitive); + if (procBufContains(sGeneralBuf, needle, needleLen, caseSensitive)) { + showProcAndFind(-1, needle, caseSensitive, forward); return true; } } @@ -3307,18 +3410,21 @@ static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) { break; } - int32_t pos = snprintf(buf, bufSize, "%s(", typeNameStr(arr->elementType)); + // emitClamped clamps pos to bufSize-1 on truncation, so a + // multi-dim array can't push pos past bufSize and make a later + // 'bufSize - pos' go negative (huge size_t -> OOB write). + int32_t pos = emitClamped(buf, bufSize, 0, "%s(", typeNameStr(arr->elementType)); for (int32_t d = 0; d < arr->dims && pos < bufSize - 10; d++) { if (d > 0) { - pos += snprintf(buf + pos, bufSize - pos, ", "); + pos = emitClamped(buf, bufSize, pos, ", "); } - pos += snprintf(buf + pos, bufSize - pos, "%d To %d", - (int)arr->lbound[d], (int)arr->ubound[d]); + pos = emitClamped(buf, bufSize, pos, "%d To %d", + (int)arr->lbound[d], (int)arr->ubound[d]); } - snprintf(buf + pos, bufSize - pos, ") [%d]", (int)arr->totalElements); + emitClamped(buf, bufSize, pos, ") [%d]", (int)arr->totalElements); break; } default: snprintf(buf, bufSize, "..."); break; @@ -3427,8 +3533,10 @@ static FindScopeE getFindScope(void) { } -// getFullSource -- reassemble all buffers into one source string -// Caller must free the returned buffer. +// getFullSource -- reassemble all buffers into one source string. +// Returns a pointer into an internal cache (sFullSourceCache) that is +// owned by getFullSource and freed on the next call. Callers must NOT +// free it; strdup it if the result must outlive the next call. static char *sFullSourceCache = NULL; @@ -3514,7 +3622,7 @@ static void handleEditCmd(int32_t cmd) { case CMD_COPY: case CMD_PASTE: case CMD_SELECT_ALL: { - static const int32_t keys[] = { 24, 3, 22, 1 }; + static const int32_t keys[] = { KEY_CTRL_X, KEY_CTRL_C, KEY_CTRL_V, IDE_KEY_CTRL_A }; int32_t key = keys[cmd - CMD_CUT]; WindowT *target = getLastFocusWin(); @@ -3890,7 +3998,7 @@ static void handleViewCmd(int32_t cmd) { // Rebuild menu bar preview if (sFormWin) { - wmDestroyMenuBar(sFormWin); + wmDestroyMenuBar(sFormWin, &sAc->display); dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form); dvxInvalidateWindow(sAc, sFormWin); } @@ -4633,8 +4741,7 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS // Append prompt to output if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) { - int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt); - sOutputLen += n; + sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, "%s", prompt); setOutputText(sOutputBuf); } @@ -4853,32 +4960,22 @@ static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fal // The editor shows one procedure at a time (sCurProcIdx), so we need // to add the procedure's starting line offset within the file's code. // For multi-file projects, we also add the file's offset in the -// concatenated source. For .frm files, an injected BEGINFORM directive -// adds one extra line before the code. +// concatenated source. The source map's startLine already accounts for +// the injected BEGINFORM directive, so no extra adjustment is needed. static int32_t localToConcatLine(int32_t editorLine) { - int32_t fileLine = editorLine; - // Add the current procedure's start offset within the file's code. // sCurProcIdx == -1 means (General) section which starts at line 1 // of the file's code (not the concatenated source). - if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { - fileLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1; - } + int32_t fileLine = editorLineToCodeLine(editorLine); // For projects, add the file's offset in the concatenated source. - // For .frm files, startLine points to the injected BEGINFORM line, - // so the actual code starts at startLine + 1. + // startLine is recorded AFTER the injected BEGINFORM line, so it + // already points at the first real code line (matching prjMapLine). if (sProject.sourceMapCount > 0 && sProject.activeFileIdx >= 0) { for (int32_t i = 0; i < sProject.sourceMapCount; i++) { if (sProject.sourceMap[i].fileIdx == sProject.activeFileIdx) { int32_t base = sProject.sourceMap[i].startLine; - // .frm files have an injected BEGINFORM line before the code - if (sProject.activeFileIdx < sProject.fileCount && - sProject.files[sProject.activeFileIdx].isForm) { - base++; - } - return base + fileLine - 1; } } @@ -5033,10 +5130,13 @@ static void makeExecutable(void) { } // Ask debug or release - const char *modeItems[] = { "Debug (include error info)" }; + const char *modeItems[] = { + "Debug (include error info)", + "Release (smaller, no debug info)" + }; int32_t modeChoice = 0; - if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) { + if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, (int32_t)(sizeof(modeItems) / sizeof(modeItems[0])), 0, &modeChoice)) { return; } @@ -5121,10 +5221,18 @@ static void makeExecutable(void) { return; } - fwrite(stubData, 1, stubSize, outFile); - fclose(outFile); + size_t stubWritten = fwrite(stubData, 1, stubSize, outFile); + int stubClose = fclose(outFile); free(stubData); + if (stubWritten != stubSize || stubClose != 0) { + setStatus("Failed writing stub to output file."); + free(modData); + free(dbgData); + dvxSetBusy(sAc, false); + return; + } + // Build parallel form arrays: load each form, strip comments. // The loop preserves the prior quirk where a malloc failure skips the // resource but still advances the index -- formData[i] is left NULL @@ -5226,7 +5334,7 @@ static void makeExecutable(void) { spec.formData = (const uint8_t *const *)frmData; spec.formLens = frmLens; - basBuildEmitResources(outPath, &spec); + int32_t emitRc = basBuildEmitResources(outPath, &spec); free(modData); free(dbgData); @@ -5286,6 +5394,11 @@ static void makeExecutable(void) { dvxSetBusy(sAc, false); + if (emitRc != 0) { + setStatus("Failed writing resources to output file."); + return; + } + char msg[512]; snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug"); setStatus(msg); @@ -5535,6 +5648,15 @@ static void newProject(void) { static void onBreakpointHit(void *ctx, int32_t line) { (void)ctx; + + // The VM fires this with line == -1 at end-of-sub after a breakpointed + // handler finishes; that is not a real pause point (every genuine pause -- + // breakpoint or Step Into/Over/Out -- arrives with a positive line), so + // ignore it. Otherwise Continue would re-pause and require a second press. + if (line < 0) { + return; + } + sDbgState = DBG_PAUSED; if (sVm) { @@ -5831,8 +5953,11 @@ static void onClose(WindowT *win) { static void onCodeWinClose(WindowT *win) { - // Stash code back before the window is destroyed. - stashFormCode(); + // Stash code back before the window is destroyed. Use stashCurrentFile() + // (not stashFormCode()) so a plain .bas file's editor contents are written + // back to its project buffer too -- stashFormCode() is a no-op for + // non-form files and would discard unsaved .bas edits on close/reopen. + stashCurrentFile(); dvxDestroyWindow(sAc, win); sCodeWin = NULL; @@ -5901,11 +6026,7 @@ static void onEditorChange(WidgetT *w) { int32_t cursorLine = wgtTextAreaGetCursorLine(sEditor); // Convert editor cursor line to file code line - int32_t editCodeLine = cursorLine; - - if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { - editCodeLine = sProcTable[sCurProcIdx].lineNum + cursorLine - 1; - } + int32_t editCodeLine = editorLineToCodeLine(cursorLine); bool changed = false; @@ -5984,7 +6105,7 @@ static void onEvtDropdownChange(WidgetT *w) { } // Strip brackets if present (unimplemented event) - char evtName[64]; + char evtName[IDE_NAME_BUF]; if (selEvt[0] == '[') { snprintf(evtName, sizeof(evtName), "%s", selEvt + 1); int32_t len = (int32_t)strlen(evtName); @@ -6063,7 +6184,7 @@ static void onFindNext(WidgetT *w) { setStatus("Not found."); } } else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) { - if (!findInProject(sFindText, caseSens)) { + if (!findInProject(sFindText, caseSens, forward)) { setStatus("Not found."); } } @@ -6443,7 +6564,7 @@ static const char *sFormEvents[] = { }; // Buffer for event dropdown labels (with [] for unimplemented) -static char sEvtLabelBufs[64][32]; +static char sEvtLabelBufs[IDE_MAX_EVT_LABELS][IDE_EVT_LABEL_LEN]; static void onObjDropdownChange(WidgetT *w) { (void)w; @@ -6555,11 +6676,11 @@ static void onObjDropdownChange(WidgetT *w) { } } - if (labelIdx < 64) { + if (labelIdx < IDE_MAX_EVT_LABELS) { if (hasCode) { - snprintf(sEvtLabelBufs[labelIdx], 32, "%s", availEvents[i]); + snprintf(sEvtLabelBufs[labelIdx], IDE_EVT_LABEL_LEN, "%s", availEvents[i]); } else { - snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", availEvents[i]); + snprintf(sEvtLabelBufs[labelIdx], IDE_EVT_LABEL_LEN, "[%s]", availEvents[i]); } arrput(sEvtItems, sEvtLabelBufs[labelIdx]); @@ -6595,11 +6716,11 @@ static void onObjDropdownChange(WidgetT *w) { } } - if (labelIdx < 64) { + if (labelIdx < IDE_MAX_EVT_LABELS) { if (hasCode) { - snprintf(sEvtLabelBufs[labelIdx], 32, "%s", evtName); + snprintf(sEvtLabelBufs[labelIdx], IDE_EVT_LABEL_LEN, "%s", evtName); } else { - snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", evtName); + snprintf(sEvtLabelBufs[labelIdx], IDE_EVT_LABEL_LEN, "[%s]", evtName); } arrput(sEvtItems, sEvtLabelBufs[labelIdx]); @@ -6619,14 +6740,14 @@ static void onObjDropdownChange(WidgetT *w) { // Strip brackets for comparison if (label[0] == '[') { label++; } - if (strncasecmp(label, existingEvts[i], strlen(existingEvts[i])) == 0) { + if (strcasecmp(label, existingEvts[i]) == 0) { alreadyListed = true; break; } } - if (!alreadyListed && labelIdx < 64) { - snprintf(sEvtLabelBufs[labelIdx], 32, "%s", existingEvts[i]); + if (!alreadyListed && labelIdx < IDE_MAX_EVT_LABELS) { + snprintf(sEvtLabelBufs[labelIdx], IDE_EVT_LABEL_LEN, "%s", existingEvts[i]); arrput(sEvtItems, sEvtLabelBufs[labelIdx]); labelIdx++; } @@ -6686,7 +6807,7 @@ static void onObjDropdownChange(WidgetT *w) { label++; } - if (strncasecmp(label, defEvt, strlen(defEvt)) == 0) { + if (strcasecmp(label, defEvt) == 0) { wgtDropdownSetSelected(sEvtDropdown, i); onEvtDropdownChange(sEvtDropdown); break; @@ -6743,14 +6864,21 @@ static void onReplace(WidgetT *w) { snprintf(sFindText, sizeof(sFindText), "%s", needle); snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : ""); - // If text is selected and matches the search, replace it, then find next - const char *edText = wgtGetText(sEditor); + bool caseSens = getFindMatchCase(); + bool forward = getFindForward(); - if (edText) { - // TODO: replace current selection if it matches, then find next - // For now, just do find next - onFindNext(w); + // Single Replace always acts on the editor currently shown: if its + // selection matches the search term, splice in the replacement and + // advance to the next match; otherwise just find next. Find Next's + // own scope/direction handling (which may switch procs or files) is + // only reached when there is no matching selection to replace, so a + // cross-file single replace stays unambiguous -- consistent with how + // onReplaceAll treats each scope as a sequence of in-editor passes. + if (wgtTextAreaReplaceSelection(sEditor, sFindText, sReplaceText, caseSens, forward)) { + return; } + + onFindNext(w); } @@ -6783,8 +6911,18 @@ static void onReplaceAll(WidgetT *w) { for (int32_t i = 0; i < sProject.fileCount; i++) { activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE); - if (sEditor) { - totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); + // wgtTextAreaReplaceAll only touches the proc currently shown + // in the editor, so visit every proc of this file (General + + // each Sub/Function), exactly as the ScopeFileE path does -- + // otherwise only the (General) section gets replaced. + int32_t procCount = (int32_t)arrlen(sProcBufs); + + for (int32_t p = -1; p < procCount; p++) { + showProc(p); + + if (sEditor) { + totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); + } } } } else if (scope == ScopeFileE && sEditor) { @@ -6818,7 +6956,7 @@ static void onReplaceAll(WidgetT *w) { } } - char statusBuf[64]; + char statusBuf[IDE_NAME_BUF]; snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount); setStatus(statusBuf); } @@ -7239,6 +7377,12 @@ static void printCallback(void *ctx, const char *text, bool newline) { sOutputBuf[sOutputLen++] = '\n'; } + // Defend against a caller that accumulated past the cap: never index the + // terminator out of bounds. + if (sOutputLen >= IDE_MAX_OUTPUT) { + sOutputLen = IDE_MAX_OUTPUT - 1; + } + sOutputBuf[sOutputLen] = '\0'; // Update the output textarea immediately so PRINT is visible @@ -7490,6 +7634,7 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne int32_t oldLen = (int32_t)strlen(oldName); int32_t newLen = (int32_t)strlen(newName); int32_t srcLen = (int32_t)strlen(src); + bool skipComments = prefsGetBool(sPrefs, "editor", "renameSkipComments", true); // First pass: count replacements to compute output size int32_t count = 0; @@ -7509,7 +7654,7 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne continue; } - if (prefsGetBool(sPrefs, "editor", "renameSkipComments", true) && isInStringOrComment(src, i)) { + if (skipComments && isInStringOrComment(src, i)) { continue; } @@ -7538,7 +7683,7 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne if ((after == '.' || after == '_') && (i == 0 || !isIdentChar(src[i - 1])) && - (!prefsGetBool(sPrefs, "editor", "renameSkipComments", true) || !isInStringOrComment(src, i))) { + (!skipComments || !isInStringOrComment(src, i))) { memcpy(out + op, newName, newLen); op += newLen; i += oldLen; @@ -7656,7 +7801,7 @@ static void runModule(BasModuleT *mod) { sDbgState = DBG_RUNNING; // Set project help file on form runtime for F1 context help - if (sProject.helpFile[0]) { + if (formRt && sProject.helpFile[0]) { snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.helpFile); } @@ -7746,11 +7891,11 @@ static void runModule(BasModuleT *mod) { // VB-style event loop: after module-level code finishes, // keep processing events as long as any form is loaded. // The program ends when all forms are unloaded (closed). - if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) { + if (result == BAS_VM_HALTED && formRt && (int32_t)arrlen(formRt->forms) > 0) { setStatus("Running (event loop)..."); sStopRequested = false; - while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) { + while (sWin && sAc->running && formRt && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) { if (sDbgState == DBG_PAUSED) { // Paused inside an event handler @@ -7831,24 +7976,22 @@ static void runModule(BasModuleT *mod) { navigateToCodeLine(errFileIdx, errLocalLine, procName[0] ? procName : NULL, false); } - int32_t pos = sOutputLen; - int32_t n; - + // emitClamped clamps the running length to IDE_MAX_OUTPUT-1 on + // truncation; a bare snprintf would return the would-be length and + // push sOutputLen past the cap, later OOB-writing the NUL terminator. if (procName[0]) { - n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, - "\nRUNTIME ERROR:\n%s line %d: %s\n", - procName, (int)procLine, vm->errorMsg); + sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, + "\nRUNTIME ERROR:\n%s line %d: %s\n", + procName, (int)procLine, vm->errorMsg); } else if (errFile[0]) { - n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, - "\nRUNTIME ERROR:\n%s line %d: %s\n", - errFile, (int)errLocalLine, vm->errorMsg); + sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, + "\nRUNTIME ERROR:\n%s line %d: %s\n", + errFile, (int)errLocalLine, vm->errorMsg); } else { - n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, - "\nRUNTIME ERROR:\nline %d: %s\n", - (int)errLocalLine, vm->errorMsg); + sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, + "\nRUNTIME ERROR:\nline %d: %s\n", + (int)errLocalLine, vm->errorMsg); } - - sOutputLen += n; } sVm = NULL; @@ -8118,16 +8261,45 @@ static bool saveCurProc(void) { } } - // Extract extra procs into their own buffers + // Extract extra procs into their own buffers. extractNewProcs + // returns leftover (duplicate-named) blocks in a malloc'd + // buffer; append them to the General section so the compiler + // reports the duplicate rather than silently losing the code. if (extPos > 0) { - extractNewProcs(extras); + char *remaining = extractNewProcs(extras); + + if (remaining && remaining[0]) { + int32_t genLen = sGeneralBuf ? (int32_t)strlen(sGeneralBuf) : 0; + int32_t remLen = (int32_t)strlen(remaining); + char *merged = (char *)malloc((size_t)(genLen + remLen + 2)); + + if (merged) { + int32_t mpos = 0; + + if (genLen > 0) { + memcpy(merged, sGeneralBuf, (size_t)genLen); + mpos = genLen; + merged[mpos++] = '\n'; + } + + memcpy(merged + mpos, remaining, (size_t)remLen); + mpos += remLen; + merged[mpos] = '\0'; + free(sGeneralBuf); + sGeneralBuf = merged; + } + } + + free(remaining); } free(extras); - // Update editor to show only this proc + // Update editor to show only this proc. Extraction keeps the + // owned proc at sProcBufs[sCurProcIdx] and only appends new + // procs to the end, so no earlier index shifts -- report false. wgtSetText(sEditor, sProcBufs[sCurProcIdx]); - return true; + return false; } } } @@ -8206,7 +8378,7 @@ static void selectDropdowns(const char *objName, const char *evtName) { label++; } - if (strncasecmp(label, evtName, strlen(evtName)) == 0) { + if (strcasecmp(label, evtName) == 0) { wgtDropdownSetSelected(sEvtDropdown, i); break; } @@ -8332,7 +8504,7 @@ static void showCodeWindow(void) { } int32_t codeY = toolbarBottom(); - int32_t codeH = sAc->display.height - codeY - 122; + int32_t codeH = sAc->display.height - codeY - (IDE_OUTPUT_WIN_H + IDE_OUTPUT_WIN_GAP); sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true); @@ -8406,7 +8578,7 @@ static void showImmediateWindow(void) { return; } - int32_t outH = 120; + int32_t outH = IDE_OUTPUT_WIN_H; int32_t outY = sAc->display.height - outH; sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true); @@ -8485,7 +8657,7 @@ static void showOutputWindow(void) { return; } - int32_t outH = 120; + int32_t outH = IDE_OUTPUT_WIN_H; int32_t outY = sAc->display.height - outH; sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); @@ -8769,7 +8941,7 @@ static void showProc(int32_t procIdx) { // showProcAndFind -- switch to a procedure, sync the dropdowns, and // select the search match in the editor. -static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive) { +static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive, bool forward) { showProc(procIdx); // Sync the Object/Event dropdowns to match @@ -8780,7 +8952,16 @@ static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensit } if (sEditor) { - return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true); + // showProc leaves the cursor at the top of the proc, which is the + // wrong anchor for a backward scan (it would find nothing). When + // searching backward, move the cursor to the true end of the buffer + // (offset == len) so the scan walks upward through every match, + // including any on the final line of a proc with no trailing newline. + if (!forward) { + wgtTextAreaCursorToEnd(sEditor); + } + + return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward); } return false; @@ -9053,11 +9234,7 @@ static void toggleBreakpointLine(int32_t editorLine) { int32_t fileIdx = sProject.activeFileIdx; // Convert editor line to file code line by adding proc offset - int32_t codeLine = editorLine; - - if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { - codeLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1; - } + int32_t codeLine = editorLineToCodeLine(editorLine); // Check if this breakpoint already exists — remove it for (int32_t i = 0; i < sBreakpointCount; i++) { @@ -9432,6 +9609,23 @@ static void updateDropdowns(void) { return; } + // Collect all known object names once; they only depend on sDesigner.form + // and never change during the proc scan below. + const char *objNames[512]; + int32_t objNameCount = 0; + + if (sDesigner.form) { + objNames[objNameCount++] = sDesigner.form->name; + + for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) { + objNames[objNameCount++] = sDesigner.form->controls[ci]->name; + } + + for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) { + objNames[objNameCount++] = sDesigner.form->menuItems[mi].name; + } + } + // Scan line by line for SUB / FUNCTION const char *pos = src; int32_t lineNum = 1; @@ -9449,7 +9643,7 @@ static void updateDropdowns(void) { if (isSub || isFunc) { pos = dvxSkipWs(pos + (isSub ? 4 : 9)); - char procName[64]; + char procName[IDE_NAME_BUF]; int32_t nameLen = 0; while (*pos && *pos != '(' && *pos != ' ' && *pos != '\t' && *pos != '\n' && *pos != '\r' && nameLen < 63) { @@ -9506,31 +9700,15 @@ static void updateDropdowns(void) { // "This_Is_A_Dumb_Name" matching a control named "This"). bool isEvent = false; - if (sDesigner.form) { - // Collect all known object names - const char *objNames[512]; - int32_t objNameCount = 0; + // Try each known object name as prefix + "_" (table hoisted above). + for (int32_t oi = 0; oi < objNameCount; oi++) { + int32_t nameLen = (int32_t)strlen(objNames[oi]); - objNames[objNameCount++] = sDesigner.form->name; - - for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) { - objNames[objNameCount++] = sDesigner.form->controls[ci]->name; - } - - for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) { - objNames[objNameCount++] = sDesigner.form->menuItems[mi].name; - } - - // Try each object name as prefix + "_" - for (int32_t oi = 0; oi < objNameCount; oi++) { - int32_t nameLen = (int32_t)strlen(objNames[oi]); - - if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') { - snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]); - snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1); - isEvent = true; - break; - } + if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') { + snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]); + snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1); + isEvent = true; + break; } } @@ -9691,7 +9869,7 @@ static void updateLocalsWindow(void) { char nameBuf[BAS_MAX_PROC_NAME]; char typeBuf[16]; - char valueBuf[64]; + char valueBuf[IDE_NAME_BUF]; snprintf(nameBuf, sizeof(nameBuf), "%s", dv->name); diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c index 9272e89..270e905 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c @@ -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; @@ -247,7 +249,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { DsgnMenuItemT mi; memset(&mi, 0, sizeof(mi)); mi.enabled = true; - mi.visible = true; + mi.visible = true; arrput(sMed.items, mi); } @@ -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 @@ -603,7 +622,7 @@ static void onNext(WidgetT *w) { DsgnMenuItemT mi; memset(&mi, 0, sizeof(mi)); mi.enabled = true; - mi.visible = true; + mi.visible = true; if (count > 0) { mi.level = sMed.items[count - 1].level; @@ -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; diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c index b8379ec..8ac4c00 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c @@ -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); } diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.h b/src/apps/kpunch/dvxbasic/ide/ideProject.h index 5621150..59a8b56 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.h +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.h @@ -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 ") + 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; // ============================================================ diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c index 85190be..97f328f 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c @@ -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); - - 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); - 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); - } + bool ok = propDropdownOrInput(sPrpCtx, propName, "Select column:", fieldNames, fieldCount, curValue, newValue, sizeof(newValue)); free(fieldNames); + + if (!ok) { + return; + } } 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); - - 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); - 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); - } + bool ok = propDropdownOrInput(sPrpCtx, propName, "Select table:", tableNames, tableCount, curValue, newValue, sizeof(newValue)); free(tableNames); + + if (!ok) { + return; + } } 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; } diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs index 43868d9..6665eb4 100644 --- a/src/apps/kpunch/dvxbasic/langref.dhs +++ b/src/apps/kpunch/dvxbasic/langref.dhs @@ -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 diff --git a/src/apps/kpunch/dvxbasic/runtime/basErrors.h b/src/apps/kpunch/dvxbasic/runtime/basErrors.h new file mode 100644 index 0000000..8c9791c --- /dev/null +++ b/src/apps/kpunch/dvxbasic/runtime/basErrors.h @@ -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 diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c index 25f1cac..eb4ca40 100644 --- a/src/apps/kpunch/dvxbasic/runtime/serialize.c +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c @@ -56,6 +56,7 @@ #include #include #include +#include // 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 - mod->constants = (BasStringT **)calloc(mod->constCount, sizeof(BasStringT *)); + if (mod->constCount > (r.len - r.pos) / BAS_CONST_MIN_ENTRY) { + 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)); - free(s); + // 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 ? 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; } diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.h b/src/apps/kpunch/dvxbasic/runtime/serialize.h index 08b7d6c..e8d3573 100644 --- a/src/apps/kpunch/dvxbasic/runtime/serialize.h +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.h @@ -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. diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c index 694da68..e3a2112 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.c +++ b/src/apps/kpunch/dvxbasic/runtime/values.c @@ -35,6 +35,8 @@ #include #include +#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++) { - arr->elements[i].type = elementType; + // 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); - s->data[newLen] = '\0'; - s->len = newLen; + + 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); } diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h index 6214ee5..b0bbdf2 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.h +++ b/src/apps/kpunch/dvxbasic/runtime/values.h @@ -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); diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c index ecd4513..8633d04 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.c +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c @@ -29,6 +29,7 @@ #include "vm.h" #include "../compiler/opcodes.h" +#include "basErrors.h" #include #include @@ -50,10 +51,46 @@ // calling doEvents on every instruction. #define SUB_YIELD_INTERVAL 500 +// Capacity of the work buffers used by the numeric formatter shared by +// OP_PRINT_USING and OP_FORMAT. Bounds the worst-case output of a +// pathological format string or numeric magnitude. +#define BAS_FORMAT_BUF_SIZE 256 + +// Fixed (non-argument) stack operands an OP_CALL_METHOD carries: the +// method-name value and the control-reference value, both below the args. +#define BAS_CALL_METHOD_FIXED_OPERANDS 2 + +// Work-buffer capacities for the OP_INI_READ / OP_INI_WRITE opcodes: the +// longest INI line we will process, and the longest section/key name. +#define BAS_INI_LINE_LEN 512 // max INI line length +#define BAS_INI_NAME_LEN 256 // max INI section/key name length + +// File attribute bits returned/accepted by GETATTR/SETATTR (BASIC vbReadOnly/ +// vbDirectory). +#define BAS_ATTR_READONLY 1 // file is read-only +#define BAS_ATTR_DIRECTORY 16 // path is a directory + +// Divergence between the PRINT USING and FORMAT$ numeric formatters, +// captured as options so a single helper serves both call sites. +typedef struct BasNumFormatT { + bool hasDecimal; + bool hasComma; + bool plusAtStart; + bool plusAtEnd; + bool minusAtEnd; + bool asteriskFill; + bool dollarFloat; + bool zeroPad; + bool formatPad; + int32_t digitsBefore; + int32_t digitsAfter; +} BasNumFormatT; + static BasCallFrameT *currentFrame(BasVmT *vm); static void defaultPrint(void *ctx, const char *text, bool newline); -static void dirClose(void); -static const char *dirNext(void); +static void dirClose(BasVmT *vm); +static const char *dirNext(BasVmT *vm); +static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize); static BasVmResultE execArith(BasVmT *vm, uint8_t op); static BasVmResultE execCompare(BasVmT *vm, uint8_t op); static BasVmResultE execFileOp(BasVmT *vm, uint8_t op); @@ -62,90 +99,30 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op); static BasVmResultE execMath(BasVmT *vm, uint8_t op); static BasVmResultE execPrint(BasVmT *vm); static BasVmResultE execStringOp(BasVmT *vm, uint8_t op); +static bool operandFits(BasVmT *vm, int32_t size); static bool pop(BasVmT *vm, BasValueT *val); +static void popCallFrame(BasVmT *vm); +static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen); static bool push(BasVmT *vm, BasValueT val); static int16_t readInt16(BasVmT *vm); static uint16_t readUint16(BasVmT *vm); static uint8_t readUint8(BasVmT *vm); +static void releaseVmState(BasVmT *vm); static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning); static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg); +static bool validArrayDims(BasVmT *vm, int32_t dims); bool basVmCallSub(BasVmT *vm, int32_t codeAddr) { - if (!vm || !vm->module) { - return false; - } - - // Reject calls while debugger is paused (break mode between events) - if (vm->debugPaused) { - return false; - } - - if (codeAddr < 0 || codeAddr >= vm->module->codeLen) { - return false; - } - - if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) { - return false; - } - - // Save VM state - int32_t savedPc = vm->pc; - int32_t savedCallDepth = vm->callDepth; - bool savedRunning = vm->running; - - // Push a call frame that returns to an invalid address (sentinel) - // We detect completion when callDepth drops back to savedCallDepth - BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; - frame->returnPc = savedPc; - frame->localCount = BAS_VM_MAX_LOCALS; - frame->errorHandler = 0; - memset(frame->locals, 0, sizeof(frame->locals)); - - // Jump to the SUB - vm->pc = codeAddr; - vm->running = true; - - return runSubLoop(vm, savedPc, savedCallDepth, savedRunning); + // Thin wrapper: no args, no out-args. basVmCallSubWithArgsOut handles + // NULL args / zero counts and contains the full validation + frame setup. + return basVmCallSubWithArgsOut(vm, codeAddr, NULL, 0, NULL, 0); } bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount) { - if (!vm || !vm->module) { - return false; - } - - if (vm->debugPaused) { - return false; - } - - if (codeAddr < 0 || codeAddr >= vm->module->codeLen) { - return false; - } - - if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) { - return false; - } - - int32_t savedPc = vm->pc; - int32_t savedCallDepth = vm->callDepth; - bool savedRunning = vm->running; - - BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; - frame->returnPc = savedPc; - frame->localCount = BAS_VM_MAX_LOCALS; - frame->errorHandler = 0; - memset(frame->locals, 0, sizeof(frame->locals)); - - // Set arguments as locals (parameter 0 = local 0, etc.) - for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) { - frame->locals[i] = basValCopy(args[i]); - } - - vm->pc = codeAddr; - vm->running = true; - - return runSubLoop(vm, savedPc, savedCallDepth, savedRunning); + // Thin wrapper: in-args only, no out-args. + return basVmCallSubWithArgsOut(vm, codeAddr, args, argCount, NULL, 0); } @@ -183,18 +160,25 @@ bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args vm->pc = codeAddr; vm->running = true; + // Arrange for the returning frame's locals to be copied into outArgs by + // OP_RET/OP_RET_VAL, BEFORE those opcodes release the locals. Reading + // them after runSubLoop returns would be a use-after-free because the + // RET path has already released every local in the frame. The fields + // are not reentrant, so save and restore them around the call in case + // the running sub itself makes another out-arg call. + BasValueT *savedOutArgs = vm->outArgs; + int32_t savedOutArgCount = vm->outArgCount; + int32_t savedOutArgFrame = vm->outArgFrame; + + vm->outArgs = (outArgs && outCount > 0) ? outArgs : NULL; + vm->outArgCount = outCount; + vm->outArgFrame = savedCallDepth; + bool ok = runSubLoop(vm, savedPc, savedCallDepth, savedRunning); - // Read back modified locals before the frame is reused. - // The frame at savedCallDepth still has the data even - // though callDepth was decremented by RET. - if (ok && outArgs && outCount > 0) { - BasCallFrameT *doneFrame = &vm->callStack[savedCallDepth]; - - for (int32_t i = 0; i < outCount && i < BAS_VM_MAX_LOCALS; i++) { - outArgs[i] = basValCopy(doneFrame->locals[i]); - } - } + vm->outArgs = savedOutArgs; + vm->outArgCount = savedOutArgCount; + vm->outArgFrame = savedOutArgFrame; return ok; } @@ -211,6 +195,7 @@ BasVmT *basVmCreate(void) { vm->stepOverDepth = -1; vm->stepOutDepth = -1; vm->runToCursorLine = -1; + vm->outArgFrame = -1; basStringSystemInit(); return vm; } @@ -221,28 +206,8 @@ void basVmDestroy(BasVmT *vm) { return; } - // Release stack values - for (int32_t i = 0; i < vm->sp; i++) { - basValRelease(&vm->stack[i]); - } - - // Release global variables - for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) { - basValRelease(&vm->globals[i]); - } - - // Release call frame locals - for (int32_t d = 0; d < vm->callDepth; d++) { - for (int32_t i = 0; i < vm->callStack[d].localCount; i++) { - basValRelease(&vm->callStack[d].locals[i]); - } - } - - // Release FOR stack - for (int32_t i = 0; i < vm->forDepth; i++) { - basValRelease(&vm->forStack[i].limit); - basValRelease(&vm->forStack[i].step); - } + // Release eval stack, globals, call-frame locals and FOR-stack values + releaseVmState(vm); // Close files for (int32_t i = 0; i < BAS_VM_MAX_FILES; i++) { @@ -252,7 +217,7 @@ void basVmDestroy(BasVmT *vm) { } // Close Dir$ iterator - dirClose(); + dirClose(vm); basStringSystemShutdown(); free(vm); @@ -265,7 +230,7 @@ int32_t basVmGetCurrentLine(const BasVmT *vm) { const char *basVmGetError(const BasVmT *vm) { - return vm->errorMsg; + return vm ? vm->errorMsg : ""; } @@ -294,6 +259,21 @@ void basVmLoadModule(BasVmT *vm, BasModuleT *module) { } } } + + // Prime the implicit main (module-level) frame. Module-level code + // runs in callStack[0]; without callDepth >= 1 the error dispatcher's + // 'while (callDepth > 0)' unwind never inspects that frame, so a + // module-level ON ERROR GOTO never traps. The IDE and test harness + // set this by hand; doing it here makes every entry path (including + // compiled apps via the stub) consistent. localCount lets RET and + // frame-local cleanup see the right slot count. + vm->callDepth = 1; + vm->callStack[0].localCount = module->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : module->globalCount; + vm->callStack[0].errorHandler = 0; + // Module-level vars are globals, so frame-0 locals are unused, but + // clear them so a future re-load into a used VM can't release stale + // values when the error dispatcher or teardown walks localCount. + memset(vm->callStack[0].locals, 0, sizeof(vm->callStack[0].locals)); } @@ -308,19 +288,20 @@ bool basVmPush(BasVmT *vm, BasValueT val) { void basVmReset(BasVmT *vm) { - for (int32_t i = 0; i < vm->sp; i++) { - basValRelease(&vm->stack[i]); - } - - for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) { - basValRelease(&vm->globals[i]); - } + // Release eval stack, globals, call-frame locals and FOR-stack values + // before the counts are zeroed below (the release loops need them). + releaseVmState(vm); vm->sp = 0; + vm->stmtSp = 0; vm->callDepth = 0; vm->forDepth = 0; + vm->outArgs = NULL; + vm->outArgCount = 0; + vm->outArgFrame = -1; vm->pc = vm->module ? vm->module->entryPoint : 0; vm->running = false; + vm->badOperand = false; vm->yielded = false; vm->dataPtr = 0; vm->errorHandler = 0; @@ -379,6 +360,18 @@ BasVmResultE basVmRun(BasVmT *vm) { } if (target != 0) { + // Release any operands the failed statement half-pushed + // before the error fired, then truncate the eval stack + // back to the statement boundary so trapped errors in a + // loop don't leak values and grow sp without bound. + if (vm->sp > vm->stmtSp) { + for (int32_t si = vm->stmtSp; si < vm->sp; si++) { + basValRelease(&vm->stack[si]); + } + + vm->sp = vm->stmtSp; + } + vm->errorPc = savedPc; vm->errorNextPc = vm->pc; vm->inErrorHandler = true; @@ -475,6 +468,16 @@ void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui) { BasVmResultE basVmStep(BasVmT *vm) { + // A prior step's operand read ran past code[] and clamped pc to codeLen. + // Check this BEFORE the halt guard below, otherwise pc == codeLen would + // report a clean BAS_VM_HALTED and mask the corruption. badOperand can + // only have been set while vm->module was non-NULL, so no NULL check here. + if (vm->badOperand) { + vm->badOperand = false; + vm->running = false; + return BAS_VM_BAD_OPCODE; + } + if (!vm->module || vm->pc < 0 || vm->pc >= vm->module->codeLen) { vm->running = false; return BAS_VM_HALTED; @@ -514,9 +517,12 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_PUSH_FLT32: { - float val; - memcpy(&val, &vm->module->code[vm->pc], sizeof(float)); - vm->pc += sizeof(float); + float val = 0.0f; + + if (operandFits(vm, (int32_t)sizeof(float))) { + memcpy(&val, &vm->module->code[vm->pc], sizeof(float)); + vm->pc += sizeof(float); + } if (!push(vm, basValSingle(val))) { return BAS_VM_STACK_OVERFLOW; @@ -526,9 +532,12 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_PUSH_FLT64: { - double val; - memcpy(&val, &vm->module->code[vm->pc], sizeof(double)); - vm->pc += sizeof(double); + double val = 0.0; + + if (operandFits(vm, (int32_t)sizeof(double))) { + memcpy(&val, &vm->module->code[vm->pc], sizeof(double)); + vm->pc += sizeof(double); + } if (!push(vm, basValDouble(val))) { return BAS_VM_STACK_OVERFLOW; @@ -581,7 +590,10 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_STACK_UNDERFLOW; } - if (!push(vm, basValCopy(vm->stack[vm->sp - 1]))) { + BasValueT dup = basValCopy(vm->stack[vm->sp - 1]); + + if (!push(vm, dup)) { + basValRelease(&dup); return BAS_VM_STACK_OVERFLOW; } @@ -597,15 +609,17 @@ BasVmResultE basVmStep(BasVmT *vm) { BasCallFrameT *frame = currentFrame(vm); if (!frame || idx >= (uint16_t)frame->localCount) { - runtimeError(vm, 9, "Invalid local variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index"); return BAS_VM_ERROR; } // ByRef: if the local holds a reference, dereference it BasValueT *slot = &frame->locals[idx]; BasValueT val = (slot->type == BAS_TYPE_REF) ? *slot->refVal : *slot; + BasValueT lv = basValCopy(val); - if (!push(vm, basValCopy(val))) { + if (!push(vm, lv)) { + basValRelease(&lv); return BAS_VM_STACK_OVERFLOW; } @@ -623,7 +637,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (!frame || idx >= (uint16_t)frame->localCount) { basValRelease(&val); - runtimeError(vm, 9, "Invalid local variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index"); return BAS_VM_ERROR; } @@ -645,11 +659,14 @@ BasVmResultE basVmStep(BasVmT *vm) { uint16_t idx = readUint16(vm); if (idx >= BAS_VM_MAX_GLOBALS) { - runtimeError(vm, 9, "Invalid global variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid global variable index"); return BAS_VM_ERROR; } - if (!push(vm, basValCopy(vm->globals[idx]))) { + BasValueT gv = basValCopy(vm->globals[idx]); + + if (!push(vm, gv)) { + basValRelease(&gv); return BAS_VM_STACK_OVERFLOW; } @@ -666,7 +683,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (idx >= BAS_VM_MAX_GLOBALS) { basValRelease(&val); - runtimeError(vm, 9, "Invalid global variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid global variable index"); return BAS_VM_ERROR; } @@ -680,7 +697,7 @@ BasVmResultE basVmStep(BasVmT *vm) { BasCallFrameT *frame = currentFrame(vm); if (!frame || idx >= (uint16_t)frame->localCount) { - runtimeError(vm, 9, "Invalid local variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index"); return BAS_VM_ERROR; } @@ -699,7 +716,7 @@ BasVmResultE basVmStep(BasVmT *vm) { uint16_t idx = readUint16(vm); if (idx >= BAS_VM_MAX_GLOBALS) { - runtimeError(vm, 9, "Invalid global variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid global variable index"); return BAS_VM_ERROR; } @@ -722,7 +739,7 @@ BasVmResultE basVmStep(BasVmT *vm) { BasValueT *top = &vm->stack[vm->sp - 1]; if (top->type != BAS_TYPE_REF || !top->refVal) { - runtimeError(vm, 9, "Expected reference"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Expected reference"); return BAS_VM_ERROR; } @@ -741,7 +758,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (ref.type != BAS_TYPE_REF || !ref.refVal) { basValRelease(&val); - runtimeError(vm, 9, "Expected reference"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Expected reference"); return BAS_VM_ERROR; } @@ -774,9 +791,13 @@ BasVmResultE basVmStep(BasVmT *vm) { BasValueT *top = &vm->stack[vm->sp - 1]; if (top->type == BAS_TYPE_INTEGER) { - top->intVal = -top->intVal; + // Unsigned negation avoids signed-overflow UB on INT16_MIN while + // preserving the two's-complement wrap the runtime relies on. + top->intVal = (int16_t)(0u - (unsigned)top->intVal); } else if (top->type == BAS_TYPE_LONG) { - top->longVal = -top->longVal; + // Same wrap: -INT32_MIN must round-trip to INT32_MIN so the QB + // idiom PRINT -2147483648 (LONG_MIN) yields the wrapped literal. + top->longVal = (int32_t)(0u - (uint32_t)top->longVal); } else { double n = basValToNumber(*top); basValRelease(top); @@ -933,31 +954,7 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_HALTED; } - BasCallFrameT *frame = &vm->callStack[--vm->callDepth]; - - // Release locals - for (int32_t i = 0; i < frame->localCount; i++) { - basValRelease(&frame->locals[i]); - } - - bool hadHandler = (frame->errorHandler != 0); - frame->errorHandler = 0; - vm->pc = frame->returnPc; - - // Restore the active handler to whatever the caller had set - BasCallFrameT *caller = currentFrame(vm); - vm->errorHandler = caller ? caller->errorHandler : 0; - - // If this SUB owned the active error handler, any handler - // body that ran inside it is now done -- clear the flag so - // the next error (in this or another SUB) can trap again. - // QBASIC documents this: an unresumed handler is cleared - // when the procedure that installed it returns. - if (hadHandler) { - vm->inErrorHandler = false; - vm->errorNumber = 0; - vm->errorMsg[0] = '\0'; - } + popCallFrame(vm); break; } @@ -975,24 +972,7 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_HALTED; } - BasCallFrameT *frame = &vm->callStack[--vm->callDepth]; - - for (int32_t i = 0; i < frame->localCount; i++) { - basValRelease(&frame->locals[i]); - } - - bool hadHandler = (frame->errorHandler != 0); - frame->errorHandler = 0; - vm->pc = frame->returnPc; - - BasCallFrameT *caller = currentFrame(vm); - vm->errorHandler = caller ? caller->errorHandler : 0; - - if (hadHandler) { - vm->inErrorHandler = false; - vm->errorNumber = 0; - vm->errorMsg[0] = '\0'; - } + popCallFrame(vm); if (!push(vm, retVal)) { basValRelease(&retVal); @@ -1029,7 +1009,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (vm->forDepth >= BAS_VM_MAX_FOR_DEPTH) { basValRelease(&stepVal); basValRelease(&limitVal); - runtimeError(vm, 26, "FOR loop nesting too deep"); + runtimeError(vm, BAS_ERR_FOR_NESTING, "FOR loop nesting too deep"); return BAS_VM_ERROR; } @@ -1062,6 +1042,13 @@ BasVmResultE basVmStep(BasVmT *vm) { } } + // ByRef loop variable (e.g. FOR over a BYREF SUB parameter): + // the local slot holds a reference, so operate on the caller's + // storage just as OP_LOAD_LOCAL/OP_STORE_LOCAL do. + if (varSlot && varSlot->type == BAS_TYPE_REF) { + varSlot = varSlot->refVal; + } + if (varSlot) { double curVal = basValToNumber(*varSlot); double stepNum = basValToNumber(fs->step); @@ -1085,14 +1072,14 @@ BasVmResultE basVmStep(BasVmT *vm) { int16_t loopTopOffset = readInt16(vm); if (vm->forDepth <= 0) { - runtimeError(vm, 1, "NEXT without FOR"); + runtimeError(vm, BAS_ERR_NEXT_WITHOUT_FOR, "NEXT without FOR"); return BAS_VM_ERROR; } BasForStateT *fs = &vm->forStack[vm->forDepth - 1]; if (fs->varIdx != (int32_t)varIdx) { - runtimeError(vm, 1, "NEXT variable mismatch"); + runtimeError(vm, BAS_ERR_NEXT_WITHOUT_FOR, "NEXT variable mismatch"); return BAS_VM_ERROR; } @@ -1103,27 +1090,33 @@ BasVmResultE basVmStep(BasVmT *vm) { BasCallFrameT *frame = currentFrame(vm); if (!frame || varIdx >= (uint16_t)frame->localCount) { - runtimeError(vm, 9, "Invalid local variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index"); return BAS_VM_ERROR; } varSlot = &frame->locals[varIdx]; } else if (scopeTag == SCOPE_FORM) { if (!vm->currentFormVars || varIdx >= (uint16_t)vm->currentFormVarCount) { - runtimeError(vm, 9, "Invalid form variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid form variable index"); return BAS_VM_ERROR; } varSlot = &vm->currentFormVars[varIdx]; } else { if (varIdx >= BAS_VM_MAX_GLOBALS) { - runtimeError(vm, 9, "Invalid global variable index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid global variable index"); return BAS_VM_ERROR; } varSlot = &vm->globals[varIdx]; } + // ByRef loop variable: dereference so the increment writes the + // caller's storage, matching OP_STORE_LOCAL. + if (varSlot->type == BAS_TYPE_REF) { + varSlot = varSlot->refVal; + } + // Increment: var = var + step. Preserve the loop variable's // original type so extern calls that pass it as an int32_t // argument don't get an 8-byte double instead. Exception: @@ -1144,9 +1137,23 @@ BasVmResultE basVmStep(BasVmT *vm) { if (stepIsFractional) { *varSlot = basValDouble(varVal); } else if (varType == BAS_TYPE_INTEGER) { - *varSlot = basValInteger((int16_t)varVal); + // Promote out-of-range counters instead of truncating, so the + // slot read back next iteration matches the un-truncated varVal + // used by the continue test (otherwise an INTEGER counter that + // crosses 32767 wraps and the loop never terminates). + if (varVal >= (double)INT16_MIN && varVal <= (double)INT16_MAX) { + *varSlot = basValInteger((int16_t)varVal); + } else if (varVal >= (double)INT32_MIN && varVal <= (double)INT32_MAX) { + *varSlot = basValLong((int32_t)varVal); + } else { + *varSlot = basValDouble(varVal); + } } else if (varType == BAS_TYPE_LONG) { - *varSlot = basValLong((int32_t)varVal); + if (varVal >= (double)INT32_MIN && varVal <= (double)INT32_MAX) { + *varSlot = basValLong((int32_t)varVal); + } else { + *varSlot = basValDouble(varVal); + } } else if (varType == BAS_TYPE_SINGLE) { *varSlot = basValSingle((float)varVal); } else { @@ -1177,7 +1184,7 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_FOR_POP: { if (vm->forDepth <= 0) { - runtimeError(vm, 1, "FOR stack underflow"); + runtimeError(vm, BAS_ERR_NEXT_WITHOUT_FOR, "FOR stack underflow"); return BAS_VM_ERROR; } @@ -1203,31 +1210,9 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } - case OP_CONV_FLT_INT: { - if (vm->sp < 1) { - return BAS_VM_STACK_UNDERFLOW; - } - - BasValueT *top = &vm->stack[vm->sp - 1]; - BasValueT conv = basValToInteger(*top); - basValRelease(top); - *top = conv; - break; - } - - case OP_CONV_INT_STR: { - if (vm->sp < 1) { - return BAS_VM_STACK_UNDERFLOW; - } - - BasValueT *top = &vm->stack[vm->sp - 1]; - BasValueT conv = basValToString(*top); - basValRelease(top); - *top = conv; - break; - } - - case OP_CONV_STR_INT: { + case OP_CONV_FLT_INT: + case OP_CONV_STR_INT: + case OP_CONV_LONG_INT: { if (vm->sp < 1) { return BAS_VM_STACK_UNDERFLOW; } @@ -1239,6 +1224,7 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_CONV_INT_STR: case OP_CONV_FLT_STR: { if (vm->sp < 1) { return BAS_VM_STACK_UNDERFLOW; @@ -1275,18 +1261,6 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } - case OP_CONV_LONG_INT: { - if (vm->sp < 1) { - return BAS_VM_STACK_UNDERFLOW; - } - - BasValueT *top = &vm->stack[vm->sp - 1]; - BasValueT conv = basValToInteger(*top); - basValRelease(top); - *top = conv; - break; - } - // ============================================================ // I/O // ============================================================ @@ -1395,7 +1369,7 @@ BasVmResultE basVmStep(BasVmT *vm) { const char *fmt = fmtStr.strVal->data; int32_t fmtLen = fmtStr.strVal->len; - char buf[256]; + char buf[BAS_FORMAT_BUF_SIZE]; if (val.type == BAS_TYPE_STRING) { // String formatting @@ -1418,6 +1392,15 @@ BasVmResultE basVmStep(BasVmT *vm) { } width = i + 2; } + + // The format is a runtime string, so width is + // attacker-controlled; clamp so the pad loop and the + // buf[width] terminator stay inside buf (overflowed + // the interpreter stack otherwise). + if (width > BAS_FORMAT_BUF_SIZE - 1) { + width = BAS_FORMAT_BUF_SIZE - 1; + } + int32_t srcLen = (int32_t)strlen(src); int32_t copyLen = srcLen < width ? srcLen : width; memcpy(buf, src, copyLen); @@ -1505,93 +1488,22 @@ BasVmResultE basVmStep(BasVmT *vm) { snprintf(sciFmt, sizeof(sciFmt), "%%.%dE", (int)decimals); snprintf(buf, sizeof(buf), sciFmt, n); } else { - // Standard formatting - bool isNeg = (n < 0); - double absN = isNeg ? -n : n; - int32_t decimals = hasDecimal ? digitsAfter : 0; + // Standard formatting via the shared bounded helper. + BasNumFormatT f; - char numBuf[128]; - snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN); + f.hasDecimal = hasDecimal; + f.hasComma = hasComma; + f.plusAtStart = plusAtStart; + f.plusAtEnd = plusAtEnd; + f.minusAtEnd = minusAtEnd; + f.asteriskFill = asteriskFill; + f.dollarFloat = dollarFloat; + f.zeroPad = false; + f.formatPad = false; + f.digitsBefore = digitsBefore; + f.digitsAfter = digitsAfter; - // Split into integer and decimal parts - char intPart[128]; - char decPart[128]; - intPart[0] = '\0'; - decPart[0] = '\0'; - char *dot = strchr(numBuf, '.'); - if (dot) { - int32_t intLen = (int32_t)(dot - numBuf); - memcpy(intPart, numBuf, intLen); - intPart[intLen] = '\0'; - strncpy(decPart, dot + 1, sizeof(decPart) - 1); - decPart[sizeof(decPart) - 1] = '\0'; - } else { - strncpy(intPart, numBuf, sizeof(intPart) - 1); - intPart[sizeof(intPart) - 1] = '\0'; - } - - // Apply thousands separator - char fmtIntPart[128]; - if (hasComma) { - int32_t srcLen = (int32_t)strlen(intPart); - int32_t dstIdx = 0; - for (int32_t i = 0; i < srcLen; i++) { - if (i > 0 && (srcLen - i) % 3 == 0) { - fmtIntPart[dstIdx++] = ','; - } - fmtIntPart[dstIdx++] = intPart[i]; - } - fmtIntPart[dstIdx] = '\0'; - } else { - strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1); - fmtIntPart[sizeof(fmtIntPart) - 1] = '\0'; - } - - // Build result - int32_t idx = 0; - - // Sign prefix - if (plusAtStart) { - buf[idx++] = isNeg ? '-' : '+'; - } else if (isNeg && !minusAtEnd) { - buf[idx++] = '-'; - } - - // Dollar sign - if (dollarFloat) { - buf[idx++] = '$'; - } - - // Pad leading - int32_t intLen = (int32_t)strlen(fmtIntPart); - int32_t padNeeded = digitsBefore - intLen; - char fillChar = asteriskFill ? '*' : ' '; - - for (int32_t i = 0; i < padNeeded; i++) { - buf[idx++] = fillChar; - } - - // Integer part - for (int32_t i = 0; fmtIntPart[i]; i++) { - buf[idx++] = fmtIntPart[i]; - } - - // Decimal part - if (hasDecimal) { - buf[idx++] = '.'; - for (int32_t i = 0; i < decimals; i++) { - buf[idx++] = decPart[i] ? decPart[i] : '0'; - } - } - - // Trailing sign - if (plusAtEnd) { - buf[idx++] = isNeg ? '-' : '+'; - } else if (minusAtEnd) { - buf[idx++] = isNeg ? '-' : ' '; - } - - buf[idx] = '\0'; + formatNumber(n, &f, buf, sizeof(buf)); } } @@ -1848,9 +1760,12 @@ BasVmResultE basVmStep(BasVmT *vm) { } BasStringT *s = basStringAlloc(count + 1); - memset(s->data, ch, count); - s->data[count] = '\0'; - s->len = count; + + if (s->cap >= count + 1) { + memset(s->data, ch, count); + s->data[count] = '\0'; + s->len = count; + } if (!push(vm, basValString(s))) { basStringUnref(s); @@ -2086,7 +2001,7 @@ BasVmResultE basVmStep(BasVmT *vm) { BasUdtT *udt = basUdtNew(typeId, fieldCount); if (!udt) { - runtimeError(vm, 7, "Out of memory allocating TYPE"); + runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating TYPE"); return BAS_VM_OUT_OF_MEMORY; } @@ -2102,6 +2017,10 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + if (!validArrayDims(vm, dims)) { + return BAS_VM_ERROR; + } + // For UDT arrays, parser pushes typeId and fieldCount after bounds int32_t udtTypeId = -1; int32_t udtFieldCnt = 0; @@ -2142,7 +2061,7 @@ BasVmResultE basVmStep(BasVmT *vm) { BasArrayT *arr = basArrayNew(dims, lbounds, ubounds, elementType); if (!arr) { - runtimeError(vm, 7, "Out of memory allocating array"); + runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating array"); return BAS_VM_OUT_OF_MEMORY; } @@ -2153,7 +2072,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (!udt) { basArrayFree(arr); - runtimeError(vm, 7, "Out of memory allocating TYPE array elements"); + runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating TYPE array elements"); return BAS_VM_OUT_OF_MEMORY; } @@ -2176,6 +2095,11 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_LOAD_ARRAY: { uint8_t dims = readUint8(vm); + + if (!validArrayDims(vm, dims)) { + return BAS_VM_ERROR; + } + int32_t indices[BAS_ARRAY_MAX_DIMS]; // Pop indices in reverse order @@ -2199,7 +2123,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { basValRelease(&arrRef); - runtimeError(vm, 13, "Not an array"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array"); return BAS_VM_TYPE_MISMATCH; } @@ -2207,7 +2131,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (flatIdx < 0) { basValRelease(&arrRef); - runtimeError(vm, 9, "Subscript out of range"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range"); return BAS_VM_SUBSCRIPT_RANGE; } @@ -2229,6 +2153,11 @@ BasVmResultE basVmStep(BasVmT *vm) { // the local it came from, so the element memory is stable // for the duration of the call. uint8_t dims = readUint8(vm); + + if (!validArrayDims(vm, dims)) { + return BAS_VM_ERROR; + } + int32_t indices[BAS_ARRAY_MAX_DIMS]; for (int32_t d = dims - 1; d >= 0; d--) { @@ -2250,7 +2179,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { basValRelease(&arrRef); - runtimeError(vm, 13, "Not an array"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array"); return BAS_VM_TYPE_MISMATCH; } @@ -2258,7 +2187,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (flatIdx < 0) { basValRelease(&arrRef); - runtimeError(vm, 9, "Subscript out of range"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range"); return BAS_VM_SUBSCRIPT_RANGE; } @@ -2277,6 +2206,10 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_STORE_ARRAY: { uint8_t dims = readUint8(vm); + if (!validArrayDims(vm, dims)) { + return BAS_VM_ERROR; + } + // Pop value to store BasValueT storeVal; @@ -2310,7 +2243,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { basValRelease(&arrRef); basValRelease(&storeVal); - runtimeError(vm, 13, "Not an array"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array"); return BAS_VM_TYPE_MISMATCH; } @@ -2319,7 +2252,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (flatIdx < 0) { basValRelease(&arrRef); basValRelease(&storeVal); - runtimeError(vm, 9, "Subscript out of range"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range"); return BAS_VM_SUBSCRIPT_RANGE; } @@ -2333,6 +2266,10 @@ BasVmResultE basVmStep(BasVmT *vm) { uint8_t dims = readUint8(vm); uint16_t fieldIdx = readUint16(vm); + if (!validArrayDims(vm, dims)) { + return BAS_VM_ERROR; + } + // Pop value to store BasValueT storeVal; @@ -2366,7 +2303,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { basValRelease(&arrRef); basValRelease(&storeVal); - runtimeError(vm, 13, "Not an array"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array"); return BAS_VM_TYPE_MISMATCH; } @@ -2375,7 +2312,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (flatIdx < 0) { basValRelease(&arrRef); basValRelease(&storeVal); - runtimeError(vm, 9, "Subscript out of range"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range"); return BAS_VM_SUBSCRIPT_RANGE; } @@ -2385,14 +2322,14 @@ BasVmResultE basVmStep(BasVmT *vm) { if (elem->type != BAS_TYPE_UDT || !elem->udtVal) { basValRelease(&arrRef); basValRelease(&storeVal); - runtimeError(vm, 13, "Array element is not a TYPE instance"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Array element is not a TYPE instance"); return BAS_VM_TYPE_MISMATCH; } if (fieldIdx >= (uint16_t)elem->udtVal->fieldCount) { basValRelease(&arrRef); basValRelease(&storeVal); - runtimeError(vm, 9, "Invalid field index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid field index"); return BAS_VM_ERROR; } @@ -2406,6 +2343,10 @@ BasVmResultE basVmStep(BasVmT *vm) { uint8_t dims = readUint8(vm); uint8_t preserve = readUint8(vm); + if (!validArrayDims(vm, dims)) { + return BAS_VM_ERROR; + } + int32_t lbounds[BAS_ARRAY_MAX_DIMS]; int32_t ubounds[BAS_ARRAY_MAX_DIMS]; @@ -2440,7 +2381,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (!newArr) { basValRelease(&oldRef); - runtimeError(vm, 7, "Out of memory in REDIM"); + runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory in REDIM"); return BAS_VM_OUT_OF_MEMORY; } @@ -2453,6 +2394,10 @@ BasVmResultE basVmStep(BasVmT *vm) { } for (int32_t i = 0; i < copyCount; i++) { + // basArrayNew pre-initialized STRING elements with an + // empty-string ref; release it before overwriting so + // the copy doesn't drop that reference on the floor. + basValRelease(&newArr->elements[i]); newArr->elements[i] = basValCopy(oldRef.arrVal->elements[i]); } } @@ -2501,13 +2446,13 @@ BasVmResultE basVmStep(BasVmT *vm) { if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { basValRelease(&arrRef); - runtimeError(vm, 13, "Not an array"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array"); return BAS_VM_TYPE_MISMATCH; } if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) { basValRelease(&arrRef); - runtimeError(vm, 9, "Invalid dimension for LBOUND"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid dimension for LBOUND"); return BAS_VM_SUBSCRIPT_RANGE; } @@ -2531,13 +2476,13 @@ BasVmResultE basVmStep(BasVmT *vm) { if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { basValRelease(&arrRef); - runtimeError(vm, 13, "Not an array"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array"); return BAS_VM_TYPE_MISMATCH; } if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) { basValRelease(&arrRef); - runtimeError(vm, 9, "Invalid dimension for UBOUND"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid dimension for UBOUND"); return BAS_VM_SUBSCRIPT_RANGE; } @@ -2561,13 +2506,13 @@ BasVmResultE basVmStep(BasVmT *vm) { if (udtRef.type != BAS_TYPE_UDT || !udtRef.udtVal) { basValRelease(&udtRef); - runtimeError(vm, 13, "Not a TYPE instance"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance"); return BAS_VM_TYPE_MISMATCH; } if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) { basValRelease(&udtRef); - runtimeError(vm, 9, "Invalid field index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid field index"); return BAS_VM_ERROR; } @@ -2600,14 +2545,14 @@ BasVmResultE basVmStep(BasVmT *vm) { if (udtRef.type != BAS_TYPE_UDT || !udtRef.udtVal) { basValRelease(&udtRef); basValRelease(&storeVal); - runtimeError(vm, 13, "Not a TYPE instance"); + runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance"); return BAS_VM_TYPE_MISMATCH; } if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) { basValRelease(&udtRef); basValRelease(&storeVal); - runtimeError(vm, 9, "Invalid field index"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid field index"); return BAS_VM_ERROR; } @@ -2623,7 +2568,7 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_READ_DATA: { if (!vm->module || vm->dataPtr >= vm->module->dataCount) { - runtimeError(vm, 4, "Out of DATA"); + runtimeError(vm, BAS_ERR_OUT_OF_DATA, "Out of DATA"); return BAS_VM_ERROR; } @@ -2659,7 +2604,7 @@ BasVmResultE basVmStep(BasVmT *vm) { double n = basValToNumber(val); basValRelease(&val); - char buf[256]; + char buf[BAS_FORMAT_BUF_SIZE]; buf[0] = '\0'; // Check for "percent" format @@ -2714,115 +2659,22 @@ BasVmResultE basVmStep(BasVmT *vm) { } } - int32_t decimals = hashAfter + zeroAfter; - bool isNeg = (n < 0); - double absN = isNeg ? -n : n; + // Format via the shared bounded helper. + BasNumFormatT f; - // Format the number - char numBuf[128]; + f.hasDecimal = hasDecimal; + f.hasComma = hasComma; + f.plusAtStart = plusStart; + f.plusAtEnd = plusEnd; + f.minusAtEnd = minusEnd; + f.asteriskFill = false; + f.dollarFloat = false; + f.zeroPad = (zeroBefore > 0); + f.formatPad = true; + f.digitsBefore = hashBefore + zeroBefore; + f.digitsAfter = hashAfter + zeroAfter; - if (hasDecimal) { - snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN); - } else { - snprintf(numBuf, sizeof(numBuf), "%.0f", absN); - } - - // Split into integer and decimal parts - char intPart[128]; - char decPart[128]; - intPart[0] = '\0'; - decPart[0] = '\0'; - - char *dot = strchr(numBuf, '.'); - - if (dot) { - int32_t intLen = (int32_t)(dot - numBuf); - memcpy(intPart, numBuf, intLen); - intPart[intLen] = '\0'; - strncpy(decPart, dot + 1, sizeof(decPart) - 1); - decPart[sizeof(decPart) - 1] = '\0'; - } else { - strncpy(intPart, numBuf, sizeof(intPart) - 1); - intPart[sizeof(intPart) - 1] = '\0'; - } - - // Apply thousands separator - char fmtIntPart[128]; - - if (hasComma) { - int32_t srcLen = (int32_t)strlen(intPart); - int32_t dstIdx = 0; - - for (int32_t i = 0; i < srcLen; i++) { - if (i > 0 && (srcLen - i) % 3 == 0) { - fmtIntPart[dstIdx++] = ','; - } - fmtIntPart[dstIdx++] = intPart[i]; - } - - fmtIntPart[dstIdx] = '\0'; - } else { - strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1); - fmtIntPart[sizeof(fmtIntPart) - 1] = '\0'; - } - - // Pad integer part with leading zeros if format has 0's - int32_t totalIntDigits = hashBefore + zeroBefore; - int32_t curIntLen = (int32_t)strlen(fmtIntPart); - - // Build result - int32_t idx = 0; - - // Sign prefix - if (plusStart || plusEnd) { - if (isNeg) { - buf[idx++] = '-'; - } else if (plusStart) { - buf[idx++] = '+'; - } - } else if (isNeg) { - buf[idx++] = '-'; - } - - // Pad with leading spaces or zeros - int32_t padNeeded = totalIntDigits - curIntLen; - - for (int32_t i = 0; i < padNeeded; i++) { - if (i < padNeeded - (int32_t)strlen(intPart)) { - // Positions before the number - if (zeroBefore > 0) { - buf[idx++] = '0'; - } else { - buf[idx++] = ' '; - } - } else { - buf[idx++] = '0'; - } - } - - // Integer part - for (int32_t i = 0; fmtIntPart[i]; i++) { - buf[idx++] = fmtIntPart[i]; - } - - // Decimal part - if (hasDecimal) { - buf[idx++] = '.'; - for (int32_t i = 0; decPart[i] && i < decimals; i++) { - buf[idx++] = decPart[i]; - } - } - - // Trailing sign - if (plusEnd && !isNeg) { - buf[idx++] = '+'; - } else if (minusEnd && isNeg) { - buf[idx++] = '-'; - } else if (minusEnd && !isNeg) { - buf[idx++] = ' '; - } - - buf[idx] = '\0'; + formatNumber(n, &f, buf, sizeof(buf)); } basValRelease(&fmtStr); @@ -2939,11 +2791,34 @@ BasVmResultE basVmStep(BasVmT *vm) { BasValueT ctrlRefVal; // Pop args in reverse - BasValueT args[16]; - int32_t argCount = argc < 16 ? argc : 16; + BasValueT args[BAS_VM_MAX_CALL_ARGS]; + int32_t argCount = argc; + + // Reject an over-limit argc rather than silently truncating, which + // would leak the un-popped args and desync the eval stack. Drain + // every pushed operand (args + method name + control ref) first to + // keep the stack balanced, then raise the error. + if (argc > BAS_VM_MAX_CALL_ARGS) { + int32_t drainCount = argc + BAS_CALL_METHOD_FIXED_OPERANDS; + + for (int32_t i = 0; i < drainCount; i++) { + BasValueT tmp; + + if (pop(vm, &tmp)) { + basValRelease(&tmp); + } + } + + runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Too many arguments in method call"); + return BAS_VM_ERROR; + } for (int32_t i = argCount - 1; i >= 0; i--) { if (!pop(vm, &args[i])) { + for (int32_t j = i + 1; j < argCount; j++) { + basValRelease(&args[j]); + } + return BAS_VM_STACK_UNDERFLOW; } } @@ -3318,11 +3193,17 @@ BasVmResultE basVmStep(BasVmT *vm) { uint16_t idx = readUint16(vm); if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) { - runtimeError(vm, 9, "Form variable access outside form context"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Form variable access outside form context"); return BAS_VM_ERROR; } - push(vm, basValCopy(vm->currentFormVars[idx])); + BasValueT fv = basValCopy(vm->currentFormVars[idx]); + + if (!push(vm, fv)) { + basValRelease(&fv); + return BAS_VM_STACK_OVERFLOW; + } + break; } @@ -3336,7 +3217,7 @@ BasVmResultE basVmStep(BasVmT *vm) { if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) { basValRelease(&val); - runtimeError(vm, 9, "Form variable access outside form context"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Form variable access outside form context"); return BAS_VM_ERROR; } @@ -3349,7 +3230,7 @@ BasVmResultE basVmStep(BasVmT *vm) { uint16_t idx = readUint16(vm); if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) { - runtimeError(vm, 9, "Form variable address outside form context"); + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Form variable address outside form context"); return BAS_VM_ERROR; } @@ -3413,7 +3294,7 @@ BasVmResultE basVmStep(BasVmT *vm) { const char *fn = funcNameIdx < (uint16_t)vm->module->constCount ? vm->module->constants[funcNameIdx]->data : "?"; snprintf(msg, sizeof(msg), "External function not found: %s", fn); - runtimeError(vm, 453, msg); + runtimeError(vm, BAS_ERR_EXTERNAL_NOT_FOUND, msg); return BAS_VM_ERROR; } @@ -3427,11 +3308,32 @@ BasVmResultE basVmStep(BasVmT *vm) { } // Pop arguments (reverse order) - BasValueT args[16]; - int32_t argCount = argc < 16 ? argc : 16; + BasValueT args[BAS_VM_MAX_CALL_ARGS]; + int32_t argCount = argc; + + // Reject an over-limit argc rather than silently truncating, which + // would leak the un-popped args and desync the eval stack. Drain + // the pushed args (extern has no extra fixed operands) to keep the + // stack balanced, then raise the error. + if (argc > BAS_VM_MAX_CALL_ARGS) { + for (int32_t i = 0; i < argc; i++) { + BasValueT tmp; + + if (pop(vm, &tmp)) { + basValRelease(&tmp); + } + } + + runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Too many arguments in external call"); + return BAS_VM_ERROR; + } for (int32_t i = argCount - 1; i >= 0; i--) { if (!pop(vm, &args[i])) { + for (int32_t j = i + 1; j < argCount; j++) { + basValRelease(&args[j]); + } + return BAS_VM_STACK_UNDERFLOW; } } @@ -3464,6 +3366,11 @@ BasVmResultE basVmStep(BasVmT *vm) { uint16_t lineNum = readUint16(vm); vm->currentLine = lineNum; + // Snapshot the eval-stack depth at this statement boundary so an + // ON ERROR GOTO dispatch can release any operands left half-pushed + // by an error mid-expression and reset sp to here. + vm->stmtSp = vm->sp; + // Step into: break at any OP_LINE if (vm->debugBreak) { vm->debugBreak = false; @@ -3529,9 +3436,12 @@ BasVmResultE basVmStep(BasVmT *vm) { BasStringT *defStr = basValFormatString(defVal); const char *result = defStr->data; + // line[] must outlive the block below: on a key match 'result' + // points into it and is consumed by the push() after fclose(). + char line[BAS_INI_LINE_LEN]; + FILE *fp = fopen(fileStr->data, "r"); if (fp) { - char line[512]; bool inSection = false; while (fgets(line, sizeof(line), fp)) { // Strip trailing whitespace @@ -3540,7 +3450,9 @@ BasVmResultE basVmStep(BasVmT *vm) { *end-- = '\0'; } char *p = (char *)dvxSkipWs(line); - if (*p == '\0' || *p == ';' || *p == '#') { continue; } + if (*p == '\0' || *p == ';' || *p == '#') { + continue; + } if (*p == '[') { char *rb = strchr(p, ']'); if (rb) { @@ -3555,7 +3467,9 @@ BasVmResultE basVmStep(BasVmT *vm) { *eq = '\0'; char *k = p; char *ke = eq - 1; - while (ke >= k && (*ke == ' ' || *ke == '\t')) { *ke-- = '\0'; } + while (ke >= k && (*ke == ' ' || *ke == '\t')) { + *ke-- = '\0'; + } if (strcasecmp(k, keyStr->data) == 0) { result = dvxSkipWs(eq + 1); break; @@ -3603,12 +3517,16 @@ BasVmResultE basVmStep(BasVmT *vm) { char *line = content; while (line && *line) { char *eol = strchr(line, '\n'); - char lineBuf[512]; + char lineBuf[BAS_INI_LINE_LEN]; int32_t ll; if (eol) { ll = (int32_t)(eol - line); - if (ll > 0 && line[ll - 1] == '\r') { ll--; } - if (ll >= (int32_t)sizeof(lineBuf)) { ll = (int32_t)sizeof(lineBuf) - 1; } + if (ll > 0 && line[ll - 1] == '\r') { + ll--; + } + if (ll >= (int32_t)sizeof(lineBuf)) { + ll = (int32_t)sizeof(lineBuf) - 1; + } memcpy(lineBuf, line, ll); lineBuf[ll] = '\0'; line = eol + 1; @@ -3628,13 +3546,17 @@ BasVmResultE basVmStep(BasVmT *vm) { } char *rb = strchr(p, ']'); if (rb) { - char secName[256]; + char secName[BAS_INI_NAME_LEN]; int32_t sn = (int32_t)(rb - p - 1); - if (sn >= (int32_t)sizeof(secName)) { sn = (int32_t)sizeof(secName) - 1; } + if (sn >= (int32_t)sizeof(secName)) { + sn = (int32_t)sizeof(secName) - 1; + } memcpy(secName, p + 1, sn); secName[sn] = '\0'; inSection = (strcasecmp(secName, secStr->data) == 0); - if (inSection) { sectionFound = true; } + if (inSection) { + sectionFound = true; + } } fprintf(fp, "%s\n", lineBuf); continue; @@ -3643,10 +3565,14 @@ BasVmResultE basVmStep(BasVmT *vm) { if (inSection && !keyWritten) { char *eq = strchr(p, '='); if (eq) { - char k[256]; + char k[BAS_INI_NAME_LEN]; int32_t kl = (int32_t)(eq - p); - while (kl > 0 && (p[kl - 1] == ' ' || p[kl - 1] == '\t')) { kl--; } - if (kl >= (int32_t)sizeof(k)) { kl = (int32_t)sizeof(k) - 1; } + while (kl > 0 && (p[kl - 1] == ' ' || p[kl - 1] == '\t')) { + kl--; + } + if (kl >= (int32_t)sizeof(k)) { + kl = (int32_t)sizeof(k) - 1; + } memcpy(k, p, kl); k[kl] = '\0'; if (strcasecmp(k, keyStr->data) == 0) { @@ -3661,12 +3587,10 @@ BasVmResultE basVmStep(BasVmT *vm) { } } - // Key not found in existing section — append + // Key not found in existing section -- append if (!keyWritten) { if (!sectionFound) { fprintf(fp, "[%s]\n", secStr->data); - } else if (inSection) { - // Still in the right section at EOF — just append } fprintf(fp, "%s = %s\n", keyStr->data, valStr->data); } @@ -3687,7 +3611,7 @@ BasVmResultE basVmStep(BasVmT *vm) { } default: - runtimeError(vm, 51, "Bad opcode"); + runtimeError(vm, BAS_ERR_BAD_OPCODE, "Bad opcode"); return BAS_VM_BAD_OPCODE; } @@ -3736,37 +3660,220 @@ static void defaultPrint(void *ctx, const char *text, bool newline) { } -// Dir$ state -- persists across Dir$() calls within a VM session -static DIR *sDirHandle = NULL; -static char sDirPattern[DVX_MAX_PATH]; -static char sDirPath[DVX_MAX_PATH]; - -static void dirClose(void) { - if (sDirHandle) { - closedir(sDirHandle); - sDirHandle = NULL; +// Dir$ state lives in BasVmT (vm->dirHandle/dirPattern/dirPath) so that a +// second embedded VM cannot close another VM's in-progress enumeration. +static void dirClose(BasVmT *vm) { + if (vm->dirHandle) { + closedir((DIR *)vm->dirHandle); + vm->dirHandle = NULL; } } -static const char *dirNext(void) { - if (!sDirHandle) { +static const char *dirNext(BasVmT *vm) { + if (!vm->dirHandle) { return NULL; } struct dirent *ent; - while ((ent = readdir(sDirHandle)) != NULL) { - if (fnmatch(sDirPattern, ent->d_name, FNM_CASEFOLD) == 0) { + while ((ent = readdir((DIR *)vm->dirHandle)) != NULL) { + if (fnmatch(vm->dirPattern, ent->d_name, FNM_CASEFOLD) == 0) { return ent->d_name; } } - dirClose(); + dirClose(vm); return NULL; } +// Shared numeric formatter for OP_PRINT_USING (numeric branch) and OP_FORMAT +// (non-PERCENT branch). Renders n into out per the flags in f. Every cursor +// write is bounded against outSize, and every intermediate buffer is sized to +// BAS_FORMAT_BUF_SIZE and clamped, so a pathological format string or numeric +// magnitude cannot overflow the caller's stack-resident buffer. +static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize) { + bool isNeg = (n < 0); + double absN = isNeg ? -n : n; + int32_t decimals = f->hasDecimal ? f->digitsAfter : 0; + int32_t limit = (int32_t)outSize - 1; + int32_t idx = 0; + char numBuf[BAS_FORMAT_BUF_SIZE]; + + snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN); + + // Split into integer and decimal parts. + char intPart[BAS_FORMAT_BUF_SIZE]; + char decPart[BAS_FORMAT_BUF_SIZE]; + char *dot = strchr(numBuf, '.'); + + intPart[0] = '\0'; + decPart[0] = '\0'; + + if (dot) { + int32_t intLen = (int32_t)(dot - numBuf); + + if (intLen > (int32_t)sizeof(intPart) - 1) { + intLen = (int32_t)sizeof(intPart) - 1; + } + + memcpy(intPart, numBuf, intLen); + intPart[intLen] = '\0'; + strncpy(decPart, dot + 1, sizeof(decPart) - 1); + decPart[sizeof(decPart) - 1] = '\0'; + } else { + strncpy(intPart, numBuf, sizeof(intPart) - 1); + intPart[sizeof(intPart) - 1] = '\0'; + } + + // Apply thousands separator. + char fmtIntPart[BAS_FORMAT_BUF_SIZE]; + + if (f->hasComma) { + int32_t srcLen = (int32_t)strlen(intPart); + int32_t dstIdx = 0; + + for (int32_t i = 0; i < srcLen; i++) { + if (i > 0 && (srcLen - i) % 3 == 0) { + if (dstIdx < (int32_t)sizeof(fmtIntPart) - 1) { + fmtIntPart[dstIdx++] = ','; + } + } + + if (dstIdx < (int32_t)sizeof(fmtIntPart) - 1) { + fmtIntPart[dstIdx++] = intPart[i]; + } + } + + fmtIntPart[dstIdx] = '\0'; + } else { + strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1); + fmtIntPart[sizeof(fmtIntPart) - 1] = '\0'; + } + + // Sign prefix. FORMAT$ (formatPad) and PRINT USING differ here, so each + // call site's original rule is reproduced exactly. + if (f->formatPad) { + // FORMAT$: leading '-' for negatives whenever a sign position exists, + // leading '+' only for plusAtStart positives. + if (f->plusAtStart || f->plusAtEnd) { + if (isNeg) { + if (idx < limit) { + out[idx++] = '-'; + } + } else if (f->plusAtStart) { + if (idx < limit) { + out[idx++] = '+'; + } + } + } else if (isNeg) { + if (idx < limit) { + out[idx++] = '-'; + } + } + } else { + // PRINT USING: plusAtStart always shows a sign; otherwise a leading + // '-' for negatives unless the minus is deferred to the end. + if (f->plusAtStart) { + if (idx < limit) { + out[idx++] = isNeg ? '-' : '+'; + } + } else if (isNeg && !f->minusAtEnd) { + if (idx < limit) { + out[idx++] = '-'; + } + } + } + + // Dollar sign (PRINT USING $$ only). + if (f->dollarFloat) { + if (idx < limit) { + out[idx++] = '$'; + } + } + + // Pad leading. PRINT USING fills the whole pad region with fillChar + // (space or '*'). FORMAT$ (formatPad) fills the positions before the + // number with '0' when a zero digit precedes (zeroPad) else ' ', and the + // remaining positions with '0'. + int32_t fmtIntLen = (int32_t)strlen(fmtIntPart); + int32_t intRawLen = (int32_t)strlen(intPart); + int32_t padNeeded = f->digitsBefore - fmtIntLen; + char fillChar = f->asteriskFill ? '*' : ' '; + + for (int32_t i = 0; i < padNeeded; i++) { + char padChar; + + if (f->formatPad) { + if (i < padNeeded - intRawLen) { + padChar = f->zeroPad ? '0' : ' '; + } else { + padChar = '0'; + } + } else { + padChar = fillChar; + } + + if (idx < limit) { + out[idx++] = padChar; + } + } + + // Integer part. + for (int32_t i = 0; fmtIntPart[i]; i++) { + if (idx < limit) { + out[idx++] = fmtIntPart[i]; + } + } + + // Decimal part. Missing digits fill with '0' (PRINT USING behavior); + // FORMAT$ always has exactly 'decimals' digits so this never triggers. + if (f->hasDecimal) { + if (idx < limit) { + out[idx++] = '.'; + } + + for (int32_t i = 0; i < decimals; i++) { + if (idx < limit) { + out[idx++] = decPart[i] ? decPart[i] : '0'; + } + } + } + + // Trailing sign. FORMAT$ already emitted the leading '-' for negatives, + // so its trailing plusEnd path only shows '+' for positives. PRINT USING + // defers the sign to the end, so its plusEnd path shows '-' for negatives. + if (f->formatPad) { + if (f->plusAtEnd && !isNeg) { + if (idx < limit) { + out[idx++] = '+'; + } + } else if (f->minusAtEnd && isNeg) { + if (idx < limit) { + out[idx++] = '-'; + } + } else if (f->minusAtEnd && !isNeg) { + if (idx < limit) { + out[idx++] = ' '; + } + } + } else { + if (f->plusAtEnd) { + if (idx < limit) { + out[idx++] = isNeg ? '-' : '+'; + } + } else if (f->minusAtEnd) { + if (idx < limit) { + out[idx++] = isNeg ? '-' : ' '; + } + } + } + + out[idx] = '\0'; +} + + static BasVmResultE execArith(BasVmT *vm, uint8_t op) { BasValueT b; BasValueT a; @@ -3777,21 +3884,9 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { // VB behavior: + on two strings concatenates if ((op == OP_ADD_INT || op == OP_ADD_FLT) && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) { - BasStringT *sa = a.strVal ? a.strVal : basStringNew("", 0); - BasStringT *sb = b.strVal ? b.strVal : basStringNew("", 0); - int32_t newLen = sa->len + sb->len; - BasStringT *cat; - - if (newLen > 0) { - char *buf = (char *)malloc(newLen + 1); - memcpy(buf, sa->data, sa->len); - memcpy(buf + sa->len, sb->data, sb->len); - buf[newLen] = '\0'; - cat = basStringNew(buf, newLen); - free(buf); - } else { - cat = basStringNew("", 0); - } + BasStringT *sa = a.strVal ? a.strVal : basEmptyString; + BasStringT *sb = b.strVal ? b.strVal : basEmptyString; + BasStringT *cat = basStringConcat(sa, sb); basValRelease(&a); basValRelease(&b); @@ -3838,16 +3933,24 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { } case OP_IDIV_INT: if (ib == 0) { - runtimeError(vm, 11, "Division by zero"); + runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero"); return BAS_VM_DIV_BY_ZERO; } + if (ia == INT32_MIN && ib == -1) { + handled = false; + break; + } ir = ia / ib; break; case OP_MOD_INT: if (ib == 0) { - runtimeError(vm, 11, "Division by zero"); + runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero"); return BAS_VM_DIV_BY_ZERO; } + if (ia == INT32_MIN && ib == -1) { + ir = 0; + break; + } ir = ia % ib; break; default: @@ -3899,16 +4002,21 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { case OP_IDIV_INT: if ((int32_t)nb == 0) { - runtimeError(vm, 11, "Division by zero"); + runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero"); return BAS_VM_DIV_BY_ZERO; } + if ((int32_t)na == INT32_MIN && (int32_t)nb == -1) { + result = -(double)INT32_MIN; + break; + } + result = (double)((int32_t)na / (int32_t)nb); break; case OP_DIV_FLT: if (nb == 0.0) { - runtimeError(vm, 11, "Division by zero"); + runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero"); return BAS_VM_DIV_BY_ZERO; } @@ -3917,10 +4025,15 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { case OP_MOD_INT: if ((int32_t)nb == 0) { - runtimeError(vm, 11, "Division by zero"); + runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero"); return BAS_VM_DIV_BY_ZERO; } + if ((int32_t)na == INT32_MIN && (int32_t)nb == -1) { + result = 0.0; + break; + } + result = (double)((int32_t)na % (int32_t)nb); break; @@ -3996,26 +4109,23 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) { static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { switch (op) { case OP_FILE_OPEN: { - uint8_t mode = readUint8(vm); - BasValueT channelVal; + uint8_t mode = readUint8(vm); + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, false); + + if (chResult != BAS_VM_OK) { + return chResult; + } + BasValueT filenameVal; - if (!pop(vm, &channelVal) || !pop(vm, &filenameVal)) { + if (!pop(vm, &filenameVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - BasValueT fnStr = basValToString(filenameVal); basValRelease(&filenameVal); - if (channel < 1 || channel >= BAS_VM_MAX_FILES) { - basValRelease(&fnStr); - runtimeError(vm, 52, "Bad file channel number"); - return BAS_VM_FILE_ERROR; - } - // Close existing file on this channel if (vm->files[channel].handle) { fclose((FILE *)vm->files[channel].handle); @@ -4041,7 +4151,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { break; default: basValRelease(&fnStr); - runtimeError(vm, 54, "Bad file mode"); + runtimeError(vm, BAS_ERR_BAD_FILE_MODE, "Bad file mode"); return BAS_VM_FILE_ERROR; } @@ -4063,7 +4173,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { basValRelease(&fnStr); if (!fp) { - runtimeError(vm, 53, "File not found or cannot open"); + runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot open"); return BAS_VM_FILE_ERROR; } @@ -4073,18 +4183,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } case OP_FILE_CLOSE: { - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, false); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES) { - runtimeError(vm, 52, "Bad file channel number"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } if (vm->files[channel].handle) { @@ -4102,19 +4205,17 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { // without a trailing ; so multi-value PRINT with `;` separator // works correctly. BasValueT val; - BasValueT channelVal; - if (!pop(vm, &val) || !pop(vm, &channelVal)) { + if (!pop(vm, &val)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + if (chResult != BAS_VM_OK) { basValRelease(&val); - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + return chResult; } BasStringT *s = basValFormatString(val); @@ -4129,18 +4230,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } case OP_FILE_INPUT: { - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } char buf[1024]; @@ -4163,18 +4257,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } case OP_FILE_EOF: { - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } FILE *fp = (FILE *)vm->files[channel].handle; @@ -4192,18 +4279,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } case OP_FILE_LINE_INPUT: { - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } char buf[1024]; @@ -4227,19 +4307,17 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_WRITE: { // Pop value and channel, write value in WRITE format BasValueT val; - BasValueT channelVal; - if (!pop(vm, &val) || !pop(vm, &channelVal)) { + if (!pop(vm, &val)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + if (chResult != BAS_VM_OK) { basValRelease(&val); - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + return chResult; } FILE *fp = (FILE *)vm->files[channel].handle; @@ -4271,18 +4349,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_WRITE_SEP: { // Pop channel, write comma separator - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } fputc(',', (FILE *)vm->files[channel].handle); @@ -4291,18 +4362,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_WRITE_NL: { // Pop channel, write newline - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } fputc('\n', (FILE *)vm->files[channel].handle); @@ -4313,22 +4377,21 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { // Pop type, recno, channel; read data; push value BasValueT typeVal; BasValueT recnoVal; - BasValueT channelVal; - if (!pop(vm, &typeVal) || !pop(vm, &recnoVal) || !pop(vm, &channelVal)) { + if (!pop(vm, &typeVal) || !pop(vm, &recnoVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); int32_t recno = (int32_t)basValToNumber(recnoVal); int32_t dataType = (int32_t)basValToNumber(typeVal); - basValRelease(&channelVal); basValRelease(&recnoVal); basValRelease(&typeVal); - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); + + if (chResult != BAS_VM_OK) { + return chResult; } FILE *fp = (FILE *)vm->files[channel].handle; @@ -4336,8 +4399,8 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { // Seek to record position if recno > 0 // (recno is 1-based in QB; 0 means current position) if (recno > 0) { - // For simplicity, use fixed 128-byte records for RANDOM - fseek(fp, (long)(recno - 1) * 128, SEEK_SET); + // For simplicity, use fixed-size records for RANDOM + fseek(fp, (long)(recno - 1) * BAS_VM_RANDOM_RECORD_SIZE, SEEK_SET); } BasValueT result; @@ -4377,8 +4440,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } char *buf = (char *)malloc(len + 1); if (buf) { - if (fread(buf, 1, len, fp) < 1) { /* EOF ok */ } - buf[len] = '\0'; + // Terminate at the bytes actually read so a short or + // truncated record yields a shorter string rather than + // exposing uninitialized heap past the read. + int32_t bytesRead = (int32_t)fread(buf, 1, len, fp); + buf[bytesRead] = '\0'; result = basValStringFromC(buf); free(buf); } else { @@ -4403,27 +4469,26 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { // Pop value, recno, channel; write data BasValueT val; BasValueT recnoVal; - BasValueT channelVal; - if (!pop(vm, &val) || !pop(vm, &recnoVal) || !pop(vm, &channelVal)) { + if (!pop(vm, &val) || !pop(vm, &recnoVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); - int32_t recno = (int32_t)basValToNumber(recnoVal); - basValRelease(&channelVal); + int32_t recno = (int32_t)basValToNumber(recnoVal); basValRelease(&recnoVal); - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); + + if (chResult != BAS_VM_OK) { basValRelease(&val); - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + return chResult; } FILE *fp = (FILE *)vm->files[channel].handle; if (recno > 0) { - fseek(fp, (long)(recno - 1) * 128, SEEK_SET); + fseek(fp, (long)(recno - 1) * BAS_VM_RANDOM_RECORD_SIZE, SEEK_SET); } switch (val.type) { @@ -4448,8 +4513,14 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { break; } case BAS_TYPE_STRING: { - // Write length-prefixed string - int16_t len = val.strVal ? (int16_t)val.strVal->len : 0; + // Write length-prefixed string. Clamp before the int16 + // cast so an over-long string can't wrap to a negative + // prefix that desyncs the length from the payload. + int32_t slen = val.strVal ? val.strVal->len : 0; + if (slen > INT16_MAX) { + slen = INT16_MAX; + } + int16_t len = (int16_t)slen; fwrite(&len, sizeof(len), 1, fp); if (len > 0 && val.strVal) { fwrite(val.strVal->data, 1, len, fp); @@ -4471,20 +4542,19 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_SEEK: { // Pop position and channel, seek BasValueT posVal; - BasValueT channelVal; - if (!pop(vm, &posVal) || !pop(vm, &channelVal)) { + if (!pop(vm, &posVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); - int32_t pos = (int32_t)basValToNumber(posVal); - basValRelease(&channelVal); + int32_t pos = (int32_t)basValToNumber(posVal); basValRelease(&posVal); - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); + + if (chResult != BAS_VM_OK) { + return chResult; } // QB SEEK is 1-based @@ -4494,18 +4564,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_LOF: { // Pop channel, push file length - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } FILE *fp = (FILE *)vm->files[channel].handle; @@ -4523,18 +4586,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_LOC: { // Pop channel, push current position - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); - if (!pop(vm, &channelVal)) { - return BAS_VM_STACK_UNDERFLOW; - } - - int32_t channel = (int32_t)basValToNumber(channelVal); - basValRelease(&channelVal); - - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; + if (chResult != BAS_VM_OK) { + return chResult; } long pos = ftell((FILE *)vm->files[channel].handle); @@ -4559,7 +4615,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } if (freeNum == 0) { - runtimeError(vm, 67, "Too many files open"); + runtimeError(vm, BAS_ERR_TOO_MANY_FILES, "Too many files open"); return BAS_VM_FILE_ERROR; } @@ -4572,23 +4628,22 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { case OP_FILE_INPUT_N: { // Pop channel and n, read n chars, push string - BasValueT channelVal; + int32_t channel; + BasVmResultE chResult = popFileChannel(vm, &channel, true); + + if (chResult != BAS_VM_OK) { + return chResult; + } + BasValueT nVal; - if (!pop(vm, &channelVal) || !pop(vm, &nVal)) { + if (!pop(vm, &nVal)) { return BAS_VM_STACK_UNDERFLOW; } - int32_t channel = (int32_t)basValToNumber(channelVal); - int32_t n = (int32_t)basValToNumber(nVal); - basValRelease(&channelVal); + int32_t n = (int32_t)basValToNumber(nVal); basValRelease(&nVal); - if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { - runtimeError(vm, 52, "Bad file number or file not open"); - return BAS_VM_FILE_ERROR; - } - if (n < 0) { n = 0; } @@ -4600,7 +4655,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { char *buf = (char *)malloc(n + 1); if (!buf) { - runtimeError(vm, 7, "Out of memory"); + runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory"); return BAS_VM_OUT_OF_MEMORY; } @@ -4644,7 +4699,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (remove(fnStr.strVal->data) != 0) { basValRelease(&fnStr); - runtimeError(vm, 53, "File not found"); + runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found"); return BAS_VM_FILE_ERROR; } @@ -4668,7 +4723,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (rename(oldStr.strVal->data, newStr.strVal->data) != 0) { basValRelease(&oldStr); basValRelease(&newStr); - runtimeError(vm, 58, "File already exists or rename failed"); + runtimeError(vm, BAS_ERR_FILE_EXISTS, "File already exists or rename failed"); return BAS_VM_FILE_ERROR; } @@ -4709,7 +4764,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { fclose(fout); } - runtimeError(vm, 53, "File not found or cannot create destination"); + runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot create destination"); return BAS_VM_FILE_ERROR; } @@ -4743,7 +4798,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (rc != 0) { basValRelease(&pathStr); - runtimeError(vm, 75, "Path/File access error"); + runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error"); return BAS_VM_FILE_ERROR; } @@ -4763,7 +4818,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (rmdir(pathStr.strVal->data) != 0) { basValRelease(&pathStr); - runtimeError(vm, 75, "Path/File access error"); + runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error"); return BAS_VM_FILE_ERROR; } @@ -4783,7 +4838,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (platformChdir(pathStr.strVal->data) != 0) { basValRelease(&pathStr); - runtimeError(vm, 76, "Path not found"); + runtimeError(vm, BAS_ERR_PATH_NOT_FOUND, "Path not found"); return BAS_VM_FILE_ERROR; } @@ -4845,7 +4900,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { BasValueT patStr = basValToString(patVal); basValRelease(&patVal); - dirClose(); + dirClose(vm); // Split pattern into directory + filename pattern const char *pat = patStr.strVal->data; @@ -4860,27 +4915,27 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (lastSep) { int32_t dirLen = (int32_t)(lastSep - pat); - if (dirLen >= (int32_t)sizeof(sDirPath)) { - dirLen = (int32_t)sizeof(sDirPath) - 1; + if (dirLen >= (int32_t)sizeof(vm->dirPath)) { + dirLen = (int32_t)sizeof(vm->dirPath) - 1; } - memcpy(sDirPath, pat, dirLen); - sDirPath[dirLen] = '\0'; - snprintf(sDirPattern, sizeof(sDirPattern), "%s", lastSep + 1); + memcpy(vm->dirPath, pat, dirLen); + vm->dirPath[dirLen] = '\0'; + snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", lastSep + 1); } else { - snprintf(sDirPath, sizeof(sDirPath), "."); - snprintf(sDirPattern, sizeof(sDirPattern), "%s", pat); + snprintf(vm->dirPath, sizeof(vm->dirPath), "."); + snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", pat); } basValRelease(&patStr); - if (sDirPattern[0] == '\0') { - snprintf(sDirPattern, sizeof(sDirPattern), "*"); + if (vm->dirPattern[0] == '\0') { + snprintf(vm->dirPattern, sizeof(vm->dirPattern), "*"); } - sDirHandle = opendir(sDirPath); + vm->dirHandle = opendir(vm->dirPath); - const char *match = dirNext(); + const char *match = dirNext(vm); const char *text = match ? match : ""; BasStringT *s = basStringNew(text, (int32_t)strlen(text)); BasValueT result; @@ -4896,7 +4951,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { } case OP_FS_DIR_NEXT: { - const char *match = dirNext(); + const char *match = dirNext(vm); const char *text = match ? match : ""; BasStringT *s = basStringNew(text, (int32_t)strlen(text)); BasValueT result; @@ -4956,11 +5011,11 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (stat(fnStr.strVal->data, &st) == 0) { if (S_ISDIR(st.st_mode)) { - attrs |= 16; // vbDirectory + attrs |= BAS_ATTR_DIRECTORY; } if (!(st.st_mode & S_IWUSR)) { - attrs |= 1; // vbReadOnly + attrs |= BAS_ATTR_READONLY; } } @@ -4995,7 +5050,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { if (stat(fnStr.strVal->data, &st) == 0) { mode_t mode = st.st_mode; - if (attrs & 1) { + if (attrs & BAS_ATTR_READONLY) { mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH); } else { mode |= S_IWUSR; @@ -5025,7 +5080,16 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) { BasValueT *top = &vm->stack[vm->sp - 1]; int32_t n = (int32_t)basValToNumber(*top); basValRelease(top); - *top = basValInteger((int16_t)(~n)); + int32_t r = ~n; + + // Promote to LONG when the result exceeds INTEGER range, matching + // execArith -- truncation corrupts LONG masks like NOT &H10000. + if (r >= INT16_MIN && r <= INT16_MAX) { + *top = basValInteger((int16_t)r); + } else { + *top = basValLong(r); + } + return BAS_VM_OK; } @@ -5052,7 +5116,14 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) { default: result = 0; break; } - push(vm, basValInteger((int16_t)result)); + // Promote to LONG when the result exceeds INTEGER range, matching + // execArith -- truncation corrupts LONG masks like c& AND &HFF0000. + if (result >= INT16_MIN && result <= INT16_MAX) { + push(vm, basValInteger((int16_t)result)); + } else { + push(vm, basValLong(result)); + } + return BAS_VM_OK; } @@ -5378,9 +5449,9 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { for (int32_t i = 0; i < result->len; i++) { if (op == OP_STR_UCASE && result->data[i] >= 'a' && result->data[i] <= 'z') { - result->data[i] -= 32; + result->data[i] -= ('a' - 'A'); } else if (op == OP_STR_LCASE && result->data[i] >= 'A' && result->data[i] <= 'Z') { - result->data[i] += 32; + result->data[i] += ('a' - 'A'); } } } else { @@ -5453,9 +5524,12 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { } BasStringT *s = basStringAlloc(n + 1); - memset(s->data, ' ', n); - s->data[n] = '\0'; - s->len = n; + + if (s->cap >= n + 1) { + memset(s->data, ' ', n); + s->data[n] = '\0'; + s->len = n; + } top->type = BAS_TYPE_STRING; top->strVal = s; @@ -5476,21 +5550,25 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { BasStringT *src = sv.strVal; BasStringT *result = basStringAlloc(fixLen + 1); - result->len = fixLen; - int32_t srcLen = src ? src->len : 0; - int32_t copyLen = srcLen < (int32_t)fixLen ? srcLen : (int32_t)fixLen; + if (result->cap >= (int32_t)fixLen + 1) { + int32_t srcLen = src ? src->len : 0; + int32_t copyLen = srcLen < (int32_t)fixLen ? srcLen : (int32_t)fixLen; - if (copyLen > 0 && src) { - memcpy(result->data, src->data, copyLen); + result->len = fixLen; + + if (copyLen > 0 && src) { + memcpy(result->data, src->data, copyLen); + } + + // Pad with spaces + for (int32_t i = copyLen; i < (int32_t)fixLen; i++) { + result->data[i] = ' '; + } + + result->data[fixLen] = '\0'; } - // Pad with spaces - for (int32_t i = copyLen; i < (int32_t)fixLen; i++) { - result->data[i] = ' '; - } - - result->data[fixLen] = '\0'; basValRelease(&sv); top->type = BAS_TYPE_STRING; @@ -5568,6 +5646,22 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { } +// Returns whether `size` bytes can be read at vm->pc without running past +// the end of code[]. On overflow it flags the VM and clamps pc to codeLen so +// the next basVmStep entry raises BAS_VM_BAD_OPCODE. Compared as +// `codeLen - size` (not `pc + size`) to avoid signed overflow; codeLen is +// always >= 0 and size is a small operand width. +static inline bool operandFits(BasVmT *vm, int32_t size) { + if (vm->pc < 0 || vm->pc > vm->module->codeLen - size) { + vm->badOperand = true; + vm->pc = vm->module->codeLen; + return false; + } + + return true; +} + + static bool pop(BasVmT *vm, BasValueT *val) { if (vm->sp <= 0) { return false; @@ -5578,6 +5672,67 @@ static bool pop(BasVmT *vm, BasValueT *val) { } +// Pops the top call frame (the shared SUB/FUNCTION return epilogue used by +// OP_RET and OP_RET_VAL). The caller MUST have already verified callDepth > 0. +// Copies out-args, releases locals, restores PC and the caller's error handler. +static void popCallFrame(BasVmT *vm) { + BasCallFrameT *frame = &vm->callStack[--vm->callDepth]; + + // Copy out-args (for basVmCallSubWithArgsOut) BEFORE releasing the + // locals -- reading them after release is a use-after-free. + if (vm->outArgs && vm->callDepth == vm->outArgFrame) { + for (int32_t i = 0; i < vm->outArgCount && i < frame->localCount; i++) { + vm->outArgs[i] = basValCopy(frame->locals[i]); + } + + vm->outArgFrame = -1; + } + + // Release locals + for (int32_t i = 0; i < frame->localCount; i++) { + basValRelease(&frame->locals[i]); + } + + bool hadHandler = (frame->errorHandler != 0); + frame->errorHandler = 0; + vm->pc = frame->returnPc; + + // Restore the active handler to whatever the caller had set + BasCallFrameT *caller = currentFrame(vm); + vm->errorHandler = caller ? caller->errorHandler : 0; + + // If this SUB owned the active error handler, any handler body that ran + // inside it is now done -- clear the flag so the next error can trap again. + // QBASIC documents this: an unresumed handler is cleared when the procedure + // that installed it returns. + if (hadHandler) { + vm->inErrorHandler = false; + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + } +} + + +static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen) { + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || (requireOpen && !vm->files[channel].handle)) { + runtimeError(vm, BAS_ERR_BAD_FILE_NUM, requireOpen ? "Bad file number or file not open" : "Bad file channel number"); + return BAS_VM_FILE_ERROR; + } + + *outChannel = channel; + return BAS_VM_OK; +} + + static bool push(BasVmT *vm, BasValueT val) { if (vm->sp >= BAS_VM_STACK_SIZE) { return false; @@ -5591,26 +5746,69 @@ static bool push(BasVmT *vm, BasValueT val) { // memcpy with constant size is folded to a single load by the compiler and // is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned). static inline int16_t readInt16(BasVmT *vm) { - int16_t val; - memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t)); - vm->pc += sizeof(int16_t); + int16_t val = 0; + + if (operandFits(vm, (int32_t)sizeof(int16_t))) { + memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t)); + vm->pc += sizeof(int16_t); + } + return val; } static inline uint16_t readUint16(BasVmT *vm) { - uint16_t val; - memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t)); - vm->pc += sizeof(uint16_t); + uint16_t val = 0; + + if (operandFits(vm, (int32_t)sizeof(uint16_t))) { + memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t)); + vm->pc += sizeof(uint16_t); + } + return val; } static inline uint8_t readUint8(BasVmT *vm) { + if (!operandFits(vm, (int32_t)sizeof(uint8_t))) { + return 0; + } + return vm->module->code[vm->pc++]; } +// Releases every refcounted value the VM holds: the eval stack, the +// globals, all call-frame locals across callDepth, and the FOR-stack +// limit/step across forDepth. Shared by basVmDestroy and basVmReset so +// the teardown lives in one place. Must run BEFORE the caller zeroes +// sp/callDepth/forDepth, because the loops use those counts. +static void releaseVmState(BasVmT *vm) { + // Release eval stack values + for (int32_t i = 0; i < vm->sp; i++) { + basValRelease(&vm->stack[i]); + } + + // Release global variables + for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) { + basValRelease(&vm->globals[i]); + } + + // Release call frame locals + for (int32_t d = 0; d < vm->callDepth; d++) { + for (int32_t i = 0; i < vm->callStack[d].localCount; i++) { + basValRelease(&vm->callStack[d].locals[i]); + } + } + + // Release FOR stack + for (int32_t i = 0; i < vm->forDepth; i++) { + basValRelease(&vm->forStack[i].limit); + basValRelease(&vm->forStack[i].step); + } +} + + // Executes instructions until callDepth drops back to savedCallDepth // (meaning the subroutine returned). Periodically yields via the // doEvents callback to keep the GUI responsive during long-running @@ -5649,7 +5847,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool } } - // User resumed — continue executing the sub + // User resumed -- continue executing the sub stepsSinceYield = 0; continue; } @@ -5680,6 +5878,18 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool } if (target != 0) { + // Release any operands the failed statement half-pushed + // before the error fired, then truncate the eval stack + // back to the statement boundary so trapped errors in a + // loop don't leak values and grow sp without bound. + if (vm->sp > vm->stmtSp) { + for (int32_t si = vm->stmtSp; si < vm->sp; si++) { + basValRelease(&vm->stack[si]); + } + + vm->sp = vm->stmtSp; + } + vm->errorPc = stepPc; vm->errorNextPc = vm->pc; vm->inErrorHandler = true; @@ -5709,8 +5919,9 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool } } - vm->pc = savedPc; - vm->running = savedRunning; + vm->pc = savedPc; + vm->callDepth = savedCallDepth; + vm->running = savedRunning; // If we paused at a breakpoint during this sub, notify the host // so it can pause the event loop before any new event fires. @@ -5724,6 +5935,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) { vm->errorNumber = errNum; + vm->errorLine = vm->currentLine; if (vm->currentLine > 0) { snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d on line %d: %s", (int)errNum, (int)vm->currentLine, msg); @@ -5731,3 +5943,13 @@ static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) { snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", (int)errNum, (int)vm->pc, msg); } } + + +static bool validArrayDims(BasVmT *vm, int32_t dims) { + if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS) { + runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid array dimension count"); + return false; + } + + return true; +} diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h index 7d4888a..c321ec9 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.h +++ b/src/apps/kpunch/dvxbasic/runtime/vm.h @@ -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; // ============================================================ diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c index 6026a29..feed6c5 100644 --- a/src/apps/kpunch/dvxbasic/stub/bascomp.c +++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c @@ -55,19 +55,45 @@ #include #include +// 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 " 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; } diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c index f036d76..8a4f19a 100644 --- a/src/apps/kpunch/dvxbasic/stub/basstub.c +++ b/src/apps/kpunch/dvxbasic/stub/basstub.c @@ -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,42 +202,39 @@ 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 " 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++; - } - - frmName[ni] = '\0'; - break; - } - - while (*p && *p != '\n') { p++; } - - if (*p == '\n') { p++; } + if (!frmTextZ) { + free(frmText); + continue; } + frmText = frmTextZ; + frmText[frmSize] = '\0'; + + // Extract form name from "Begin Form " 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); } diff --git a/src/apps/kpunch/dvxbasic/test_compact.c b/src/apps/kpunch/dvxbasic/test_compact.c index 6e612de..cea9237 100644 --- a/src/apps/kpunch/dvxbasic/test_compact.c +++ b/src/apps/kpunch/dvxbasic/test_compact.c @@ -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); diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c index e76a8fc..1ba4255 100644 --- a/src/apps/kpunch/dvxbasic/test_compiler.c +++ b/src/apps/kpunch/dvxbasic/test_compiler.c @@ -31,10 +31,23 @@ #include #include +// 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,13 +903,15 @@ int main(void) { printf("COMPILE ERROR: %s\n", parser.error); } else { BasModuleT *mod = basParserBuildModule(&parser); - BasVmT *vm = basVmCreate(); - basVmLoadModule(vm, mod); - vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; - vm->callDepth = 1; - basVmRun(vm); - basVmDestroy(vm); - basModuleFree(mod); + + if (mod) { + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + primeMainFrame(vm, mod); + basVmRun(vm); + basVmDestroy(vm); + basModuleFree(mod); + } } basParserFree(&parser); @@ -1010,17 +1028,24 @@ int main(void) { printf("COMPILE ERROR: %s\n", parser.error); } else { BasModuleT *mod = basParserBuildModule(&parser); - bool found = false; - for (int32_t i = 0; i < mod->codeLen; i++) { - if (mod->code[i] == OP_CALL_EXTERN) { - found = true; - break; + if (mod) { + bool found = false; + + for (int32_t i = 0; i < mod->codeLen; i++) { + if (mod->code[i] == OP_CALL_EXTERN) { + found = true; + break; + } } - } - printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n"); - basModuleFree(mod); + 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; } diff --git a/src/apps/kpunch/dvxbasic/test_quick.c b/src/apps/kpunch/dvxbasic/test_quick.c index af99ed0..ad2b807 100644 --- a/src/apps/kpunch/dvxbasic/test_quick.c +++ b/src/apps/kpunch/dvxbasic/test_quick.c @@ -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 diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c index 57b6501..96c6eff 100644 --- a/src/apps/kpunch/dvxbasic/test_suite.c +++ b/src/apps/kpunch/dvxbasic/test_suite.c @@ -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; @@ -418,8 +436,8 @@ typedef struct MiniCtrlT { typedef struct MiniFormT { - char name[MINI_MAX_NAME]; - MiniCtrlT *ctrls; // stb_ds array + char name[MINI_MAX_NAME]; + 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 diff --git a/src/apps/kpunch/dvxbasic/test_vm.c b/src/apps/kpunch/dvxbasic/test_vm.c index 280060d..c495ceb 100644 --- a/src/apps/kpunch/dvxbasic/test_vm.c +++ b/src/apps/kpunch/dvxbasic/test_vm.c @@ -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); } diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.c b/src/apps/kpunch/dvxdemo/dvxdemo.c index c47e65b..7283e14 100644 --- a/src/apps/kpunch/dvxdemo/dvxdemo.c +++ b/src/apps/kpunch/dvxdemo/dvxdemo.c @@ -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); diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.c b/src/apps/kpunch/dvxhelp/dvxhelp.c index a91c4c9..205dfb3 100644 --- a/src/apps/kpunch/dvxhelp/dvxhelp.c +++ b/src/apps/kpunch/dvxhelp/dvxhelp.c @@ -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,9 +441,12 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) { if (w) { HelpTextDataT *td = (HelpTextDataT *)dvxCalloc(1, sizeof(HelpTextDataT)); - td->text = dvxStrdup(payload); - td->wrapWidth = -1; - w->data = td; + + if (td) { + td->text = dvxStrdup(payload); + td->wrapWidth = -1; + w->data = td; + } } break; @@ -451,9 +459,12 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) { if (w) { HelpHeadingDataT *hd = (HelpHeadingDataT *)dvxCalloc(1, sizeof(HelpHeadingDataT)); - hd->text = dvxStrdup(payload); - hd->level = hdr->type - HLP_REC_HEADING1 + 1; - w->data = hd; + + if (hd) { + hd->text = dvxStrdup(payload); + hd->level = hdr->type - HLP_REC_HEADING1 + 1; + w->data = hd; + } } break; @@ -461,15 +472,26 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) { 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)); - ld->displayText = dvxStrdup(displayText); - ld->targetTopicId = dvxStrdup(targetTopicId); - w->data = ld; + + 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,18 +559,21 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) { if (w) { HelpListItemDataT *ld = (HelpListItemDataT *)dvxCalloc(1, sizeof(HelpListItemDataT)); - // Prepend bullet + space to the text - int32_t pLen = (int32_t)strlen(payload); - ld->text = (char *)dvxMalloc(pLen + 3); - if (ld->text) { - ld->text[0] = '\x07'; - ld->text[1] = ' '; - memcpy(ld->text + 2, payload, pLen + 1); + if (ld) { + // Prepend bullet + space to the text + int32_t pLen = (int32_t)strlen(payload); + ld->text = (char *)dvxMalloc(pLen + 3); + + if (ld->text) { + ld->text[0] = '\x07'; + ld->text[1] = ' '; + memcpy(ld->text + 2, payload, pLen + 1); + } + + ld->wrapWidth = -1; + w->data = ld; } - - 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)); - nd->text = dvxStrdup(payload); - nd->wrapWidth = -1; - nd->noteType = hdr->flags; - w->data = nd; + + 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)); - 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; + if (cd) { + 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 diff --git a/src/apps/kpunch/dvxhelp/hlpformat.h b/src/apps/kpunch/dvxhelp/hlpformat.h index 22a47a5..08b586c 100644 --- a/src/apps/kpunch/dvxhelp/hlpformat.h +++ b/src/apps/kpunch/dvxhelp/hlpformat.h @@ -80,7 +80,7 @@ // --------------------------------------------------------------------------- -// File header (64 bytes, stored at EOF) +// File header (60 bytes, stored at EOF) // --------------------------------------------------------------------------- typedef struct { diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c index f475939..13acc5a 100644 --- a/src/apps/kpunch/progman/progman.c +++ b/src/apps/kpunch/progman/progman.c @@ -63,6 +63,7 @@ #include #include #include +#include #include #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); diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs index c7cb135..83f16f1 100644 --- a/src/libs/kpunch/dvxshell/dvxshell.dhs +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs @@ -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 diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c index 79c8ec0..1337d61 100644 --- a/src/libs/kpunch/dvxshell/shellApp.c +++ b/src/libs/kpunch/dvxshell/shellApp.c @@ -45,8 +45,7 @@ // Dynamic app slot table (stb_ds array). App IDs are array indices. // 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) +static ShellAppT *sApps = NULL; // 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,10 +112,20 @@ 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->state = AppStateTerminatingE; + // 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 diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h index 0f0b695..8837615 100644 --- a/src/libs/kpunch/dvxshell/shellApp.h +++ b/src/libs/kpunch/dvxshell/shellApp.h @@ -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) // ============================================================ diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c index ab6bfb5..368dbd0 100644 --- a/src/libs/kpunch/dvxshell/shellMain.c +++ b/src/libs/kpunch/dvxshell/shellMain.c @@ -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; diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs index 3fd5134..450244b 100644 --- a/src/libs/kpunch/libdvx/apiref.dhs +++ b/src/libs/kpunch/libdvx/apiref.dhs @@ -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 diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs index dd7f2d5..3674a55 100644 --- a/src/libs/kpunch/libdvx/arch.dhs +++ b/src/libs/kpunch/libdvx/arch.dhs @@ -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 diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c index 274b019..11d2a60 100644 --- a/src/libs/kpunch/libdvx/dvxApp.c +++ b/src/libs/kpunch/libdvx/dvxApp.c @@ -134,17 +134,24 @@ // Prototypes // ============================================================ +static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled); +static void addSysMenuSeparator(AppContextT *ctx); static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch); static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode); static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph); static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers); +static uint8_t clampByte(int32_t v); +static void clampPopupToScreen(AppContextT *ctx); +static void clearMenuBarActive(AppContextT *ctx); static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx); static void closeAllPopups(AppContextT *ctx); +static void closeOpenPopup(AppContextT *ctx); static void closePopupLevel(AppContextT *ctx); static void closeSysMenu(AppContextT *ctx); static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id); static void compositeAndFlush(AppContextT *ctx); static int32_t countVisibleWindows(const AppContextT *ctx); +static void deferDestroyWindow(AppContextT *ctx, WindowT *win); static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y); static bool dispatchAccelKey(AppContextT *ctx, char key); static void dispatchEvents(AppContextT *ctx); @@ -154,8 +161,10 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData); static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd); static WindowT *findWindowById(AppContextT *ctx, int32_t id); +static void flushPendingDestroys(AppContextT *ctx); static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons); static void initColorScheme(AppContextT *ctx); +static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uint8_t *src, int32_t w, int32_t h, int32_t pitch); static void interactiveScreenshot(AppContextT *ctx); static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win); static void invalidateAllWindows(AppContextT *ctx); @@ -168,6 +177,7 @@ static void pollMouse(AppContextT *ctx); static void pollWidgets(AppContextT *ctx); static void refreshMinimizedIcons(AppContextT *ctx); static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); +static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL]); static void updateCursorShape(AppContextT *ctx); static void updateTooltip(AppContextT *ctx); @@ -179,6 +189,7 @@ static void updateTooltip(AppContextT *ctx); // keyboard activation was registered, matching Win3.x/Motif behavior. WidgetT *sKeyPressedBtn = NULL; void (*sCursorBlinkFn)(void) = NULL; +void (*sWidgetDestroyFn)(WidgetT *w) = NULL; // 4x4 Bayer dithering offsets for ordered dithering when converting // 24-bit wallpaper images to 15/16-bit pixel formats. @@ -190,6 +201,26 @@ static const int32_t sBayerMatrix[4][4] = { }; +// Appends one item to the window system menu, copying the label (NUL- +// terminated), setting its command/enabled state and parsing its accelerator. +static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled) { + SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; + strncpy(item->label, label, MAX_MENU_LABEL - 1); + item->label[MAX_MENU_LABEL - 1] = '\0'; + item->cmd = cmd; + item->separator = false; + item->enabled = enabled; + item->accelKey = accelParse(item->label); +} + + +static void addSysMenuSeparator(AppContextT *ctx) { + SysMenuItemT *sep = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; + memset(sep, 0, sizeof(*sep)); + sep->separator = true; +} + + // ============================================================ // bufferToRgb -- convert native pixel format to 24-bit RGB // ============================================================ @@ -447,22 +478,13 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw for (int32_t k = 0; k < menu->itemCount; k++) { const char *label = menu->items[k].label; - const char *tab = strchr(label, '\t'); - int32_t itemW; + char leftBuf[MAX_MENU_LABEL]; + const char *shortcut = splitMenuLabel(label, leftBuf); + int32_t itemW; - if (tab) { + if (shortcut) { // Left part (with accel underline) + gap + right part (shortcut text) - char leftBuf[MAX_MENU_LABEL]; - int32_t leftLen = (int32_t)(tab - label); - - if (leftLen >= MAX_MENU_LABEL) { - leftLen = MAX_MENU_LABEL - 1; - } - - memcpy(leftBuf, label, leftLen); - leftBuf[leftLen] = '\0'; - - itemW = textWidthAccel(&ctx->font, leftBuf) + ctx->font.charWidth * MENU_TAB_GAP_CHARS + textWidth(&ctx->font, tab + 1); + itemW = textWidthAccel(&ctx->font, leftBuf) + ctx->font.charWidth * MENU_TAB_GAP_CHARS + textWidth(&ctx->font, shortcut); } else { itemW = textWidthAccel(&ctx->font, label); } @@ -532,7 +554,6 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t } } - (void)ctx; return false; } @@ -548,6 +569,56 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t // boundaries, then uncheck everything in the group except the clicked // item. This is the same approach Windows uses for menu radio groups. +// Clamps an integer to the 0..255 byte range, for parsed color channels. +static uint8_t clampByte(int32_t v) { + if (v < 0) { + return 0; + } + + if (v > UINT8_MAX) { + return UINT8_MAX; + } + + return (uint8_t)v; +} + + +// Keeps the active popup rect on-screen. Shared by the context-menu, +// menu-bar dropdown, and submenu open paths so off-screen items (and +// thus unreachable clicks, since the mouse can't leave the screen) +// can't happen near the bottom/right edges. +static void clampPopupToScreen(AppContextT *ctx) { + if (ctx->popup.popupX + ctx->popup.popupW > ctx->display.width) { + ctx->popup.popupX = ctx->display.width - ctx->popup.popupW; + } + + if (ctx->popup.popupY + ctx->popup.popupH > ctx->display.height) { + ctx->popup.popupY = ctx->display.height - ctx->popup.popupH; + } + + if (ctx->popup.popupX < 0) { + ctx->popup.popupX = 0; + } + + if (ctx->popup.popupY < 0) { + ctx->popup.popupY = 0; + } +} + + +// Clears the depressed (open) menu-bar item on the popup's owning window and +// dirties the title/menu band so it repaints raised. Shared by the popup +// close paths. +static void clearMenuBarActive(AppContextT *ctx) { + WindowT *popupWin = findWindowById(ctx, ctx->popup.windowId); + + if (popupWin && popupWin->menuBar && popupWin->menuBar->activeIdx >= 0) { + popupWin->menuBar->activeIdx = -1; + dirtyListAdd(&ctx->dirty, popupWin->x, popupWin->y, popupWin->w, CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT); + } +} + + static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) { MenuItemT *item = &menu->items[itemIdx]; @@ -600,17 +671,40 @@ static void closeAllPopups(AppContextT *ctx) { } // Clear the depressed menu bar item - WindowT *popupWin = findWindowById(ctx, ctx->popup.windowId); - if (popupWin && popupWin->menuBar && popupWin->menuBar->activeIdx >= 0) { - popupWin->menuBar->activeIdx = -1; - dirtyListAdd(&ctx->dirty, popupWin->x, popupWin->y, popupWin->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT); - } + clearMenuBarActive(ctx); ctx->popup.active = false; ctx->popup.depth = 0; } +// Close the open dropdown/combobox list popup (sOpenPopup), dirtying its +// on-screen area so the overlay is erased. Used by the ESC key and by the +// focus-change paths (Tab, accelerator) so a popup never lingers -- with its +// d->open flag stale -- after focus moves to another widget. No-op when no +// popup is open. +static void closeOpenPopup(AppContextT *ctx) { + if (!sOpenPopup) { + return; + } + + WindowT *popWin = sOpenPopup->window; + int32_t popX; + int32_t popY; + int32_t popW; + int32_t popH; + + wclsGetPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH); + dirtyListAdd(&ctx->dirty, popWin->x + popWin->contentX + popX, popWin->y + popWin->contentY + popY, popW, popH); + + WidgetT *closing = sOpenPopup; + sOpenPopup = NULL; + + wclsClosePopup(closing); + wgtInvalidate(closing); +} + + // ============================================================ // closePopupLevel -- close one submenu level (or deactivate if top) // ============================================================ @@ -643,11 +737,7 @@ static void closePopupLevel(AppContextT *ctx) { ctx->popup.hoverItem = pl->hoverItem; } else { // Clear the depressed menu bar item - WindowT *popupWin = findWindowById(ctx, ctx->popup.windowId); - if (popupWin && popupWin->menuBar && popupWin->menuBar->activeIdx >= 0) { - popupWin->menuBar->activeIdx = -1; - dirtyListAdd(&ctx->dirty, popupWin->x, popupWin->y, popupWin->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT); - } + clearMenuBarActive(ctx); ctx->popup.active = false; } @@ -718,6 +808,28 @@ static const uint8_t sDefaultColors[ColorCountE][3] = { }; // Access the packed color value in ColorSchemeT by index. +_Static_assert(offsetof(ColorSchemeT, desktop) == (size_t)ColorDesktopE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: desktop"); +_Static_assert(offsetof(ColorSchemeT, windowFace) == (size_t)ColorWindowFaceE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: windowFace"); +_Static_assert(offsetof(ColorSchemeT, windowHighlight) == (size_t)ColorWindowHighlightE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: windowHighlight"); +_Static_assert(offsetof(ColorSchemeT, windowShadow) == (size_t)ColorWindowShadowE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: windowShadow"); +_Static_assert(offsetof(ColorSchemeT, activeTitleBg) == (size_t)ColorActiveTitleBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: activeTitleBg"); +_Static_assert(offsetof(ColorSchemeT, activeTitleFg) == (size_t)ColorActiveTitleFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: activeTitleFg"); +_Static_assert(offsetof(ColorSchemeT, inactiveTitleBg) == (size_t)ColorInactiveTitleBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: inactiveTitleBg"); +_Static_assert(offsetof(ColorSchemeT, inactiveTitleFg) == (size_t)ColorInactiveTitleFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: inactiveTitleFg"); +_Static_assert(offsetof(ColorSchemeT, contentBg) == (size_t)ColorContentBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: contentBg"); +_Static_assert(offsetof(ColorSchemeT, contentFg) == (size_t)ColorContentFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: contentFg"); +_Static_assert(offsetof(ColorSchemeT, menuBg) == (size_t)ColorMenuBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuBg"); +_Static_assert(offsetof(ColorSchemeT, menuFg) == (size_t)ColorMenuFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuFg"); +_Static_assert(offsetof(ColorSchemeT, menuHighlightBg) == (size_t)ColorMenuHighlightBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuHighlightBg"); +_Static_assert(offsetof(ColorSchemeT, menuHighlightFg) == (size_t)ColorMenuHighlightFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: menuHighlightFg"); +_Static_assert(offsetof(ColorSchemeT, buttonFace) == (size_t)ColorButtonFaceE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: buttonFace"); +_Static_assert(offsetof(ColorSchemeT, scrollbarBg) == (size_t)ColorScrollbarBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: scrollbarBg"); +_Static_assert(offsetof(ColorSchemeT, scrollbarFg) == (size_t)ColorScrollbarFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: scrollbarFg"); +_Static_assert(offsetof(ColorSchemeT, scrollbarTrough) == (size_t)ColorScrollbarTroughE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: scrollbarTrough"); +_Static_assert(offsetof(ColorSchemeT, cursorFg) == (size_t)ColorCursorFgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: cursorFg"); +_Static_assert(offsetof(ColorSchemeT, cursorBg) == (size_t)ColorCursorBgE * sizeof(uint32_t), "ColorSchemeT/ColorIdE order mismatch: cursorBg"); + + static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) { return ((uint32_t *)cs) + (int32_t)id; } @@ -834,11 +946,7 @@ static void compositeAndFlush(AppContextT *ctx) { RectT smIsect; if (rectIntersect(dr, &smRect, &smIsect)) { - BevelStyleT smBevel; - smBevel.highlight = ctx->colors.windowHighlight; - smBevel.shadow = ctx->colors.windowShadow; - smBevel.face = ctx->colors.menuBg; - smBevel.width = POPUP_BEVEL_WIDTH; + BevelStyleT smBevel = BEVEL_POPUP(&ctx->colors, POPUP_BEVEL_WIDTH); drawBevel(d, ops, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH, &smBevel); int32_t itemY = ctx->sysMenu.popupY + POPUP_BEVEL_WIDTH; @@ -875,10 +983,7 @@ static void compositeAndFlush(AppContextT *ctx) { if (rectIntersect(dr, &ttRect, &ttIsect)) { rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg); - drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->colors.contentFg); - drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY + ctx->tooltipH - 1, ctx->tooltipW, ctx->colors.contentFg); - drawVLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg); - drawVLine(d, ops, ctx->tooltipX + ctx->tooltipW - 1, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg); + drawRectOutline(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.contentFg); drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true); } } @@ -886,7 +991,7 @@ static void compositeAndFlush(AppContextT *ctx) { // 6. Draw cursor drawCursorAt(ctx, ctx->mouseX, ctx->mouseY); - // 6. Flush this dirty rect to LFB + // 7. Flush this dirty rect to LFB flushRect(d, dr); } @@ -914,6 +1019,65 @@ static int32_t countVisibleWindows(const AppContextT *ctx) { } +// ============================================================ +// deferDestroyWindow +// ============================================================ +// +// Queues a window for destruction at the end of the current dispatch +// pass (see dvxUpdate). Called by dvxDestroyWindow when a user callback +// is on the stack: freeing the window immediately would leave the +// dispatch code touching freed widgets when the callback returns. The +// window is hidden right away -- it vanishes and stops receiving input +// this frame -- but its memory stays valid until flushPendingDestroys. + +static void deferDestroyWindow(AppContextT *ctx, WindowT *win) { + if (win->destroyPending) { + return; + } + + // Queue overflow: destroy immediately. This re-risks the callback + // use-after-free, but only past DVX_PENDING_DESTROY_MAX simultaneous + // destroys in one dispatch, which no real path produces. + if (ctx->pendingDestroyCount >= DVX_PENDING_DESTROY_MAX) { + dvxLog("WM: pending-destroy queue full, destroying window %ld immediately", (long)win->id); + wmDestroyWindow(&ctx->stack, win); + return; + } + + win->destroyPending = true; + ctx->pendingDestroyIds[ctx->pendingDestroyCount] = win->id; + ctx->pendingDestroyCount++; + + // Hide the window now so this frame already composites without it. + if (win->visible) { + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + win->visible = false; + } + + if (win->minimized) { + int32_t iconY; + int32_t iconH; + wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); + dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); + } + + // Stop key routing into the dying window: drop widget focus and move + // WM focus to the topmost window that is not itself pending destroy. + if (sFocusedWidget && sFocusedWidget->window == win) { + sFocusedWidget = NULL; + } + + if (win->focused) { + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + if (!ctx->stack.windows[i]->destroyPending) { + wmSetFocus(&ctx->stack, &ctx->dirty, i); + break; + } + } + } +} + + // ============================================================ // dirtyCursorArea // ============================================================ @@ -954,7 +1118,7 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { // Menu bar accelerators take priority over widget accelerators if (win->menuBar) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - if (win->menuBar->menus[i].accelKey == key) { + if (win->menuBar->menus[i]->accelKey == key) { openPopupAtMenu(ctx, win, i); return true; } @@ -971,10 +1135,17 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target); if (next) { + closeOpenPopup(ctx); WidgetT *prev = sFocusedWidget; sFocusedWidget = next; if (prev) { + if (prev != next) { + wclsClearSelection(prev); + // Commit/clamp any in-progress edit. + wclsOnBlur(prev); + } + wgtInvalidatePaint(prev); } @@ -982,10 +1153,14 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { wgtInvalidatePaint(next); } } else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) { + closeOpenPopup(ctx); WidgetT *prev = sFocusedWidget; sFocusedWidget = target; if (prev && prev != target) { + wclsClearSelection(prev); + // Commit/clamp any in-progress edit. + wclsOnBlur(prev); wgtInvalidatePaint(prev); } @@ -1108,6 +1283,15 @@ static void dispatchEvents(AppContextT *ctx) { SysMenuItemT *item = &ctx->sysMenu.items[itemIdx]; if (item->enabled && !item->separator) { + // Consume the press and swallow the matching release. + // executeSysMenuCmd may pump a nested dvxUpdate loop + // (Close confirm dialog, Screenshot file dialog); + // without this the still-held button is re-seen as a + // fresh MouseDown by the nested loop and the stray + // release fires on whatever sits under the (now-closed) + // sys menu. Mirrors the popup-menu click path. + ctx->prevMouseButtons |= MOUSE_LEFT; + ctx->suppressNextMouseUp = true; executeSysMenuCmd(ctx, item->cmd); } } @@ -1148,7 +1332,6 @@ static void dispatchEvents(AppContextT *ctx) { } if (itemIdx != ctx->popup.hoverItem) { - int32_t prevHover = ctx->popup.hoverItem; ctx->popup.hoverItem = itemIdx; dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH); @@ -1161,15 +1344,6 @@ static void dispatchEvents(AppContextT *ctx) { openSubMenu(ctx); } } - - // If we moved away from a submenu item to a non-submenu item, - // close any child submenus that were opened from this level - if (prevHover >= 0 && ctx->popup.menu && prevHover < ctx->popup.menu->itemCount) { - // Already handled: openSubMenu replaces the child, and if current - // item is not a submenu, no child opens. But we may still have - // a stale child -- check if depth was increased by a previous hover. - // This case is handled below by the parent-level hit test popping levels. - } } // Click on item in current level @@ -1267,7 +1441,7 @@ static void dispatchEvents(AppContextT *ctx) { WindowT *win = findWindowById(ctx, ctx->popup.windowId); if (win && win->menuBar) { - int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; + int32_t barY = win->y + CHROME_TITLEBAR_BOTTOM; if (my >= barY && my < barY + CHROME_MENU_HEIGHT && mx >= win->x + CHROME_TOTAL_SIDE && @@ -1276,7 +1450,7 @@ static void dispatchEvents(AppContextT *ctx) { int32_t relX = mx - win->x; for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - MenuT *menu = &win->menuBar->menus[i]; + MenuT *menu = win->menuBar->menus[i]; if (relX >= menu->barX && relX < menu->barX + menu->barW) { if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT) && i == ctx->popup.menuIdx && ctx->popup.depth == 0) { @@ -1330,6 +1504,12 @@ static void dispatchEvents(AppContextT *ctx) { if (hitIdx >= 0 && hitPart == HIT_CONTENT) { WindowT *win = ctx->stack.windows[hitIdx]; + // A modal dialog captures all input: a right-click on another + // window must not raise/focus it or open its context menu. + if (ctx->modalWindow && win != ctx->modalWindow) { + return; + } + if (hitIdx != ctx->stack.focusedIdx) { wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx); hitIdx = ctx->stack.count - 1; @@ -1389,6 +1569,24 @@ static void dispatchEvents(AppContextT *ctx) { } } + // Deliver right-release and middle press/release transitions to the + // focused window (right-press is handled above with context menus, + // left-press/release and movement elsewhere). The widget layer detects + // the specific button edge from its own prevMouseButtons, so a bare + // dispatch on the transition is enough for onMouseDown/onMouseUp(2/3). + bool rightRelease = !(buttons & MOUSE_RIGHT) && (prevBtn & MOUSE_RIGHT); + bool middleChange = ((buttons & MOUSE_MIDDLE) != (prevBtn & MOUSE_MIDDLE)); + + if ((rightRelease || middleChange) && ctx->stack.focusedIdx >= 0) { + WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; + + if (win->onMouse) { + int32_t relX = mx - win->x - win->contentX; + int32_t relY = my - win->y - win->contentY; + win->onMouse(win, relX, relY, buttons); + } + } + // Mouse movement in content area -- send to focused window if ((mx != ctx->prevMouseX || my != ctx->prevMouseY) && ctx->stack.focusedIdx >= 0) { @@ -1448,12 +1646,8 @@ static void dispatchEvents(AppContextT *ctx) { consumed = true; } } - } - - // Fire onScroll callback on the focused widget - if (win->widgetRoot) { - WidgetT *focus = wgtGetFocused(); + // Fire onScroll callback on the focused widget if (focus && focus->onScroll) { focus->onScroll(focus, ctx->mouseWheel * ctx->wheelDirection); } @@ -1532,7 +1726,7 @@ static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t c static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) { const CursorT *cur = &ctx->cursors[ctx->cursorId]; - drawMaskedBitmap(&ctx->display, &ctx->blitOps, x - cur->hotX, y - cur->hotY, cur->width, cur->height, cur->andMask, cur->xorData, ctx->cursorFg, ctx->cursorBg); + drawMaskedBitmap(&ctx->display, &ctx->blitOps, x - cur->hotX, y - cur->hotY, cur->width, cur->height, cur->andMask, cur->xorData, ctx->colors.cursorFg, ctx->colors.cursorBg); } @@ -1572,11 +1766,7 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c int32_t checkMargin = hasCheck ? MENU_CHECK_WIDTH : 0; // Draw popup background - BevelStyleT popBevel; - popBevel.highlight = ctx->colors.windowHighlight; - popBevel.shadow = ctx->colors.windowShadow; - popBevel.face = ctx->colors.menuBg; - popBevel.width = POPUP_BEVEL_WIDTH; + BevelStyleT popBevel = BEVEL_POPUP(&ctx->colors, POPUP_BEVEL_WIDTH); drawBevel(d, ops, px, py, pw, ph, &popBevel); // Draw menu items @@ -1602,30 +1792,20 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c rectFill(d, ops, px + POPUP_BEVEL_WIDTH, itemY, pw - POPUP_BEVEL_WIDTH * 2, ctx->font.charHeight, bg); // Split label at tab: left part is the menu text, right part is the shortcut - const char *tab = strchr(item->label, '\t'); - - if (tab) { - char leftBuf[MAX_MENU_LABEL]; - int32_t leftLen = (int32_t)(tab - item->label); - - if (leftLen >= MAX_MENU_LABEL) { - leftLen = MAX_MENU_LABEL - 1; - } - - memcpy(leftBuf, item->label, leftLen); - leftBuf[leftLen] = '\0'; + char leftBuf[MAX_MENU_LABEL]; + const char *shortcut = splitMenuLabel(item->label, leftBuf); + if (shortcut) { drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + POPUP_BEVEL_WIDTH + checkMargin, itemY, leftBuf, fg, bg, true); - const char *right = tab + 1; - int32_t rightW = textWidth(&ctx->font, right); + int32_t rightW = textWidth(&ctx->font, shortcut); int32_t rightX = px + pw - rightW - CHROME_TITLE_PAD - POPUP_BEVEL_WIDTH * 2; if (item->subMenu) { rightX -= SUBMENU_ARROW_WIDTH; } - drawText(d, ops, &ctx->font, rightX, itemY, right, fg, bg, true); + drawText(d, ops, &ctx->font, rightX, itemY, shortcut, fg, bg, true); } else { drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + POPUP_BEVEL_WIDTH + checkMargin, itemY, item->label, fg, bg, true); } @@ -1687,7 +1867,6 @@ static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) { m.h = h; m.bpp = bpp; arrput(ctx->videoModes, m); - ctx->videoModeCount++; } @@ -1735,12 +1914,7 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) { case SysMenuMinimizeE: if (ctx->modalWindow != win) { - wmMinimize(&ctx->stack, &ctx->dirty, win); - - int32_t iconY; - int32_t iconH; - wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); - dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); + dvxMinimizeWindow(ctx, win); } break; @@ -1793,6 +1967,26 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) { } +// ============================================================ +// flushPendingDestroys +// ============================================================ +// +// Destroys every window queued by deferDestroyWindow. Called from +// dvxUpdate once dispatchDepth reaches zero, i.e. no user callback is +// on the stack any more, so freeing widget memory is safe. + +static void flushPendingDestroys(AppContextT *ctx) { + while (ctx->pendingDestroyCount > 0) { + ctx->pendingDestroyCount--; + WindowT *win = findWindowById(ctx, ctx->pendingDestroyIds[ctx->pendingDestroyCount]); + + if (win) { + dvxDestroyWindow(ctx, win); + } + } +} + + // ============================================================ // handleMouseButton // ============================================================ @@ -1814,12 +2008,15 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) { // hit, ensuring the clicked window always comes to the front. static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons) { + int32_t hitPart; + int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); + // Modal window gating: only the modal window receives clicks if (ctx->modalWindow) { - int32_t hitPart; - int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); - - if (hitIdx >= 0 && ctx->stack.windows[hitIdx] != ctx->modalWindow) { + // A modal dialog captures all input: block clicks on any other + // window AND desktop/minimized-icon clicks (hitIdx < 0), so a + // minimized-icon restore can't raise a window over the modal. + if (hitIdx < 0 || ctx->stack.windows[hitIdx] != ctx->modalWindow) { return; } } @@ -1827,13 +2024,9 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t // Check for click on minimized icon, but only if no window covers the point. // Icons are drawn below windows, so a window at this position takes priority. int32_t iconIdx = -1; - { - int32_t hitPart; - int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); - if (hitIdx < 0) { - iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); - } + if (hitIdx < 0) { + iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my); } if (iconIdx >= 0) { @@ -1854,9 +2047,6 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t return; } - int32_t hitPart; - int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); - if (hitIdx < 0) { return; // clicked on desktop } @@ -1872,7 +2062,10 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t } // Clear widget focus for non-content clicks (scrollbars, title bar, etc.) - // Content clicks clear focus inside widgetOnMouse instead. + // Content clicks clear focus inside widgetOnMouse instead. Selection is + // intentionally kept: chrome clicks are transient (focus auto-returns on + // the next repaint) and menu commands like Edit > Copy need the selection + // to survive the click that opened the menu. if (hitPart != HIT_CONTENT && sFocusedWidget) { wgtInvalidatePaint(sFocusedWidget); sFocusedWidget = NULL; @@ -1957,7 +2150,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t int32_t relX = mx - win->x; for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - MenuT *menu = &win->menuBar->menus[i]; + MenuT *menu = win->menuBar->menus[i]; if (relX >= menu->barX && relX < menu->barX + menu->barW) { openPopupAtMenu(ctx, win, i); @@ -1977,12 +2170,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t case HIT_MINIMIZE: if (ctx->modalWindow != win) { - wmMinimize(&ctx->stack, &ctx->dirty, win); - - int32_t iconY; - int32_t iconH; - wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); - dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); + dvxMinimizeWindow(ctx, win); } break; @@ -2019,33 +2207,37 @@ static void initColorScheme(AppContextT *ctx) { // interactiveScreenshot -- snapshot screen, prompt for save path // ============================================================ -static void interactiveScreenshot(AppContextT *ctx) { - FileFilterT filters[] = { - { "PNG Images (*.png)" }, - { "BMP Images (*.bmp)" } - }; - char path[DVX_MAX_PATH]; +// Snapshot a pixel buffer, prompt for a save path, and write it. Shared by +// the full-screen and per-window interactive screenshot commands. +static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uint8_t *src, int32_t w, int32_t h, int32_t pitch) { + int32_t size = pitch * h; + uint8_t *snap = (uint8_t *)malloc(size); - int32_t scrW = ctx->display.width; - int32_t scrH = ctx->display.height; - int32_t scrPitch = ctx->display.pitch; - int32_t scrSize = scrPitch * scrH; - uint8_t *scrBuf = (uint8_t *)malloc(scrSize); + if (snap) { + FileFilterT filters[] = { + { "PNG Images (*.png)" }, + { "BMP Images (*.bmp)" } + }; + char path[DVX_MAX_PATH]; - if (scrBuf) { - memcpy(scrBuf, ctx->display.backBuf, scrSize); + memcpy(snap, src, size); - if (dvxFileDialog(ctx, "Save Screenshot", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { + if (dvxFileDialog(ctx, title, FD_SAVE, NULL, filters, 2, path, sizeof(path))) { dvxSetBusy(ctx, true); - dvxSaveImage(ctx, scrBuf, scrW, scrH, scrPitch, path); + dvxSaveImage(ctx, snap, w, h, pitch, path); dvxSetBusy(ctx, false); } - free(scrBuf); + free(snap); } } +static void interactiveScreenshot(AppContextT *ctx) { + interactiveSaveBuffer(ctx, "Save Screenshot", ctx->display.backBuf, ctx->display.width, ctx->display.height, ctx->display.pitch); +} + + // ============================================================ // interactiveWindowScreenshot -- snapshot window content, prompt for save path // ============================================================ @@ -2055,29 +2247,7 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) { return; } - FileFilterT filters[] = { - { "PNG Images (*.png)" }, - { "BMP Images (*.bmp)" } - }; - char path[DVX_MAX_PATH]; - - int32_t capW = win->contentW; - int32_t capH = win->contentH; - int32_t capPitch = win->contentPitch; - int32_t capSize = capPitch * capH; - uint8_t *capBuf = (uint8_t *)malloc(capSize); - - if (capBuf) { - memcpy(capBuf, win->contentBuf, capSize); - - if (dvxFileDialog(ctx, "Save Window Screenshot", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { - dvxSetBusy(ctx, true); - dvxSaveImage(ctx, capBuf, capW, capH, capPitch, path); - dvxSetBusy(ctx, false); - } - - free(capBuf); - } + interactiveSaveBuffer(ctx, "Save Window Screenshot", win->contentBuf, win->contentW, win->contentH, win->contentPitch); } @@ -2131,14 +2301,7 @@ static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t // Position at mouse, clamped to screen ctx->popup.popupX = screenX; ctx->popup.popupY = screenY; - - if (ctx->popup.popupX + ctx->popup.popupW > ctx->display.width) { - ctx->popup.popupX = ctx->display.width - ctx->popup.popupW; - } - - if (ctx->popup.popupY + ctx->popup.popupH > ctx->display.height) { - ctx->popup.popupY = ctx->display.height - ctx->popup.popupH; - } + clampPopupToScreen(ctx); dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH); @@ -2163,7 +2326,7 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) { // Close any existing popup chain first closeAllPopups(ctx); - MenuT *menu = &win->menuBar->menus[menuIdx]; + MenuT *menu = win->menuBar->menus[menuIdx]; ctx->popup.active = true; ctx->popup.isContextMenu = false; @@ -2171,15 +2334,16 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) { ctx->popup.menuIdx = menuIdx; ctx->popup.menu = menu; ctx->popup.popupX = win->x + menu->barX; - ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT; + ctx->popup.popupY = win->y + CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT; // Mark the menu bar item as active (depressed look) win->menuBar->activeIdx = menuIdx; - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT); + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT); ctx->popup.hoverItem = -1; ctx->popup.depth = 0; calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH); + clampPopupToScreen(ctx); dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH); @@ -2241,6 +2405,7 @@ static void openSubMenu(AppContextT *ctx) { ctx->popup.hoverItem = -1; calcPopupSize(ctx, ctx->popup.menu, &ctx->popup.popupW, &ctx->popup.popupH); + clampPopupToScreen(ctx); dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH); @@ -2270,82 +2435,39 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) { ctx->sysMenu.itemCount = 0; ctx->sysMenu.windowId = win->id; - SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "&Restore", MAX_MENU_LABEL - 1); - item->cmd = SysMenuRestoreE; - item->separator = false; - item->enabled = win->maximized; - item->accelKey = accelParse(item->label); + // Restore -- enabled only when maximized + addSysMenuItem(ctx, "&Restore", SysMenuRestoreE, win->maximized); // Move -- disabled when maximized - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "&Move", MAX_MENU_LABEL - 1); - item->cmd = SysMenuMoveE; - item->separator = false; - item->enabled = !win->maximized; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "&Move", SysMenuMoveE, !win->maximized); // Size -- only if resizable and not maximized - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "&Size", MAX_MENU_LABEL - 1); - item->cmd = SysMenuSizeE; - item->separator = false; - item->enabled = win->resizable && !win->maximized; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "&Size", SysMenuSizeE, win->resizable && !win->maximized); // Minimize -- not available on modal windows - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "Mi&nimize", MAX_MENU_LABEL - 1); - item->cmd = SysMenuMinimizeE; - item->separator = false; - item->enabled = !win->modal; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "Mi&nimize", SysMenuMinimizeE, !win->modal); // Maximize -- only if resizable and not maximized - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "Ma&ximize", MAX_MENU_LABEL - 1); - item->cmd = SysMenuMaximizeE; - item->separator = false; - item->enabled = win->resizable && !win->maximized; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "Ma&ximize", SysMenuMaximizeE, win->resizable && !win->maximized); // Separator - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - memset(item, 0, sizeof(*item)); - item->separator = true; + addSysMenuSeparator(ctx); // Screenshot (full screen) - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "Scree&nshot...", MAX_MENU_LABEL - 1); - item->cmd = SysMenuScreenshotE; - item->separator = false; - item->enabled = true; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "Scree&nshot...", SysMenuScreenshotE, true); // Screenshot (this window) - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "&Window Shot...", MAX_MENU_LABEL - 1); - item->cmd = SysMenuWinScreenshotE; - item->separator = false; - item->enabled = true; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "&Window Shot...", SysMenuWinScreenshotE, true); // Separator - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - memset(item, 0, sizeof(*item)); - item->separator = true; + addSysMenuSeparator(ctx); // Close - item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; - strncpy(item->label, "&Close", MAX_MENU_LABEL - 1); - item->cmd = SysMenuCloseE; - item->separator = false; - item->enabled = true; - item->accelKey = accelParse(item->label); + addSysMenuItem(ctx, "&Close", SysMenuCloseE, true); // Compute popup geometry -- position below the close gadget ctx->sysMenu.popupX = win->x + CHROME_BORDER_WIDTH; - ctx->sysMenu.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; + ctx->sysMenu.popupY = win->y + CHROME_TITLEBAR_BOTTOM; int32_t maxW = 0; @@ -2399,6 +2521,20 @@ static void pollKeyboard(AppContextT *ctx) { ctx->keyModifiers = shiftFlags; bool shiftHeld = (shiftFlags & KEY_MOD_SHIFT) != 0; + // While busy, input is blocked (see dvxApp.h). Drain and discard any + // pending keys so they cannot re-enter dvxSetWallpaper/dvxChangeVideoMode + // during the buildWallpaperBuf yield pump, and so a burst does not replay + // the instant busy clears. Matches the dispatchEvents mouse gate. + if (ctx->busy) { + PlatformKeyEventT drain; + + while (platformKeyboardRead(&drain)) { + // discard + } + + return; + } + PlatformKeyEventT evt; while (platformKeyboardRead(&evt)) { @@ -2448,6 +2584,8 @@ static void pollKeyboard(AppContextT *ctx) { if (win->onClose) { WIN_CALLBACK(ctx, win, win->onClose(win)); + } else { + dvxDestroyWindow(ctx, win); } } @@ -2494,7 +2632,7 @@ static void pollKeyboard(AppContextT *ctx) { if (ctx->popup.active) { closeAllPopups(ctx); } else { - dispatchAccelKey(ctx, win->menuBar->menus[0].accelKey); + dispatchAccelKey(ctx, win->menuBar->menus[0]->accelKey); } } } @@ -2520,16 +2658,7 @@ static void pollKeyboard(AppContextT *ctx) { if (ctx->kbMoveResize.mode == KbModeResizeE) { kbWin->w = ctx->kbMoveResize.origW; kbWin->h = ctx->kbMoveResize.origH; - wmUpdateContentRect(kbWin); - wmReallocContentBuf(kbWin, &ctx->display); - - if (kbWin->onResize) { - kbWin->onResize(kbWin, kbWin->contentW, kbWin->contentH); - } - - if (kbWin->onPaint) { - kbWin->onPaint(kbWin, NULL); - } + wmResizeCommit(kbWin, &ctx->display); } dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h); @@ -2558,21 +2687,20 @@ static void pollKeyboard(AppContextT *ctx) { } // Clamp: keep title bar reachable - int32_t screenW = ctx->display.width; - int32_t screenH = ctx->display.height; - int32_t minVisible = 50; + int32_t screenW = ctx->display.width; + int32_t screenH = ctx->display.height; if (kbWin->y < 0) { kbWin->y = 0; } - if (kbWin->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT > screenH) { - kbWin->y = screenH - CHROME_BORDER_WIDTH - CHROME_TITLE_HEIGHT; + if (kbWin->y + CHROME_TITLEBAR_BOTTOM > screenH) { + kbWin->y = screenH - CHROME_TITLEBAR_BOTTOM; } - if (kbWin->x + kbWin->w < minVisible) { - kbWin->x = minVisible - kbWin->w; + if (kbWin->x + kbWin->w < DRAG_MIN_VISIBLE) { + kbWin->x = DRAG_MIN_VISIBLE - kbWin->w; } - if (kbWin->x > screenW - minVisible) { - kbWin->x = screenW - minVisible; + if (kbWin->x > screenW - DRAG_MIN_VISIBLE) { + kbWin->x = screenW - DRAG_MIN_VISIBLE; } if (kbWin->x != oldX || kbWin->y != oldY) { @@ -2595,12 +2723,18 @@ static void pollKeyboard(AppContextT *ctx) { newH -= KB_MOVE_STEP; } - if (newW < MIN_WINDOW_W) { - newW = MIN_WINDOW_W; + // Same dynamic minimum as mouse resize (title gadgets, + // menu bar) rather than the bare MIN_WINDOW_W/H floor. + int32_t minW; + int32_t minH; + wmMinWindowSize(kbWin, &minW, &minH); + + if (newW < minW) { + newW = minW; } - if (newH < MIN_WINDOW_H) { - newH = MIN_WINDOW_H; + if (newH < minH) { + newH = minH; } if (kbWin->maxW > 0 && newW > kbWin->maxW) { @@ -2623,22 +2757,18 @@ static void pollKeyboard(AppContextT *ctx) { newH = screenH - kbWin->y; } + // Keep the window reachable: with x off-screen left, + // shrinking must not push the whole window off-screen. + if (kbWin->x + newW < DRAG_MIN_VISIBLE) { + newW = DRAG_MIN_VISIBLE - kbWin->x; + } + if (newW != kbWin->w || newH != kbWin->h) { kbWin->maximized = false; dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h); kbWin->w = newW; kbWin->h = newH; - wmUpdateContentRect(kbWin); - wmReallocContentBuf(kbWin, &ctx->display); - - if (kbWin->onResize) { - kbWin->onResize(kbWin, kbWin->contentW, kbWin->contentH); - } - - if (kbWin->onPaint) { - kbWin->onPaint(kbWin, NULL); - } - + wmResizeCommit(kbWin, &ctx->display); dirtyListAdd(&ctx->dirty, kbWin->x, kbWin->y, kbWin->w, kbWin->h); } } @@ -2736,7 +2866,7 @@ static void pollKeyboard(AppContextT *ctx) { if (win->menuBar) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - if (win->menuBar->menus[i].accelKey == lc) { + if (win->menuBar->menus[i]->accelKey == lc) { dispatchAccelKey(ctx, lc); goto nextKey; } @@ -2920,7 +3050,7 @@ static void pollKeyboard(AppContextT *ctx) { if (win && win->menuBar) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - if (win->menuBar->menus[i].accelKey == lc && i != ctx->popup.menuIdx) { + if (win->menuBar->menus[i]->accelKey == lc && i != ctx->popup.menuIdx) { openPopupAtMenu(ctx, win, i); goto nextKey; } @@ -2930,21 +3060,7 @@ static void pollKeyboard(AppContextT *ctx) { // ESC closes open dropdown/combobox popup if (sOpenPopup && ascii == KEY_ESCAPE) { - // Dirty the popup list area - WindowT *popWin = sOpenPopup->window; - int32_t popX; - int32_t popY; - int32_t popW; - int32_t popH; - wclsGetPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH); - dirtyListAdd(&ctx->dirty, popWin->x + popWin->contentX + popX, popWin->y + popWin->contentY + popY, popW, popH); - - WidgetT *closing = sOpenPopup; - sOpenPopup = NULL; - - wclsClosePopup(closing); - - wgtInvalidate(closing); + closeOpenPopup(ctx); continue; } @@ -3014,7 +3130,9 @@ static void pollKeyboard(AppContextT *ctx) { } if (next) { - sOpenPopup = NULL; + // Close any open dropdown popup so its d->open flag + // is not left stale when focus moves off it. + closeOpenPopup(ctx); WidgetT *prev = sFocusedWidget; // Switch focus BEFORE invalidating so paint sees @@ -3022,6 +3140,15 @@ static void pollKeyboard(AppContextT *ctx) { sFocusedWidget = next; if (prev) { + // Focus can wrap to the same widget in a + // window with one focusable -- don't self-clear. + if (prev != next) { + wclsClearSelection(prev); + // Commit/clamp any in-progress edit before + // the app onBlur. + wclsOnBlur(prev); + } + wgtInvalidatePaint(prev); if (prev->onBlur) { @@ -3137,7 +3264,7 @@ static void pollMouse(AppContextT *ctx) { // rect generation for efficient repainting. static void pollWidgets(AppContextT *ctx) { - for (int32_t i = 0; i < sPollWidgetCount; i++) { + for (int32_t i = 0; i < (int32_t)arrlen(sPollWidgets); i++) { WidgetT *w = sPollWidgets[i]; WindowT *win = w->window; @@ -3241,24 +3368,37 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t win->w = w; win->h = h; - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); - - if (win->onResize) { - WIN_CALLBACK(ctx, win, win->onResize(win, win->contentW, win->contentH)); - } - - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); - win->iconNeedsRefresh = true; - } + WIN_CALLBACK(ctx, win, wmResizeCommit(win, &ctx->display)); // Dirty new position dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); } +// Splits a menu label at the tab into the left (menu text) part, copied into +// leftBuf, and returns a pointer to the right (shortcut) part. Returns NULL +// when the label has no tab. Shared by the popup measure and draw paths. +static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL]) { + const char *tab = strchr(label, '\t'); + int32_t leftLen; + + if (!tab) { + return NULL; + } + + leftLen = (int32_t)(tab - label); + + if (leftLen >= MAX_MENU_LABEL) { + leftLen = MAX_MENU_LABEL - 1; + } + + memcpy(leftBuf, label, leftLen); + leftBuf[leftLen] = '\0'; + + return tab + 1; +} + + // ============================================================ // updateCursorShape // ============================================================ @@ -3279,10 +3419,7 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t static void updateCursorShape(AppContextT *ctx) { // Busy state overrides all cursor logic if (ctx->busy) { - if (ctx->cursorId != CURSOR_BUSY) { - ctx->cursorId = CURSOR_BUSY; - } - + ctx->cursorId = CURSOR_BUSY; return; } @@ -3459,7 +3596,7 @@ static void updateTooltip(AppContextT *ctx) { int32_t hitPart; int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); - if (hitIdx < 0 || hitPart != 0) { + if (hitIdx < 0 || hitPart != HIT_CONTENT) { return; } @@ -3593,10 +3730,6 @@ void dvxApplyColorScheme(AppContextT *ctx) { *colorSlot(&ctx->colors, (ColorIdE)i) = packColor(d, ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); } - // Sync cursor colors from the scheme so the compositor uses them - ctx->cursorFg = ctx->colors.cursorFg; - ctx->cursorBg = ctx->colors.cursorBg; - invalidateAllWindows(ctx); } @@ -3617,7 +3750,7 @@ void dvxCascadeWindows(AppContextT *ctx) { int32_t screenH = ctx->display.height; int32_t offsetX = 0; int32_t offsetY = 0; - int32_t step = CHROME_TITLE_HEIGHT + CHROME_BORDER_WIDTH; + int32_t step = CHROME_TITLEBAR_BOTTOM; int32_t winW = screenW * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM; int32_t winH = screenH * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM; @@ -3742,18 +3875,7 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request // Clear maximized flag since screen size changed win->maximized = false; - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); - - if (win->onResize) { - WIN_CALLBACK(ctx, win, win->onResize(win, win->contentW, win->contentH)); - } - - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); - win->iconNeedsRefresh = true; - } + WIN_CALLBACK(ctx, win, wmResizeCommit(win, &ctx->display)); } // Reload wallpaper at the new resolution/bpp @@ -3835,7 +3957,7 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t // offset diagonally by the title bar height so the new window doesn't // sit directly on top. Keeps offsetting while collisions exist, wrapping // back to the origin if we'd go off screen. - int32_t step = CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; + int32_t step = CHROME_TITLEBAR_BOTTOM; for (;;) { bool collision = false; @@ -3911,39 +4033,72 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { + // Close any open popup or sys menu owned by this window FIRST. The + // popup state holds raw MenuT pointers into win->menuBar, which dies + // with the window, and compositeAndFlush dereferences them + // unconditionally while popup.active is set -- a guaranteed same-frame + // use-after-free without this. + if (ctx->popup.active && ctx->popup.windowId == win->id) { + closeAllPopups(ctx); + } + + if (ctx->sysMenu.active && ctx->sysMenu.windowId == win->id) { + closeSysMenu(ctx); + } + + // Clear the modal pointer if it was this window, even when deferring + // -- a hidden pending-destroy modal must not keep eating input. + if (ctx->modalWindow == win) { + ctx->modalWindow = NULL; + } + + // The tooltip text may point into a widget tooltip or a window title + // that wgtDestroy is about to free; drop it (and dirty its old area) + // before any teardown -- the string can be freed independently of the + // deferred-destroy path below. + if (ctx->tooltipText) { + dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH); + ctx->tooltipText = NULL; + } + + // A user callback is on the stack: defer the teardown so the + // dispatch code's widget pointers stay valid until it unwinds. + if (ctx->dispatchDepth > 0) { + deferDestroyWindow(ctx, win); + return; + } + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + // Cache the minimized flag before wmDestroyWindow frees win, so the + // post-destroy icon-strip dirty below does not read freed memory. + bool wasMinimized = win->minimized; + // If the window is minimized, dirty the icon strip so the icon // disappears and remaining icons repack correctly. - if (win->minimized) { + if (wasMinimized) { int32_t iconY; int32_t iconH; wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); } - // Clear the modal pointer if it was this window. Callers that open - // modal dialogs are supposed to restore the previous modal before - // destroying the dialog window, but a crash / longjmp / missed - // cleanup can leave modalWindow pointing at freed memory -- every - // subsequent click is then dropped by the modal gate. - if (ctx->modalWindow == win) { - ctx->modalWindow = NULL; - } - wmDestroyWindow(&ctx->stack, win); // Dirty icon area again with the updated count (one fewer icon) - if (win->minimized) { + if (wasMinimized) { int32_t iconY; int32_t iconH; wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); } - // Focus the new top window - if (ctx->stack.count > 0) { - wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); + // Focus the new top window, skipping any still queued for destroy + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + if (!ctx->stack.windows[i]->destroyPending) { + wmSetFocus(&ctx->stack, &ctx->dirty, i); + break; + } } } @@ -4094,7 +4249,7 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) { // ============================================================ const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count) { - *count = ctx->videoModeCount; + *count = (int32_t)arrlen(ctx->videoModes); return ctx->videoModes; } @@ -4117,8 +4272,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ // Enumerate available video modes BEFORE setting one. Some VBE // BIOSes return a stale or truncated mode list once a graphics // mode is active, so we must query while still in text mode. - ctx->videoModes = NULL; - ctx->videoModeCount = 0; + // (ctx was memset above, so videoModes is already NULL.) platformVideoEnumModes(enumModeCb, ctx); if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { @@ -4144,7 +4298,6 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ initColorScheme(ctx); - // Pre-pack cursor colors once. packColor converts RGB to the native platformMouseInit(ctx->display.width, ctx->display.height); ctx->hasMouseWheel = platformMouseWheelInit(); ctx->mouseX = ctx->display.width / 2; @@ -4268,7 +4421,7 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i int32_t bpp = d->format.bytesPerPixel; int32_t pitch = imgW * bpp; - uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + uint8_t *buf = (uint8_t *)malloc((size_t)pitch * imgH); if (!buf) { stbi_image_free(rgb); @@ -4329,7 +4482,7 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t int32_t pitch = imgW * bpp; uint32_t keyColor = packColor(d, 255, 0, 255); // magenta bool hasAlpha = false; - uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + uint8_t *buf = (uint8_t *)malloc((size_t)pitch * imgH); if (!buf) { stbi_image_free(rgba); @@ -4390,7 +4543,7 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int int32_t bpp = d->format.bytesPerPixel; int32_t pitch = imgW * bpp; - uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + uint8_t *buf = (uint8_t *)malloc((size_t)pitch * imgH); if (!buf) { stbi_image_free(rgb); @@ -4486,9 +4639,9 @@ bool dvxLoadTheme(AppContextT *ctx, const char *filename) { // Find matching color name for (int32_t i = 0; i < ColorCountE; i++) { if (strcmp(key, sColorNames[i]) == 0) { - ctx->colorRgb[i][0] = (uint8_t)r; - ctx->colorRgb[i][1] = (uint8_t)g; - ctx->colorRgb[i][2] = (uint8_t)b; + ctx->colorRgb[i][0] = clampByte(r); + ctx->colorRgb[i][1] = clampByte(g); + ctx->colorRgb[i][2] = clampByte(b); break; } } @@ -4515,6 +4668,14 @@ void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) { void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) { wmMinimize(&ctx->stack, &ctx->dirty, win); + + // wmMinimize only dirties the window's former rect; the icon strip + // must be dirtied too or the new minimized icon never composites + // (matches the sys-menu and minimize-gadget interactive paths). + int32_t iconY; + int32_t iconH; + wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); + dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); } @@ -4569,14 +4730,12 @@ void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH) win->y = 0; } - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); + // Commit the new size: this re-lays-out widget windows AND fires onPaint + // for raw-paint windows, which the old wgtInvalidate-only path left blank. + WIN_CALLBACK(ctx, win, wmResizeCommit(win, &ctx->display)); // Dirty new position dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - - // Invalidate widget tree so it repaints at the new size - wgtInvalidate(win->widgetRoot); } @@ -4718,19 +4877,11 @@ bool dvxSaveTheme(const AppContextT *ctx, const char *filename) { // we only write to the LFB, never read. int32_t dvxScreenshot(AppContextT *ctx, const char *path) { - dvxSetBusy(ctx, true); - DisplayT *d = &ctx->display; - uint8_t *rgb = bufferToRgb(d, d->backBuf, d->width, d->height, d->pitch); + int32_t result; - if (!rgb) { - dvxSetBusy(ctx, false); - return -1; - } - - int32_t result = stbi_write_png(path, d->width, d->height, RGB_CHANNELS, rgb, d->width * RGB_CHANNELS) ? 0 : -1; - - free(rgb); + dvxSetBusy(ctx, true); + result = dvxSaveImage(ctx, d->backBuf, d->width, d->height, d->pitch, path); dvxSetBusy(ctx, false); return result; @@ -4744,23 +4895,16 @@ int32_t dvxScreenshot(AppContextT *ctx, const char *path) { void dvxSetBusy(AppContextT *ctx, bool busy) { ctx->busy = busy; - // Dirty the cursor area so the shape change is visible - dirtyListAdd(&ctx->dirty, - ctx->mouseX - ctx->cursors[ctx->cursorId].hotX, - ctx->mouseY - ctx->cursors[ctx->cursorId].hotY, - CURSOR_DIRTY_SIZE, CURSOR_DIRTY_SIZE); - if (busy) { ctx->cursorId = CURSOR_BUSY; } else { ctx->cursorId = CURSOR_ARROW; } - // Dirty again for the new shape (different hotspot) - dirtyListAdd(&ctx->dirty, - ctx->mouseX - ctx->cursors[ctx->cursorId].hotX, - ctx->mouseY - ctx->cursors[ctx->cursorId].hotY, - CURSOR_DIRTY_SIZE, CURSOR_DIRTY_SIZE); + // dirtyCursorArea covers the worst-case union of all cursor shapes at the + // pointer position, so a single call erases the old shape and exposes the + // new one (both are drawn at the same mouseX/mouseY). + dirtyCursorArea(ctx, ctx->mouseX, ctx->mouseY); // Flush immediately so the cursor change is visible before // the blocking operation starts (or after it ends). @@ -4787,13 +4931,6 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) uint32_t packed = packColor(&ctx->display, r, g, b); *colorSlot(&ctx->colors, id) = packed; - // Keep cursor color cache in sync - if (id == ColorCursorFgE) { - ctx->cursorFg = packed; - } else if (id == ColorCursorBgE) { - ctx->cursorBg = packed; - } - invalidateAllWindows(ctx); } @@ -4944,8 +5081,7 @@ void dvxShutdown(AppContextT *ctx) { free(ctx->wallpaperBuf); ctx->wallpaperBuf = NULL; arrfree(ctx->videoModes); - ctx->videoModes = NULL; - ctx->videoModeCount = 0; + ctx->videoModes = NULL; videoShutdown(&ctx->display); } @@ -5132,6 +5268,13 @@ bool dvxUpdate(AppContextT *ctx) { return false; } + // Everything inside this depth-tracked section may run user + // callbacks. dvxDestroyWindow defers actual teardown while + // dispatchDepth > 0 (see deferDestroyWindow); nested dvxUpdate + // calls from modal dialog loops stack the depth, so the flush only + // happens once every callback frame has unwound. + ctx->dispatchDepth++; + pollMouse(ctx); pollKeyboard(ctx); dispatchEvents(ctx); @@ -5143,6 +5286,12 @@ bool dvxUpdate(AppContextT *ctx) { wgtUpdateTimers(); + ctx->dispatchDepth--; + + if (ctx->dispatchDepth == 0 && ctx->pendingDestroyCount > 0) { + flushPendingDestroys(ctx); + } + ctx->frameCount++; if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) { @@ -5210,22 +5359,14 @@ bool dvxUpdate(AppContextT *ctx) { // content buffer architecture. int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) { + int32_t result; + if (!win || !win->contentBuf) { return -1; } dvxSetBusy(ctx, true); - - uint8_t *rgb = bufferToRgb(&ctx->display, win->contentBuf, win->contentW, win->contentH, win->contentPitch); - - if (!rgb) { - dvxSetBusy(ctx, false); - return -1; - } - - int32_t result = stbi_write_png(path, win->contentW, win->contentH, RGB_CHANNELS, rgb, win->contentW * RGB_CHANNELS) ? 0 : -1; - - free(rgb); + result = dvxSaveImage(ctx, win->contentBuf, win->contentW, win->contentH, win->contentPitch, path); dvxSetBusy(ctx, false); return result; diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h index 538270a..c7b8d31 100644 --- a/src/libs/kpunch/libdvx/dvxApp.h +++ b/src/libs/kpunch/libdvx/dvxApp.h @@ -45,6 +45,12 @@ #include +// 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. diff --git a/src/libs/kpunch/libdvx/dvxComp.c b/src/libs/kpunch/libdvx/dvxComp.c index 7b1f30b..0ceed0c 100644 --- a/src/libs/kpunch/libdvx/dvxComp.c +++ b/src/libs/kpunch/libdvx/dvxComp.c @@ -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; diff --git a/src/libs/kpunch/libdvx/dvxComp.h b/src/libs/kpunch/libdvx/dvxComp.h index a0562a1..57a5150 100644 --- a/src/libs/kpunch/libdvx/dvxComp.h +++ b/src/libs/kpunch/libdvx/dvxComp.h @@ -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. diff --git a/src/libs/kpunch/libdvx/dvxCur.h b/src/libs/kpunch/libdvx/dvxCur.h index 5388474..7ac66e4 100644 --- a/src/libs/kpunch/libdvx/dvxCur.h +++ b/src/libs/kpunch/libdvx/dvxCur.h @@ -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 diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c index 7e11c45..09cb4cf 100644 --- a/src/libs/kpunch/libdvx/dvxDialog.c +++ b/src/libs/kpunch/libdvx/dvxDialog.c @@ -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,29 +397,41 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co } WidgetT *lb = wgtListBox(root); - lb->weight = WGT_WEIGHT_FILL; - lb->minH = wgtPixels(CB_LIST_HEIGHT); - lb->onDblClick = cbOnDblClick; - wgtListBoxSetItems(lb, items, itemCount); - if (defaultIdx >= 0 && defaultIdx < itemCount) { - wgtListBoxSetSelected(lb, defaultIdx); + if (lb) { + lb->weight = WGT_WEIGHT_FILL; + lb->minH = wgtPixels(CB_LIST_HEIGHT); + lb->onDblClick = cbOnDblClick; + wgtListBoxSetItems(lb, items, itemCount); + + if (defaultIdx >= 0 && defaultIdx < itemCount) { + wgtListBoxSetSelected(lb, defaultIdx); + } + + sChoiceBox.listBox = lb; } - sChoiceBox.listBox = lb; - WidgetT *btnRow = wgtHBox(root); - btnRow->align = AlignCenterE; - WidgetT *okBtn = wgtButton(btnRow, "&OK"); - okBtn->minW = wgtPixels(BUTTON_WIDTH); - okBtn->minH = wgtPixels(BUTTON_HEIGHT); - okBtn->onClick = cbOnOk; + if (btnRow) { + btnRow->align = AlignCenterE; - WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); - cancelBtn->minW = wgtPixels(BUTTON_WIDTH); - cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); - cancelBtn->onClick = cbOnCancel; + 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,62 +508,98 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha // Path row WidgetT *pathRow = wgtHBox(root); - wgtLabel(pathRow, "&Path:"); - sFd.pathInput = wgtTextInput(pathRow, DVX_MAX_PATH); - sFd.pathInput->onChange = fdOnPathSubmit; + + if (pathRow) { + wgtLabel(pathRow, "&Path:"); + sFd.pathInput = wgtTextInput(pathRow, DVX_MAX_PATH); + + // 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); - sFd.fileList->weight = WGT_WEIGHT_FILL; - sFd.fileList->onChange = fdOnListClick; - sFd.fileList->onDblClick = fdOnListDblClick; + + 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); - 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; + if (filterRow) { + wgtLabel(filterRow, "F&ilter:"); + sFd.filterDd = wgtDropdown(filterRow); - for (int32_t i = 0; i < fc; i++) { - filterLabels[i] = filters[i].label; + // Build filter label array (static -- lives for dialog lifetime) + 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; + } + + wgtDropdownSetItems(sFd.filterDd, filterLabels, fc); + wgtDropdownSetSelected(sFd.filterDd, 0); + + if (sFd.filterDd) { + sFd.filterDd->onChange = fdOnFilterChange; + } } - - wgtDropdownSetItems(sFd.filterDd, filterLabels, fc); - wgtDropdownSetSelected(sFd.filterDd, 0); - sFd.filterDd->onChange = fdOnFilterChange; } // Filename row WidgetT *nameRow = wgtHBox(root); - wgtLabel(nameRow, "File &name:"); - sFd.nameInput = wgtTextInput(nameRow, DVX_MAX_PATH); + + if (nameRow) { + wgtLabel(nameRow, "File &name:"); + sFd.nameInput = wgtTextInput(nameRow, DVX_MAX_PATH); + } // Button row WidgetT *btnRow = wgtHBox(root); - btnRow->align = AlignEndE; - WidgetT *okBtn = wgtButton(btnRow, (flags & FD_SAVE) ? "&Save" : "&Open"); - okBtn->onClick = fdOnOk; - okBtn->minW = wgtPixels(80); - WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); - cancelBtn->onClick = fdOnCancel; - cancelBtn->minW = wgtPixels(80); + + if (btnRow) { + btnRow->align = AlignEndE; + + WidgetT *okBtn = wgtButton(btnRow, (flags & FD_SAVE) ? "&Save" : "&Open"); + + if (okBtn) { + okBtn->onClick = fdOnOk; + okBtn->minW = wgtPixels(BUTTON_WIDTH); + } + + WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); + + if (cancelBtn) { + cancelBtn->onClick = fdOnCancel; + cancelBtn->minW = wgtPixels(BUTTON_WIDTH); + } + } // Load initial directory fdLoadDir(); wgtInvalidate(root); - // Modal loop - ctx->modalWindow = win; + // 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) { - strncpy(outPath, sFd.outPath, outPathSize - 1); - outPath[outPathSize - 1] = '\0'; - } + // outPath/outPathSize are validated non-NULL/positive at entry. + strncpy(outPath, sFd.outPath, outPathSize - 1); + outPath[outPathSize - 1] = '\0'; result = true; } @@ -617,26 +682,38 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const // Text input WidgetT *input = wgtTextInput(root, IB_INPUT_MAXLEN); - input->weight = 0; - sInputBox.input = input; - if (defaultText) { - wgtSetText(input, defaultText); + if (input) { + input->weight = 0; + sInputBox.input = input; + + if (defaultText) { + wgtSetText(input, defaultText); + } } // Button row WidgetT *btnRow = wgtHBox(root); - btnRow->align = AlignCenterE; - WidgetT *okBtn = wgtButton(btnRow, "&OK"); - okBtn->minW = wgtPixels(BUTTON_WIDTH); - okBtn->minH = wgtPixels(BUTTON_HEIGHT); - okBtn->onClick = ibOnOk; + if (btnRow) { + btnRow->align = AlignCenterE; - WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); - cancelBtn->minW = wgtPixels(BUTTON_WIDTH); - cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); - cancelBtn->onClick = ibOnCancel; + 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,21 +779,33 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int } WidgetT *spinner = wgtSpinner(root, minVal, maxVal, step); - wgtSpinnerSetValue(spinner, defaultVal); - sIntBox.spinner = spinner; + + if (spinner) { + wgtSpinnerSetValue(spinner, defaultVal); + sIntBox.spinner = spinner; + } WidgetT *btnRow = wgtHBox(root); - btnRow->align = AlignCenterE; - WidgetT *okBtn = wgtButton(btnRow, "&OK"); - okBtn->minW = wgtPixels(BUTTON_WIDTH); - okBtn->minH = wgtPixels(BUTTON_HEIGHT); - okBtn->onClick = iibOnOk; + if (btnRow) { + btnRow->align = AlignCenterE; - WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); - cancelBtn->minW = wgtPixels(BUTTON_WIDTH); - cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); - cancelBtn->onClick = iibOnCancel; + 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) { - int32_t charW = font->charWidth; - int32_t lineH = font->charHeight; - int32_t maxChars = maxW / charW; - int32_t curY = y; + 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 maxChars = maxW / charW; + 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; -} diff --git a/src/libs/kpunch/libdvx/dvxDlg.h b/src/libs/kpunch/libdvx/dvxDlg.h index 84ea0cd..66822c6 100644 --- a/src/libs/kpunch/libdvx/dvxDlg.h +++ b/src/libs/kpunch/libdvx/dvxDlg.h @@ -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)" diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c index 7ee5ce2..bf3985c 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.c +++ b/src/libs/kpunch/libdvx/dvxDraw.c @@ -79,11 +79,25 @@ #include #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++; + char ch; + AccelTokenE token; - if (*text == '&') { - // Escaped && -- literal &, not an accelerator - text++; - continue; + while ((token = accelNextToken(&text, &ch)) != AccelTokenEndE) { + if (token == AccelTokenAccelE) { + if (ch >= 'A' && ch <= 'Z') { + return (char)(ch + ('a' - 'A')); } - if (*text && *text != '&') { - char ch = *text; - - if (ch >= 'A' && ch <= 'Z') { - return (char)(ch + 32); - } - - if (ch >= 'a' && ch <= 'z') { - return ch; - } - - if (ch >= '0' && ch <= '9') { - return ch; - } - - return ch; - } - - break; + return ch; } - - 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); - } - - 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); - 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); + drawChar(d, ops, font, x, y, ch, fg, bg, opaque); + + if (token == AccelTokenAccelE) { + drawHLine(d, ops, x, y + font->charHeight - 1, cw, fg); + } } 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++; - - if (*text == '&') { - // Escaped && -- counts as one character - w += font->charWidth; - text++; - continue; - } - - if (*text) { - // Accelerator character -- counts as one character, & is skipped - w += font->charWidth; - text++; - continue; - } - - break; - } + char ch; + while (accelNextToken(&text, &ch) != AccelTokenEndE) { w += font->charWidth; - text++; } return w; diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h index 95ffede..554869e 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.h +++ b/src/libs/kpunch/libdvx/dvxDraw.h @@ -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); diff --git a/src/libs/kpunch/libdvx/dvxFont.h b/src/libs/kpunch/libdvx/dvxFont.h index 9ba735c..af153fb 100644 --- a/src/libs/kpunch/libdvx/dvxFont.h +++ b/src/libs/kpunch/libdvx/dvxFont.h @@ -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, diff --git a/src/libs/kpunch/libdvx/dvxImage.c b/src/libs/kpunch/libdvx/dvxImage.c index fff7454..70d93a4 100644 --- a/src/libs/kpunch/libdvx/dvxImage.c +++ b/src/libs/kpunch/libdvx/dvxImage.c @@ -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 -#include "dvxMem.h" diff --git a/src/libs/kpunch/libdvx/dvxPal.h b/src/libs/kpunch/libdvx/dvxPal.h index 508ac87..68b0351 100644 --- a/src/libs/kpunch/libdvx/dvxPal.h +++ b/src/libs/kpunch/libdvx/dvxPal.h @@ -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++) { diff --git a/src/libs/kpunch/libdvx/dvxPrefs.c b/src/libs/kpunch/libdvx/dvxPrefs.c index cdf0b31..e56365a 100644 --- a/src/libs/kpunch/libdvx/dvxPrefs.c +++ b/src/libs/kpunch/libdvx/dvxPrefs.c @@ -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; } diff --git a/src/libs/kpunch/libdvx/dvxResource.c b/src/libs/kpunch/libdvx/dvxResource.c index cb7005e..37d53bc 100644 --- a/src/libs/kpunch/libdvx/dvxResource.c +++ b/src/libs/kpunch/libdvx/dvxResource.c @@ -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; diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h index aac77f5..b8c4dc9 100644 --- a/src/libs/kpunch/libdvx/dvxTypes.h +++ b/src/libs/kpunch/libdvx/dvxTypes.h @@ -32,6 +32,7 @@ #include #include +#include // ============================================================ // ============================================================ @@ -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. @@ -758,12 +787,12 @@ typedef struct { // Restore is disabled if the window isn't maximized). typedef enum { - SysMenuRestoreE = 1, - SysMenuMoveE = 2, - SysMenuSizeE = 3, - SysMenuMinimizeE = 4, - SysMenuMaximizeE = 5, - SysMenuCloseE = 6, + SysMenuRestoreE = 1, + SysMenuMoveE = 2, + SysMenuSizeE = 3, + SysMenuMinimizeE = 4, + SysMenuMaximizeE = 5, + SysMenuCloseE = 6, SysMenuScreenshotE = 7, SysMenuWinScreenshotE = 8 } SysMenuCmdE; diff --git a/src/libs/kpunch/libdvx/dvxVideo.c b/src/libs/kpunch/libdvx/dvxVideo.c index a911048..3ed348e 100644 --- a/src/libs/kpunch/libdvx/dvxVideo.c +++ b/src/libs/kpunch/libdvx/dvxVideo.c @@ -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)); diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h index 6a0f724..579a87c 100644 --- a/src/libs/kpunch/libdvx/dvxWgt.h +++ b/src/libs/kpunch/libdvx/dvxWgt.h @@ -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. diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h index cd53bdc..c25716b 100644 --- a/src/libs/kpunch/libdvx/dvxWgtP.h +++ b/src/libs/kpunch/libdvx/dvxWgtP.h @@ -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); diff --git a/src/libs/kpunch/libdvx/dvxWm.c b/src/libs/kpunch/libdvx/dvxWm.c index 82bdef5..c9c696f 100644 --- a/src/libs/kpunch/libdvx/dvxWm.c +++ b/src/libs/kpunch/libdvx/dvxWm.c @@ -54,6 +54,7 @@ #include "dvxWm.h" #include "dvxVideo.h" #include "dvxDraw.h" +#include "dvxFont.h" #include "dvxComp.h" #include "dvxWgt.h" #include "dvxPlat.h" @@ -76,13 +77,13 @@ #define GADGET_INSET 2 // inset from title bar edges #define MENU_BAR_GAP 8 // horizontal gap between menu bar labels #define RESIZE_BREAK_INSET 16 // distance from corner to resize groove +#define RESIZE_GRAB_SLOP 2 // extra px beyond visual border for easier edge grab #define CLOSE_ICON_INSET 3 // close bar inset from gadget edges #define MAXIMIZE_ICON_INSET 2 // maximize box inset from gadget edges #define RESTORE_ICON_INSET 4 // restore box inset from gadget edges #define MINIMIZE_ICON_SIZE 4 // filled square size for minimize icon -#define SB_ARROW_ROWS 4 // number of rows in scrollbar arrow glyph -#define SB_ARROW_HALF 2 // half-width of scrollbar arrow tip #define DRAG_DEADZONE 4 // wmDragMove: pixels before drag activates +#define CONTENT_CLEAR_BYTE 0xFF // content buffer fill: clean white background // ============================================================ // Title bar gadget geometry @@ -117,13 +118,17 @@ static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win); static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win); static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp); -static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY); -static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir); static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win); static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size); +static void freeMenuItemSubMenu(MenuItemT *item); static void freeMenuRecursive(MenuT *menu); static bool menuGrowItems(MenuT *menu); +static MenuItemT *menuNewItem(MenuT *menu, const char *label); +static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize); +static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue); static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize); +static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot); +static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY); static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu); @@ -146,7 +151,7 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { int32_t x = CHROME_TOTAL_SIDE; for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - MenuT *menu = &win->menuBar->menus[i]; + MenuT *menu = win->menuBar->menus[i]; int32_t labelW = textWidthAccel(font, menu->label) + CHROME_TITLE_PAD * 2; menu->barX = x; @@ -231,7 +236,7 @@ static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT uint32_t sh = colors->windowShadow; // Fill interior with face color - rectFill(d, ops, x + 4, y + 4, w - 8, h - 8, face); + rectFill(d, ops, x + CHROME_BORDER_WIDTH, y + CHROME_BORDER_WIDTH, w - CHROME_BORDER_WIDTH * 2, h - CHROME_BORDER_WIDTH * 2, face); // Top edge (4 rows) rectFill(d, ops, x, y, w, 1, hi); // row 0: highlight @@ -275,22 +280,30 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon return; } - int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; + int32_t barY = win->y + CHROME_TITLEBAR_BOTTOM; int32_t barH = CHROME_MENU_HEIGHT; rectFill(d, ops, win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH, colors->menuBg); - // Tighten clip to menu bar bounds to prevent label overflow + // Tighten clip to the menu bar bounds INTERSECTED with the incoming dirty + // rect (already set by wmDrawChrome) so labels never paint outside either + // region. setClipRect only clamps to screen bounds, it does not intersect + // with the prior clip, so we must compute the intersection ourselves. int32_t savedClipX = d->clipX; int32_t savedClipY = d->clipY; int32_t savedClipW = d->clipW; int32_t savedClipH = d->clipH; - setClipRect(d, win->x + CHROME_BORDER_WIDTH, barY, - win->w - CHROME_BORDER_WIDTH * 2, barH); + int32_t barLeft = win->x + CHROME_BORDER_WIDTH; + int32_t barRight = barLeft + (win->w - CHROME_BORDER_WIDTH * 2); + int32_t clipLeft = DVX_MAX(barLeft, savedClipX); + int32_t clipTop = DVX_MAX(barY, savedClipY); + int32_t clipRight = DVX_MIN(barRight, savedClipX + savedClipW); + int32_t clipBottom = DVX_MIN(barY + barH, savedClipY + savedClipH); + setClipRect(d, clipLeft, clipTop, clipRight - clipLeft, clipBottom - clipTop); for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - MenuT *menu = &win->menuBar->menus[i]; + MenuT *menu = win->menuBar->menus[i]; int32_t itemX = win->x + menu->barX; int32_t itemW = menu->barW; int32_t textX = itemX + CHROME_TITLE_PAD; @@ -385,9 +398,10 @@ static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorScheme // - Source coordinate lookup tables (srcXTab/srcYTab) are pre-computed // once per call and indexed in the inner loop, eliminating repeated // multiply+shift per pixel. -// - Static lookup table buffers avoid stack frame growth (ICON_SIZE=64 -// entries * 4 bytes = 256 bytes that would otherwise be allocated on -// every call during the compositing loop). +// - Static lookup table buffers avoid stack frame growth (ICON_TOTAL_SIZE=68 +// entries * 4 bytes = 272 bytes per table that would otherwise be allocated +// on every call during the compositing loop). Sized to the largest dstW/dstH +// ever passed (a provided icon scaled to ICON_TOTAL_SIZE), not just ICON_SIZE. // - The bpp switch is outside the column loop so the branch is taken // once per row rather than once per pixel. // - Clipping is done once up front by adjusting row/col start/end ranges, @@ -408,8 +422,8 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW return; } - static int32_t srcXTab[ICON_SIZE]; - static int32_t srcYTab[ICON_SIZE]; + static int32_t srcXTab[ICON_TOTAL_SIZE]; + static int32_t srcYTab[ICON_TOTAL_SIZE]; int32_t visibleCols = colEnd - colStart; int32_t visibleRows = rowEnd - rowStart; @@ -448,122 +462,24 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW } -// Renders a complete scrollbar: trough background, sunken bevel around the -// trough, arrow buttons at each end, and a draggable thumb. -// -// The layering order matters: trough fill first, then sunken bevel over it -// (so bevel edges aren't overwritten), then arrow button bevels + glyphs -// over the trough ends, and finally the thumb bevel in the middle. Arrow -// buttons and thumb use 1px raised bevels for a clickable appearance; -// the trough uses a 1px sunken bevel (swapped highlight/shadow) to appear -// recessed -- standard Motif scrollbar convention. +// Renders a window-level scrollbar by delegating to the shared draw-layer +// painter. Computes the screen position from the window origin plus the +// scrollbar's window-relative x/y, derives the thumb geometry from the +// scroll range, and hands everything to drawScrollbar(). // // winX/winY are the window's screen position; sb->x/y are relative to the // window origin. This split lets scrollbar positions survive window drags // without recalculation -- only winX/winY change. -static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) { +static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) { int32_t x = winX + sb->x; int32_t y = winY + sb->y; - BevelStyleT btnBevel; - btnBevel.highlight = colors->windowHighlight; - btnBevel.shadow = colors->windowShadow; - btnBevel.face = colors->scrollbarBg; - btnBevel.width = 1; + int32_t thumbPos; + int32_t thumbSize; + scrollbarThumbInfo(sb, &thumbPos, &thumbSize); - if (sb->orient == ScrollbarVerticalE) { - // Trough: dithered pattern (GEOS Motif style) - rectFill(d, ops, x, y, SCROLLBAR_WIDTH, sb->length, colors->scrollbarTrough); - - // Sunken bevel around trough - BevelStyleT troughBevel; - troughBevel.highlight = colors->windowShadow; - troughBevel.shadow = colors->windowHighlight; - troughBevel.face = 0; - troughBevel.width = 1; - drawBevel(d, ops, x, y, SCROLLBAR_WIDTH, sb->length, &troughBevel); - - // Up arrow button - drawBevel(d, ops, x, y, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, y, SCROLLBAR_WIDTH, 0); - - // Down arrow button - int32_t downY = y + sb->length - SCROLLBAR_WIDTH; - drawBevel(d, ops, x, downY, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, downY, SCROLLBAR_WIDTH, 1); - - // Thumb - int32_t thumbPos; - int32_t thumbSize; - scrollbarThumbInfo(sb, &thumbPos, &thumbSize); - drawBevel(d, ops, x, y + SCROLLBAR_WIDTH + thumbPos, - SCROLLBAR_WIDTH, thumbSize, &btnBevel); - } else { - // Trough - rectFill(d, ops, x, y, sb->length, SCROLLBAR_WIDTH, colors->scrollbarTrough); - - BevelStyleT troughBevel; - troughBevel.highlight = colors->windowShadow; - troughBevel.shadow = colors->windowHighlight; - troughBevel.face = 0; - troughBevel.width = 1; - drawBevel(d, ops, x, y, sb->length, SCROLLBAR_WIDTH, &troughBevel); - - // Left arrow button - drawBevel(d, ops, x, y, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, y, SCROLLBAR_WIDTH, 2); - - // Right arrow button - int32_t rightX = x + sb->length - SCROLLBAR_WIDTH; - drawBevel(d, ops, rightX, y, SCROLLBAR_WIDTH, SCROLLBAR_WIDTH, &btnBevel); - drawScrollbarArrow(d, ops, colors, rightX, y, SCROLLBAR_WIDTH, 3); - - // Thumb - int32_t thumbPos; - int32_t thumbSize; - scrollbarThumbInfo(sb, &thumbPos, &thumbSize); - drawBevel(d, ops, x + SCROLLBAR_WIDTH + thumbPos, y, - thumbSize, SCROLLBAR_WIDTH, &btnBevel); - } -} - - -// Draws a small triangle arrow glyph inside a scrollbar button. -// dir: 0=up, 1=down, 2=left, 3=right -// -// 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. - -static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir) { - int32_t cx = x + size / 2; - int32_t 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 0: // up - drawHLine(d, ops, cx - i, cy - SB_ARROW_HALF + i, 1 + i * 2, fg); - break; - - case 1: // down - drawHLine(d, ops, cx - i, cy + SB_ARROW_HALF - i, 1 + i * 2, fg); - break; - - case 2: // left - drawVLine(d, ops, cx - SB_ARROW_HALF + i, cy - i, 1 + i * 2, fg); - break; - - case 3: // right - drawVLine(d, ops, cx + SB_ARROW_HALF - i, cy - i, 1 + i * 2, fg); - break; - } - } + drawScrollbar(d, ops, colors, sb->orient, x, y, sb->length, SCROLLBAR_WIDTH, thumbPos, thumbSize); } @@ -608,21 +524,15 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo int32_t by = g.gadgetY + RESTORE_ICON_INSET; int32_t bs = g.gadgetS - RESTORE_ICON_INSET * 2; rectFill(d, ops, bx, by, bs, bs, colors->buttonFace); - drawHLine(d, ops, bx, by, bs, colors->contentFg); - drawHLine(d, ops, bx, by + bs - 1, bs, colors->contentFg); - drawVLine(d, ops, bx, by, bs, colors->contentFg); - drawVLine(d, ops, bx + bs - 1, by, bs, colors->contentFg); + drawRectOutline(d, ops, bx, by, bs, bs, colors->contentFg); } else { // Maximize icon: larger box outline filling more of the gadget int32_t bx = g.maxX + MAXIMIZE_ICON_INSET; int32_t by = g.gadgetY + MAXIMIZE_ICON_INSET; int32_t bs = g.gadgetS - MAXIMIZE_ICON_INSET * 2; rectFill(d, ops, bx, by, bs, bs, colors->buttonFace); - drawHLine(d, ops, bx, by, bs, colors->contentFg); + drawRectOutline(d, ops, bx, by, bs, bs, colors->contentFg); drawHLine(d, ops, bx, by + 1, bs, colors->contentFg); - drawHLine(d, ops, bx, by + bs - 1, bs, colors->contentFg); - drawVLine(d, ops, bx, by, bs, colors->contentFg); - drawVLine(d, ops, bx + bs - 1, by, bs, colors->contentFg); } } @@ -680,18 +590,27 @@ static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT } +// Frees a single menu item's submenu subtree (if any) and clears the +// pointer, so removing or clearing an item that carries a submenu does not +// leak the child MenuT and its descendants. + +static void freeMenuItemSubMenu(MenuItemT *item) { + if (item->subMenu) { + freeMenuRecursive(item->subMenu); + free(item->subMenu); + item->subMenu = NULL; + } +} + + // Walks the submenu tree depth-first, freeing heap-allocated child MenuT -// nodes. The top-level MenuT structs are embedded in the MenuBarT array -// and are freed when the MenuBarT is freed, so this only needs to handle -// dynamically allocated submenu children. +// nodes and the menu's item array. It does NOT free the MenuT it is passed; +// callers own that pointer (top-level menus are freed by wmDestroyMenuBar, +// submenu children by their parent item). static void freeMenuRecursive(MenuT *menu) { for (int32_t i = 0; i < menu->itemCount; i++) { - if (menu->items[i].subMenu) { - freeMenuRecursive(menu->items[i].subMenu); - free(menu->items[i].subMenu); - menu->items[i].subMenu = NULL; - } + freeMenuItemSubMenu(&menu->items[i]); } free(menu->items); @@ -701,11 +620,11 @@ static void freeMenuRecursive(MenuT *menu) { } -// Adds a top-level menu to a window's menu bar. The MenuT struct is -// stored inline in the MenuBarT's dynamic array rather -// than heap-allocated, since menus are created once at window setup and -// the count is small. The positionsDirty flag is set so that the next -// paint triggers computeMenuBarPositions to lay out all labels. +// Adds a top-level menu to a window's menu bar. Each MenuT is heap-allocated +// and the menu bar holds an array of owned pointers, so a MenuT * returned +// here stays valid even when a later wmAddMenu grows (reallocs) that array. +// The positionsDirty flag is set so that the next paint triggers +// computeMenuBarPositions to lay out all labels. // // The accelerator key is parsed from & markers in the label (e.g. // "&File" -> Alt+F), following the CUA/Windows convention. This is @@ -730,6 +649,78 @@ static bool menuGrowItems(MenuT *menu) { } +// Grows the item array, takes the next slot, and initializes the common +// fields (zeroed, label copied + NUL-terminated, enabled, accelerator). +// Returns the new item, or NULL if the array could not grow. +static MenuItemT *menuNewItem(MenuT *menu, const char *label) { + if (!menuGrowItems(menu)) { + return NULL; + } + + MenuItemT *item = &menu->items[menu->itemCount++]; + memset(item, 0, sizeof(*item)); + strncpy(item->label, label, MAX_MENU_LABEL - 1); + item->label[MAX_MENU_LABEL - 1] = '\0'; + item->enabled = true; + item->accelKey = accelParse(label); + return item; +} + + +// Allocates and initializes a scrollbar into *slot for the given window and +// orientation. Shared by wmAddHScrollbar/wmAddVScrollbar so the allocation, +// field init, and content-rect update live in one place. +static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize) { + // Free any prior scrollbar so a second add on the same window does not leak. + if (*slot) { + free(*slot); + *slot = NULL; + } + + *slot = (ScrollbarT *)malloc(sizeof(ScrollbarT)); + + if (!*slot) { + dvxLog("WM: failed to allocate %s scrollbar", orientName); + return NULL; + } + + (*slot)->orient = orient; + (*slot)->min = min; + (*slot)->max = max; + (*slot)->value = min; + (*slot)->pageSize = pageSize; + + // Position will be updated by wmUpdateContentRect + wmUpdateContentRect(win); + + return *slot; +} + + +// Clamps a scrollbar value to its range and, if it changed, dirties the +// scrollbar area and fires the window's onScroll callback. Shared tail of +// wmScrollbarClick and wmScrollbarDrag. +static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue) { + if (sb->value < sb->min) { + sb->value = sb->min; + } + + if (sb->value > sb->max) { + sb->value = sb->max; + } + + if (sb->value != oldValue) { + dirtyListAdd(dl, sbScreenX, sbScreenY, + sb->orient == ScrollbarVerticalE ? SCROLLBAR_WIDTH : sb->length, + sb->orient == ScrollbarVerticalE ? sb->length : SCROLLBAR_WIDTH); + + if (win->onScroll) { + win->onScroll(win, sb->orient, sb->value); + } + } +} + + // Computes thumb position and size within the scrollbar track. The track // is the total scrollbar length minus the two arrow buttons at each end. // @@ -778,7 +769,7 @@ static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) { } for (int32_t m = 0; m < bar->menuCount; m++) { - MenuT *menu = &bar->menus[m]; + MenuT *menu = bar->menus[m]; for (int32_t i = 0; i < menu->itemCount; i++) { if (menu->items[i].id == id) { @@ -808,6 +799,33 @@ static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) { } +// wmMenuFindItemInMenu -- recursively find an item by command ID within a +// single menu tree (the menu itself plus every nested submenu). Used for +// popup/context menus that are not attached to a menu bar, so the menu-bar +// lookups (wmMenuFindItem) do not apply. +MenuItemT *wmMenuFindItemInMenu(MenuT *menu, int32_t id) { + if (!menu) { + return NULL; + } + + for (int32_t i = 0; i < menu->itemCount; i++) { + if (menu->items[i].id == id) { + return &menu->items[i]; + } + + if (menu->items[i].subMenu) { + MenuItemT *found = wmMenuFindItemInMenu(menu->items[i].subMenu, id); + + if (found) { + return found; + } + } + } + + return NULL; +} + + // Adds a horizontal scrollbar to a window. The scrollbar steals // SCROLLBAR_WIDTH pixels from the bottom of the content area (handled by // wmUpdateContentRect). The caller specifies the logical scroll range; the @@ -820,29 +838,14 @@ static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) { // scrollbar's geometry depends on the first one already being accounted for. ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize) { - win->hScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); - - if (!win->hScroll) { - dvxLog("WM: failed to allocate horizontal scrollbar"); - return NULL; - } - - win->hScroll->orient = ScrollbarHorizontalE; - win->hScroll->min = min; - win->hScroll->max = max; - win->hScroll->value = min; - win->hScroll->pageSize = pageSize; - - wmUpdateContentRect(win); - - return win->hScroll; + return scrollbarAdd(win, &win->hScroll, ScrollbarHorizontalE, "horizontal", min, max, pageSize); } MenuT *wmAddMenu(MenuBarT *bar, const char *label) { if (bar->menuCount >= bar->menuCap) { - int32_t newCap = bar->menuCap ? bar->menuCap * 2 : 8; - MenuT *newBuf = (MenuT *)realloc(bar->menus, newCap * sizeof(MenuT)); + int32_t newCap = bar->menuCap ? bar->menuCap * 2 : 8; + MenuT **newBuf = (MenuT **)realloc(bar->menus, newCap * sizeof(MenuT *)); if (!newBuf) { return NULL; @@ -852,11 +855,16 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) { bar->menuCap = newCap; } - MenuT *menu = &bar->menus[bar->menuCount]; - memset(menu, 0, sizeof(*menu)); + MenuT *menu = (MenuT *)calloc(1, sizeof(MenuT)); + + if (!menu) { + return NULL; + } + strncpy(menu->label, label, MAX_MENU_LABEL - 1); menu->label[MAX_MENU_LABEL - 1] = '\0'; menu->accelKey = accelParse(label); + bar->menus[bar->menuCount] = menu; bar->menuCount++; bar->positionsDirty = true; @@ -871,6 +879,10 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) { // the content area by CHROME_MENU_HEIGHT to make room for the bar. MenuBarT *wmAddMenuBar(WindowT *win) { + // Tear down any existing bar (recursive menu cleanup) so a second add does + // not leak the prior MenuBarT, its menus array, and all MenuT/submenus. + wmDestroyMenuBar(win, NULL); + win->menuBar = (MenuBarT *)malloc(sizeof(MenuBarT)); if (!win->menuBar) { @@ -887,56 +899,39 @@ MenuBarT *wmAddMenuBar(WindowT *win) { void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) { - if (!menuGrowItems(menu)) { + MenuItemT *item = menuNewItem(menu, label); + + if (!item) { return; } - MenuItemT *item = &menu->items[menu->itemCount]; - memset(item, 0, sizeof(*item)); - strncpy(item->label, label, MAX_MENU_LABEL - 1); - item->label[MAX_MENU_LABEL - 1] = '\0'; - item->id = id; - item->type = MenuItemCheckE; - item->enabled = true; - item->checked = checked; - item->accelKey = accelParse(label); - menu->itemCount++; + item->id = id; + item->type = MenuItemCheckE; + item->checked = checked; } void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) { - if (!menuGrowItems(menu)) { + MenuItemT *item = menuNewItem(menu, label); + + if (!item) { return; } - MenuItemT *item = &menu->items[menu->itemCount]; - memset(item, 0, sizeof(*item)); - strncpy(item->label, label, MAX_MENU_LABEL - 1); - item->label[MAX_MENU_LABEL - 1] = '\0'; - item->id = id; - item->separator = false; - item->enabled = true; - item->checked = false; - item->accelKey = accelParse(label); - menu->itemCount++; + item->id = id; } void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) { - if (!menuGrowItems(menu)) { + MenuItemT *item = menuNewItem(menu, label); + + if (!item) { return; } - MenuItemT *item = &menu->items[menu->itemCount]; - memset(item, 0, sizeof(*item)); - strncpy(item->label, label, MAX_MENU_LABEL - 1); - item->label[MAX_MENU_LABEL - 1] = '\0'; - item->id = id; - item->type = MenuItemRadioE; - item->enabled = true; - item->checked = checked; - item->accelKey = accelParse(label); - menu->itemCount++; + item->id = id; + item->type = MenuItemRadioE; + item->checked = checked; } @@ -960,26 +955,21 @@ void wmAddMenuSeparator(MenuT *menu) { // item opens the child rather than firing a command. MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) { - if (!menuGrowItems(parentMenu)) { - return NULL; - } - MenuT *child = (MenuT *)calloc(1, sizeof(MenuT)); if (!child) { return NULL; } - MenuItemT *item = &parentMenu->items[parentMenu->itemCount]; - memset(item, 0, sizeof(*item)); - strncpy(item->label, label, MAX_MENU_LABEL - 1); - item->label[MAX_MENU_LABEL - 1] = '\0'; - item->id = -1; - item->separator = false; - item->enabled = true; - item->accelKey = accelParse(label); - item->subMenu = child; - parentMenu->itemCount++; + MenuItemT *item = menuNewItem(parentMenu, label); + + if (!item) { + free(child); + return NULL; + } + + item->id = -1; + item->subMenu = child; return child; } @@ -990,23 +980,25 @@ MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) { // content area. See wmAddHScrollbar for design notes. ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize) { - win->vScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); + return scrollbarAdd(win, &win->vScroll, ScrollbarVerticalE, "vertical", min, max, pageSize); +} - if (!win->vScroll) { - dvxLog("WM: failed to allocate vertical scrollbar"); - return NULL; + +// Adjusts a stored window-stack index after the window at removedSlot has +// been removed and higher entries shifted down. Returns -1 when the stored +// index was the removed window itself -- the caller's interaction (drag, +// resize, scroll) targeted a now-freed window and must be cancelled. + +static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot) { + if (index == removedSlot) { + return -1; } - win->vScroll->orient = ScrollbarVerticalE; - win->vScroll->min = min; - win->vScroll->max = max; - win->vScroll->value = min; - win->vScroll->pageSize = pageSize; + if (index > removedSlot) { + return index - 1; + } - // Position will be updated by wmUpdateContentRect - wmUpdateContentRect(win); - - return win->vScroll; + return index; } @@ -1014,6 +1006,10 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page void wmClearMenuItems(MenuT *menu) { if (menu) { + for (int32_t i = 0; i < menu->itemCount; i++) { + freeMenuItemSubMenu(&menu->items[i]); + } + menu->itemCount = 0; } } @@ -1078,13 +1074,14 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int win->y = y; win->w = w; win->h = h; - win->visible = true; - win->focused = false; - win->minimized = false; - win->maximized = false; - win->resizable = resizable; - win->maxW = WM_MAX_FROM_SCREEN; - win->maxH = WM_MAX_FROM_SCREEN; + win->visible = true; + win->focused = false; + win->minimized = false; + win->maximized = false; + win->resizable = resizable; + win->destroyPending = false; + win->maxW = WM_MAX_FROM_SCREEN; + win->maxH = WM_MAX_FROM_SCREEN; strncpy(win->title, title, MAX_TITLE_LEN - 1); win->title[MAX_TITLE_LEN - 1] = '\0'; @@ -1103,7 +1100,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int return NULL; } - memset(win->contentBuf, 0xFF, bufSize); + memset(win->contentBuf, CONTENT_CLEAR_BYTE, bufSize); } win->paintNeeded = PAINT_FULL; @@ -1115,19 +1112,27 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int } -void wmDestroyMenuBar(WindowT *win) { +void wmDestroyMenuBar(WindowT *win, const DisplayT *d) { if (!win || !win->menuBar) { return; } for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - freeMenuRecursive(&win->menuBar->menus[i]); + freeMenuRecursive(win->menuBar->menus[i]); + free(win->menuBar->menus[i]); } free(win->menuBar->menus); free(win->menuBar); win->menuBar = NULL; wmUpdateContentRect(win); + + // Removing the bar grows the content area; the buffer must grow with + // it or the next full repaint overruns it. d is NULL only during + // window teardown, where the buffer is already gone. + if (d) { + wmReallocContentBuf(win, d); + } } @@ -1135,8 +1140,9 @@ void wmDestroyMenuBar(WindowT *win) { // The stack is compacted by shifting entries down (O(N), but N <= 64 and // destroy is infrequent). focusedIdx is adjusted to track the same logical // window after the shift: if the focused window was above the destroyed one, -// its index decreases by 1; if the destroyed window was focused, focus moves -// to the new topmost window. +// its index decreases by 1; if the destroyed window itself was focused, +// focusedIdx is cleared to -1 so the caller's follow-up wmSetFocus performs a +// full old != new transition and fires onFocus on the new top window. // // Resource cleanup order: widget tree first (may reference window fields), // then content buffer, menus (with recursive submenu cleanup), scrollbars, @@ -1152,11 +1158,22 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) { stack->count--; if (stack->focusedIdx == i) { - stack->focusedIdx = stack->count > 0 ? stack->count - 1 : -1; + // Clear focus rather than eagerly assigning the new top + // window. The caller (dvxDestroyWindow) follows up with + // wmSetFocus, which only fires onFocus/onBlur when the old + // and new windows differ; pre-assigning here would make + // oldWin == newWin and suppress the new window's onFocus. + stack->focusedIdx = -1; } else if (stack->focusedIdx > i) { stack->focusedIdx--; } + // An in-progress drag/resize/scroll on the destroyed window is + // cancelled (-1); one above the removed slot shifts down. + stack->dragWindow = wmAdjustIndexForRemoval(stack->dragWindow, i); + stack->resizeWindow = wmAdjustIndexForRemoval(stack->resizeWindow, i); + stack->scrollWindow = wmAdjustIndexForRemoval(stack->scrollWindow, i); + break; } } @@ -1168,8 +1185,9 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) { } free(win->contentBuf); + win->contentBuf = NULL; - wmDestroyMenuBar(win); + wmDestroyMenuBar(win, NULL); free(win->vScroll); free(win->hScroll); @@ -1231,8 +1249,7 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou } } - WindowT *win = stack->windows[stack->dragWindow]; - int32_t minVisible = 50; + WindowT *win = stack->windows[stack->dragWindow]; dirtyListAdd(dl, win->x, win->y, win->w, win->h); @@ -1245,14 +1262,14 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou if (win->y < 0) { win->y = 0; } - if (win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT > screenH) { - win->y = screenH - CHROME_BORDER_WIDTH - CHROME_TITLE_HEIGHT; + if (win->y + CHROME_TITLEBAR_BOTTOM > screenH) { + win->y = screenH - CHROME_TITLEBAR_BOTTOM; } - if (win->x + win->w < minVisible) { - win->x = minVisible - win->w; + if (win->x + win->w < DRAG_MIN_VISIBLE) { + win->x = DRAG_MIN_VISIBLE - win->w; } - if (win->x > screenW - minVisible) { - win->x = screenW - minVisible; + if (win->x > screenW - DRAG_MIN_VISIBLE) { + win->x = screenW - DRAG_MIN_VISIBLE; } dirtyListAdd(dl, win->x, win->y, win->w, win->h); @@ -1310,7 +1327,7 @@ void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, con BevelStyleT innerBevel; innerBevel.highlight = colors->windowShadow; // swapped for sunken innerBevel.shadow = colors->windowHighlight; - innerBevel.face = 0; // no fill + innerBevel.face = BEVEL_NO_FILL; // no fill innerBevel.width = CHROME_INNER_BORDER; drawBevel(d, ops, innerX, innerY, innerW, innerH, &innerBevel); @@ -1443,11 +1460,11 @@ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colo setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h); if (win->vScroll) { - drawScrollbar(d, ops, colors, win->vScroll, win->x, win->y); + wmDrawScrollbar(d, ops, colors, win->vScroll, win->x, win->y); } if (win->hScroll) { - drawScrollbar(d, ops, colors, win->hScroll, win->x, win->y); + wmDrawScrollbar(d, ops, colors, win->hScroll, win->x, win->y); } setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH); @@ -1455,52 +1472,24 @@ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colo void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) { - int32_t sbW = SCROLLBAR_WIDTH; - - // Trough - rectFill(d, ops, x, y, sbW, h, colors->scrollbarTrough); - - BevelStyleT troughBevel; - troughBevel.highlight = colors->windowShadow; - troughBevel.shadow = colors->windowHighlight; - troughBevel.face = 0; - troughBevel.width = 1; - drawBevel(d, ops, x, y, sbW, h, &troughBevel); - - BevelStyleT btnBevel; - btnBevel.highlight = colors->windowHighlight; - btnBevel.shadow = colors->windowShadow; - btnBevel.face = colors->scrollbarBg; - btnBevel.width = 1; - - // Up arrow - drawBevel(d, ops, x, y, sbW, sbW, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, y, sbW, 0); - - // Down arrow - int32_t downY = y + h - sbW; - drawBevel(d, ops, x, downY, sbW, sbW, &btnBevel); - drawScrollbarArrow(d, ops, colors, x, downY, sbW, 1); - - // Thumb - int32_t trackLen = h - sbW * 2; + int32_t sbW = SCROLLBAR_WIDTH; + int32_t trackLen = h - sbW * 2; + int32_t thumbPos = 0; + int32_t thumbSize = 0; if (trackLen > 0 && totalItems > visibleItems) { int32_t maxScroll = totalItems - visibleItems; - int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / totalItems); + thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / totalItems); if (thumbSize < sbW) { thumbSize = sbW; } - int32_t thumbPos = 0; - - if (maxScroll > 0) { - thumbPos = (int32_t)(((int64_t)scrollPos * (trackLen - thumbSize)) / maxScroll); - } - - drawBevel(d, ops, x, y + sbW + thumbPos, sbW, thumbSize, &btnBevel); + // maxScroll is > 0 here (guaranteed by totalItems > visibleItems above) + thumbPos = (int32_t)(((int64_t)scrollPos * (trackLen - thumbSize)) / maxScroll); } + + drawScrollbar(d, ops, colors, ScrollbarVerticalE, x, y, h, sbW, thumbPos, thumbSize); } @@ -1595,10 +1584,14 @@ int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hi return i; } - // Menu bar + // Menu bar -- bounded to the drawn fill region (drawMenuBar) so the + // side border at menu height stays grabbable for resize / falls + // through to content if (win->menuBar && - my >= win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT && - my < win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT) { + my >= win->y + CHROME_TITLEBAR_BOTTOM && + my < win->y + CHROME_TITLEBAR_BOTTOM + CHROME_MENU_HEIGHT && + mx >= win->x + CHROME_BORDER_WIDTH && + mx < win->x + win->w - CHROME_BORDER_WIDTH) { *hitPart = HIT_MENU; return i; } @@ -1706,19 +1699,7 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT win->h = newH; win->maximized = true; - wmUpdateContentRect(win); - wmReallocContentBuf(win, d); - - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - win->paintNeeded = PAINT_FULL; - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->iconNeedsRefresh = true; - } + wmResizeCommit(win, d); dirtyListAdd(dl, win->x, win->y, win->w, win->h); } @@ -1762,6 +1743,48 @@ void wmMenuItemSetChecked(MenuBarT *bar, int32_t id, bool checked) { } +void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked) { + // The item may be nested in a submenu; locate both the item and the menu + // that directly contains it so radio-group siblings (which live in that + // same containing menu) can be unchecked. + if (!menu) { + return; + } + + for (int32_t i = 0; i < menu->itemCount; i++) { + if (menu->items[i].id == id) { + MenuItemT *item = &menu->items[i]; + + if (item->type == MenuItemRadioE && checked) { + int32_t groupStart = i; + + while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) { + groupStart--; + } + + int32_t groupEnd = i; + + while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) { + groupEnd++; + } + + for (int32_t g = groupStart; g <= groupEnd; g++) { + menu->items[g].checked = (g == i); + } + } else { + item->checked = checked; + } + + return; + } + + if (menu->items[i].subMenu) { + wmMenuItemSetCheckedInMenu(menu->items[i].subMenu, id, checked); + } + } +} + + void wmMenuItemSetEnabled(MenuBarT *bar, int32_t id, bool enabled) { MenuItemT *item = wmMenuFindItem(bar, id, NULL); @@ -1801,6 +1824,10 @@ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) { if (stack->focusedIdx >= 0 && stack->focusedIdx < stack->count) { WindowT *oldWin = stack->windows[stack->focusedIdx]; + // Clear the stale focused flag before dropping focus to -1, mirroring + // wmSetFocus's invariant that only the focusedIdx window has it set. + oldWin->focused = false; + if (oldWin->onBlur) { oldWin->onBlur(oldWin); } @@ -1931,19 +1958,11 @@ void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { int32_t menuW = CHROME_TOTAL_SIDE; for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - // Count visible characters (skip & accelerator prefix) - const char *lbl = win->menuBar->menus[i].label; - int32_t visChars = 0; + // Use the same width calc as computeMenuBarPositions so the + // minimum size and the laid-out bar share one source of truth. + const char *lbl = win->menuBar->menus[i]->label; - for (const char *c = lbl; *c; c++) { - if (*c == '&' && *(c + 1) != '&' && *(c + 1) != '\0') { - continue; // skip accelerator marker - } - - visChars++; - } - - menuW += visChars * charW + CHROME_TITLE_PAD * 2; + menuW += textWidthAccel(&dvxFont8x16, lbl) + CHROME_TITLE_PAD * 2; if (i < win->menuBar->menuCount - 1) { menuW += MENU_BAR_GAP; @@ -2063,6 +2082,22 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) { } else if (stack->dragWindow > idx) { stack->dragWindow--; } + + // resizeWindow / scrollWindow must shift too (wmDestroyWindow already + // adjusts all three): a raise during an active scroll or resize -- + // e.g. an onScroll callback raising a window -- would otherwise leave + // the next wmResizeMove / wmScrollbarDrag operating on the wrong slot. + if (stack->resizeWindow == idx) { + stack->resizeWindow = stack->count - 1; + } else if (stack->resizeWindow > idx) { + stack->resizeWindow--; + } + + if (stack->scrollWindow == idx) { + stack->scrollWindow = stack->count - 1; + } else if (stack->scrollWindow > idx) { + stack->scrollWindow--; + } } @@ -2094,7 +2129,7 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { return -1; } - memset(win->contentBuf, 0xFF, bufSize); + memset(win->contentBuf, CONTENT_CLEAR_BYTE, bufSize); return 0; } @@ -2109,6 +2144,7 @@ bool wmRemoveMenuItem(MenuT *menu, int32_t id) { for (int32_t i = 0; i < menu->itemCount; i++) { if (menu->items[i].id == id && !menu->items[i].separator) { + freeMenuItemSubMenu(&menu->items[i]); memmove(&menu->items[i], &menu->items[i + 1], (menu->itemCount - i - 1) * sizeof(MenuItemT)); menu->itemCount--; return true; @@ -2133,6 +2169,37 @@ void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouse } +// Applies a completed window geometry change: recomputes the content rect, +// reallocates the content buffer, notifies the app via onResize, then +// requests a full repaint into the new buffer. Shared by mouse resize, +// keyboard resize, maximize, and restore so every path repaints the same +// way. PAINT_FULL is required -- widgetOnPaint only relays out and paints +// the whole widget tree (not just dirty widgets) at that level. + +void wmResizeCommit(WindowT *win, const DisplayT *d) { + wmUpdateContentRect(win); + + if (wmReallocContentBuf(win, d) != 0) { + return; + } + + if (win->onResize) { + win->onResize(win, win->contentW, win->contentH); + } + + if (win->onPaint) { + win->paintNeeded = PAINT_FULL; + RectT fullRect = {0, 0, win->contentW, win->contentH}; + win->onPaint(win, &fullRect); + // widgetOnPaint clears paintNeeded itself; clear it for raw-paint + // windows too so the deferred flush doesn't repaint this frame. + // Callers add their own dirty rects. + win->paintNeeded = PAINT_NONE; + win->iconNeedsRefresh = true; + } +} + + // Determines which edge(s) of a window the mouse is over. Returns a bitmask // of RESIZE_LEFT/RIGHT/TOP/BOTTOM flags. Corner hits (e.g. top-left) return // two flags OR'd together, enabling diagonal resize. @@ -2144,7 +2211,7 @@ void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouse int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my) { int32_t edge = RESIZE_NONE; - int32_t border = CHROME_BORDER_WIDTH + 2; + int32_t border = CHROME_BORDER_WIDTH + RESIZE_GRAB_SLOP; if (mx >= win->x && mx < win->x + border) { edge |= RESIZE_LEFT; @@ -2236,6 +2303,14 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_ newX = win->x + win->w - minW; } + // Keep the window reachable: if the right edge is off-screen, + // dragging the left edge inward must not move the whole window + // past the right side of the screen. + if (newX > d->width - DRAG_MIN_VISIBLE) { + newW += newX - (d->width - DRAG_MIN_VISIBLE); + newX = d->width - DRAG_MIN_VISIBLE; + } + win->x = newX; win->w = newW; mx = newX; @@ -2256,6 +2331,12 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_ newW = minW; } + // Keep the window reachable: with x off-screen left, shrinking + // the right edge must not push the whole window off-screen. + if (win->x + newW < DRAG_MIN_VISIBLE) { + newW = DRAG_MIN_VISIBLE - win->x; + } + win->w = newW; mx = win->x + newW; } @@ -2279,6 +2360,14 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_ newY = win->y + win->h - minH; } + // Keep the title bar reachable: if the bottom edge is off-screen, + // dragging the top edge down must not push the title bar below + // the screen. + if (newY > d->height - CHROME_TITLEBAR_BOTTOM) { + newH += newY - (d->height - CHROME_TITLEBAR_BOTTOM); + newY = d->height - CHROME_TITLEBAR_BOTTOM; + } + win->y = newY; win->h = newH; my = newY; @@ -2306,20 +2395,7 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_ // Resizing clears the maximized state. win->maximized = false; - wmUpdateContentRect(win); - wmReallocContentBuf(win, d); - - // Call resize callback, then repaint - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - win->paintNeeded = PAINT_FULL; - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->iconNeedsRefresh = true; - } + wmResizeCommit(win, d); // Always update dragOff to the clamped position so the next delta // is computed from the edge, not from where the mouse wandered. @@ -2357,19 +2433,7 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT * win->h = win->preMaxH; win->maximized = false; - wmUpdateContentRect(win); - wmReallocContentBuf(win, d); - - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - win->paintNeeded = PAINT_FULL; - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->iconNeedsRefresh = true; - } + wmResizeCommit(win, d); // Mark restored position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); @@ -2466,7 +2530,7 @@ void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t else if (relY >= SCROLLBAR_WIDTH + thumbPos && relY < SCROLLBAR_WIDTH + thumbPos + thumbSize) { stack->scrollWindow = idx; - stack->scrollOrient = 0; + stack->scrollOrient = SCROLL_VERTICAL; stack->scrollDragOff = my - (sbScreenY + SCROLLBAR_WIDTH + thumbPos); return; } @@ -2493,7 +2557,7 @@ void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t else if (relX >= SCROLLBAR_WIDTH + thumbPos && relX < SCROLLBAR_WIDTH + thumbPos + thumbSize) { stack->scrollWindow = idx; - stack->scrollOrient = 1; + stack->scrollOrient = SCROLL_HORIZONTAL; stack->scrollDragOff = mx - (sbScreenX + SCROLLBAR_WIDTH + thumbPos); return; } @@ -2507,25 +2571,7 @@ void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t } } - // Clamp value - if (sb->value < sb->min) { - sb->value = sb->min; - } - - if (sb->value > sb->max) { - sb->value = sb->max; - } - - // Dirty the scrollbar area and fire callback - if (sb->value != oldValue) { - dirtyListAdd(dl, sbScreenX, sbScreenY, - sb->orient == ScrollbarVerticalE ? SCROLLBAR_WIDTH : sb->length, - sb->orient == ScrollbarVerticalE ? sb->length : SCROLLBAR_WIDTH); - - if (win->onScroll) { - win->onScroll(win, sb->orient, sb->value); - } - } + scrollbarCommitValue(win, sb, dl, sbScreenX, sbScreenY, oldValue); } @@ -2543,7 +2589,7 @@ void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my } WindowT *win = stack->windows[stack->scrollWindow]; - ScrollbarT *sb = (stack->scrollOrient == 0) ? win->vScroll : win->hScroll; + ScrollbarT *sb = (stack->scrollOrient == SCROLL_VERTICAL) ? win->vScroll : win->hScroll; if (!sb) { wmScrollbarEnd(stack); @@ -2576,26 +2622,9 @@ void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my } // Convert pixel position to value - sb->value = sb->min + (mousePos * range) / (trackLen - thumbSize); + sb->value = sb->min + (int32_t)(((int64_t)mousePos * range) / (trackLen - thumbSize)); - // Clamp - if (sb->value < sb->min) { - sb->value = sb->min; - } - - if (sb->value > sb->max) { - sb->value = sb->max; - } - - if (sb->value != oldValue) { - dirtyListAdd(dl, sbScreenX, sbScreenY, - sb->orient == ScrollbarVerticalE ? SCROLLBAR_WIDTH : sb->length, - sb->orient == ScrollbarVerticalE ? sb->length : SCROLLBAR_WIDTH); - - if (win->onScroll) { - win->onScroll(win, sb->orient, sb->value); - } - } + scrollbarCommitValue(win, sb, dl, sbScreenX, sbScreenY, oldValue); } @@ -2624,7 +2653,7 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) { // Dirty the old title bar dirtyListAdd(dl, oldWin->x, oldWin->y, - oldWin->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT); + oldWin->w, CHROME_TITLEBAR_BOTTOM); } // Focus new window @@ -2634,7 +2663,7 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) { // Dirty the new title bar dirtyListAdd(dl, newWin->x, newWin->y, - newWin->w, CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT); + newWin->w, CHROME_TITLEBAR_BOTTOM); // Fire callbacks (after state is updated) if (oldWin && oldWin != newWin && oldWin->onBlur) { @@ -2773,4 +2802,10 @@ void wmUpdateContentRect(WindowT *win) { if (win->contentW < 0) { win->contentW = 0; } if (win->contentH < 0) { win->contentH = 0; } + + // The content clamps above do not propagate into the scrollbar lengths + // (assigned from the pre-clamp content size), so a window smaller than its + // chrome would leave a negative length feeding scrollbarThumbInfo/draw. + if (win->vScroll && win->vScroll->length < 0) { win->vScroll->length = 0; } + if (win->hScroll && win->hScroll->length < 0) { win->hScroll->length = 0; } } diff --git a/src/libs/kpunch/libdvx/dvxWm.h b/src/libs/kpunch/libdvx/dvxWm.h index 87747d3..5afc64f 100644 --- a/src/libs/kpunch/libdvx/dvxWm.h +++ b/src/libs/kpunch/libdvx/dvxWm.h @@ -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); diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h index 0118e16..bd4e388 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlat.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h @@ -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 diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c index a302e77..3a705a8 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c @@ -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). -static int32_t sMouseRangeW = 0; -static int32_t sMouseRangeH = 0; -static int32_t sCurX = 0; -static int32_t sCurY = 0; +// 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,12 +274,22 @@ 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; +static bool sKeyUpInstalled = false; // Crash recovery state -- set up by platformInstallCrashHandler and used // by platformCrashHandler to longjmp back to the caller's recovery point. @@ -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(®ion); + } + + 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(®ion); + } } @@ -1407,72 +1609,133 @@ 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; - sMouseRangeW = screenW; - sMouseRangeH = screenH; - sCurX = screenW / 2; - sCurY = screenH / 2; + sMouseRangeW = screenW; + 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. - memset(&r, 0, sizeof(r)); - r.x.ax = 0x0004; - r.x.cx = x; - r.x.dx = y; - __dpmi_int(0x33, &r); - - memset(&r, 0, sizeof(r)); - r.x.ax = 0x000B; - __dpmi_int(0x33, &r); + 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,17 +2036,25 @@ 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; } - int32_t x0 = x < 0 ? 0 : x; - int32_t x1 = (x + w) > SPLASH_VGA_W ? SPLASH_VGA_W : (x + w); + 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) diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c index 74c07b7..c71670c 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c @@ -45,6 +45,9 @@ #include #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; } diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c index d347d15..d6e2a66 100644 --- a/src/libs/kpunch/libdvx/widgetCore.c +++ b/src/libs/kpunch/libdvx/widgetCore.c @@ -67,20 +67,23 @@ // otherwise dangling pointers would cause crashes. widgetDestroyChildren() // and wgtDestroy() handle this cleanup. -bool sCursorBlinkOn = true; // text cursor blink phase (toggled by wgtUpdateCursorBlink) -clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint -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 +bool sCursorBlinkOn = true; // text cursor blink phase (toggled by wgtUpdateCursorBlink) +clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint +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) +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; diff --git a/src/libs/kpunch/libdvx/widgetEvent.c b/src/libs/kpunch/libdvx/widgetEvent.c index 8120c2a..048dedc 100644 --- a/src/libs/kpunch/libdvx/widgetEvent.c +++ b/src/libs/kpunch/libdvx/widgetEvent.c @@ -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,7 +141,11 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) { } wmAddVScrollbar(win, 0, maxV, pageV); - win->vScroll->value = DVX_MIN(oldVValue, maxV); + + // wmAddVScrollbar leaves win->vScroll NULL if its allocation failed. + if (win->vScroll) { + win->vScroll->value = DVX_MIN(oldVValue, maxV); + } } if (needH) { @@ -152,7 +157,10 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) { } wmAddHScrollbar(win, 0, maxH, pageH); - win->hScroll->value = DVX_MIN(oldHValue, maxH); + + if (win->hScroll) { + win->hScroll->value = DVX_MIN(oldHValue, maxH); + } } if (changed) { @@ -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,82 +495,106 @@ 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); } - // Dispatch to the hit widget's mouse handler via vtable. The handler - // is responsible for setting sFocusedWidget if it wants focus. - if (hit->enabled) { - wclsOnMouse(hit, root, vx, vy); - } + // 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; - // Universal click/double-click callbacks -- fire for ALL widget types - // after the type-specific handler has run. Buttons and image buttons - // 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) { - int32_t clicks = multiClickDetect(vx, vy); - bool isBtn = (hit->wclass && (hit->wclass->flags & WCLASS_PRESS_RELEASE)); + // 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 (clicks >= 2 && hit->onDblClick) { - hit->onDblClick(hit); - } else if (!isBtn && hit->onClick) { - hit->onClick(hit); + 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) { + wclsOnMouse(hit, root, vx, vy); + } + + // Universal click/double-click callbacks -- fire for ALL widget types + // after the type-specific handler has run. Buttons and image buttons + // 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 (sWidgetGen == gen && hit->enabled) { + int32_t clicks = multiClickDetect(vx, vy); + bool isBtn = (hit->wclass && (hit->wclass->flags & WCLASS_PRESS_RELEASE)); + + if (clicks >= 2 && hit->onDblClick) { + hit->onDblClick(hit); + } else if (!isBtn && hit->onClick) { + 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,13 +606,22 @@ 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) { - prevFocus->onBlur(prevFocus); - } + // 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 (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) { - sFocusedWidget->onFocus(sFocusedWidget); + 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); +} diff --git a/src/libs/kpunch/libdvx/widgetLayout.c b/src/libs/kpunch/libdvx/widgetLayout.c index 5ed6003..cd64d5e 100644 --- a/src/libs/kpunch/libdvx/widgetLayout.c +++ b/src/libs/kpunch/libdvx/widgetLayout.c @@ -78,10 +78,11 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) { } // Widgets with getLayoutMetrics override default padding/gap/extraTop - int32_t frameExtraTop = 0; + int32_t frameExtraTop = 0; + int32_t metricBorderW = 0; + bool hasMetrics = wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS); - if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) { - int32_t metricBorderW = 0; + 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; } } diff --git a/src/libs/kpunch/libdvx/widgetOps.c b/src/libs/kpunch/libdvx/widgetOps.c index 9e1881f..080bf52 100644 --- a/src/libs/kpunch/libdvx/widgetOps.c +++ b/src/libs/kpunch/libdvx/widgetOps.c @@ -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,14 +368,20 @@ 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) { - prev->onBlur(prev); + 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) { @@ -437,20 +434,30 @@ void wgtSetVisible(WidgetT *w, bool visible) { w->visible = visible; // Notify parent chain of child visibility change 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); 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; + } + } +} + + // Recursive paint walker. For each visible widget: // 1. Call the widget's paint function (if any) via vtable. // 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion -- @@ -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; + } } } diff --git a/src/libs/kpunch/libdvx/widgetScrollbar.c b/src/libs/kpunch/libdvx/widgetScrollbar.c index 900d7f0..9de3b77 100644 --- a/src/libs/kpunch/libdvx/widgetScrollbar.c +++ b/src/libs/kpunch/libdvx/widgetScrollbar.c @@ -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 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 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); diff --git a/src/libs/kpunch/libtasks/taskSwch.h b/src/libs/kpunch/libtasks/taskSwch.h index 750671a..972700f 100644 --- a/src/libs/kpunch/libtasks/taskSwch.h +++ b/src/libs/kpunch/libtasks/taskSwch.h @@ -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); diff --git a/src/libs/kpunch/libtasks/taskswitch.c b/src/libs/kpunch/libtasks/taskswitch.c index 9d8a453..3053769 100644 --- a/src/libs/kpunch/libtasks/taskswitch.c +++ b/src/libs/kpunch/libtasks/taskswitch.c @@ -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); } } @@ -595,8 +669,15 @@ void tsRecoverToMain(void) { return; } - currentIdx = 0; + 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); } diff --git a/src/libs/kpunch/listhelp/listHelp.c b/src/libs/kpunch/listhelp/listHelp.c index 6158536..6bd9988 100644 --- a/src/libs/kpunch/listhelp/listHelp.c +++ b/src/libs/kpunch/listhelp/listHelp.c @@ -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) { diff --git a/src/libs/kpunch/serial/packet/packet.c b/src/libs/kpunch/serial/packet/packet.c index 3b86deb..9dc0e4e 100644 --- a/src/libs/kpunch/serial/packet/packet.c +++ b/src/libs/kpunch/serial/packet/packet.c @@ -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; } diff --git a/src/libs/kpunch/serial/packet/packet.h b/src/libs/kpunch/serial/packet/packet.h index ad62078..00b771c 100644 --- a/src/libs/kpunch/serial/packet/packet.h +++ b/src/libs/kpunch/serial/packet/packet.h @@ -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 diff --git a/src/libs/kpunch/serial/rs232/rs232.c b/src/libs/kpunch/serial/rs232/rs232.c index dd8633c..232cbca 100644 --- a/src/libs/kpunch/serial/rs232/rs232.c +++ b/src/libs/kpunch/serial/rs232/rs232.c @@ -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 - UART_WRITE_IER(port, IER_DATA_READY | IER_MODEM_STATUS | IER_ERRORS); - PIC_ENABLE_IRQ(port->irq); + // 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; } @@ -1256,7 +1278,8 @@ int rs232SetBase(int com, int base) { int rs232SetBps(int com, int32_t bps) { - Rs232StateT *port = &sComPorts[com]; + 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); diff --git a/src/libs/kpunch/serial/seclink/secLink.c b/src/libs/kpunch/serial/seclink/secLink.c index a5fccc0..59ccd76 100644 --- a/src/libs/kpunch/serial/seclink/secLink.c +++ b/src/libs/kpunch/serial/seclink/secLink.c @@ -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; } diff --git a/src/libs/kpunch/serial/seclink/secLink.h b/src/libs/kpunch/serial/seclink/secLink.h index 9cd86f9..d0563db 100644 --- a/src/libs/kpunch/serial/seclink/secLink.h +++ b/src/libs/kpunch/serial/seclink/secLink.h @@ -52,6 +52,7 @@ #include #include +#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. diff --git a/src/libs/kpunch/serial/security/security.c b/src/libs/kpunch/serial/security/security.c index 76949ec..bc05d99 100644 --- a/src/libs/kpunch/serial/security/security.c +++ b/src/libs/kpunch/serial/security/security.c @@ -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]; - 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++) { + uint32_t u; + uint64_t carry; + uint64_t prod; + uint64_t sum; + // 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. diff --git a/src/libs/kpunch/serial/security/security.h b/src/libs/kpunch/serial/security/security.h index b8169a8..4d9f252 100644 --- a/src/libs/kpunch/serial/security/security.h +++ b/src/libs/kpunch/serial/security/security.h @@ -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 diff --git a/src/libs/kpunch/serial/serial.dhs b/src/libs/kpunch/serial/serial.dhs index a64dc42..2a2becf 100644 --- a/src/libs/kpunch/serial/serial.dhs +++ b/src/libs/kpunch/serial/serial.dhs @@ -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 diff --git a/src/libs/kpunch/sql/dvxSql.c b/src/libs/kpunch/sql/dvxSql.c index 5ffe968..4c1932c 100644 --- a/src/libs/kpunch/sql/dvxSql.c +++ b/src/libs/kpunch/sql/dvxSql.c @@ -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 } diff --git a/src/libs/kpunch/taskmgr/shellTaskMgr.c b/src/libs/kpunch/taskmgr/shellTaskMgr.c index 995b273..82f21ce 100644 --- a/src/libs/kpunch/taskmgr/shellTaskMgr.c +++ b/src/libs/kpunch/taskmgr/shellTaskMgr.c @@ -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); diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c index 5eccd9f..c397b11 100644 --- a/src/libs/kpunch/texthelp/textHelp.c +++ b/src/libs/kpunch/texthelp/textHelp.c @@ -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,9 +268,18 @@ 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 count = forward ? (searchLen - startPos) : (startPos + 1); + 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) { return false; @@ -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; } } @@ -530,14 +552,29 @@ static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, in int32_t needed = lineCount + 1; 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 newCap = needed + TEXT_EDIT_LINE_CAP_GROWTH; + 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 newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH; + 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); @@ -879,7 +990,8 @@ int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, static void textHelpInit(void) { - sCursorBlinkFn = wgtUpdateCursorBlink; + 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,37 +1343,44 @@ 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]; - int32_t tmpLen = *pLen; - int32_t tmpCursor = CUR_OFF(); - int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 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); - memcpy(tmpBuf, buf, copyLen); - tmpBuf[copyLen] = '\0'; + if (tmpBuf) { + int32_t tmpLen = *pLen; + int32_t tmpCursor = CUR_OFF(); + int32_t copyLen = tmpLen; - int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1; - memcpy(buf, undoBuf, restLen); - buf[restLen] = '\0'; - *pLen = restLen; + memcpy(tmpBuf, buf, copyLen); + tmpBuf[copyLen] = '\0'; - int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; - memcpy(undoBuf, tmpBuf, saveLen); - undoBuf[saveLen] = '\0'; - *pUndoLen = saveLen; - *pUndoCursor = tmpCursor; + int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1; + memcpy(buf, undoBuf, restLen); + buf[restLen] = '\0'; + *pLen = restLen; - int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen; - *pUndoCursor = tmpCursor; - FROM_OFF(restoreOff); - *pDesiredCol = *pCol; - *pSA = -1; - *pSC = -1; + int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; + memcpy(undoBuf, tmpBuf, saveLen); + undoBuf[saveLen] = '\0'; + 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; + *pSA = -1; + *pSC = -1; - if (w->onChange) { - w->onChange(w); + if (w->onChange) { + w->onChange(w); + } + + textEditLineCacheDirty(lc); } - - textEditLineCacheDirty(lc); } ENSURE_VIS(); @@ -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++; } diff --git a/src/libs/kpunch/texthelp/textHelp.h b/src/libs/kpunch/texthelp/textHelp.h index 5f6d2da..4227b9e 100644 --- a/src/libs/kpunch/texthelp/textHelp.h +++ b/src/libs/kpunch/texthelp/textHelp.h @@ -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 // ============================================================ diff --git a/src/loader/loaderMain.c b/src/loader/loaderMain.c index ea9d954..1103cd0 100644 --- a/src/loader/loaderMain.c +++ b/src/loader/loaderMain.c @@ -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,13 +804,22 @@ 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] == '#') { continue; @@ -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]); } diff --git a/src/tools/Makefile b/src/tools/Makefile index cdafc37..39bd38d 100644 --- a/src/tools/Makefile +++ b/src/tools/Makefile @@ -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) diff --git a/src/tools/bmp2raw.c b/src/tools/bmp2raw.c index 0c8ca93..fe02906 100644 --- a/src/tools/bmp2raw.c +++ b/src/tools/bmp2raw.c @@ -88,11 +88,20 @@ int main(int argc, char **argv) { uint32_t dibSize = hdr[14] | (hdr[15] << 8) | (hdr[16] << 16) | (hdr[17] << 24); uint32_t palOffset = 14 + dibSize; // BMP file header (14) + DIB header + // biClrUsed (DIB header offset 32 = file offset 46): an optimized 8bpp + // BMP can have fewer than 256 entries, so a fixed 256-entry read would + // pull pixel bytes into the palette tail. 0 means "all" (256). + uint32_t clrUsed = hdr[46] | (hdr[47] << 8) | (hdr[48] << 16) | (hdr[49] << 24); + + if (clrUsed == 0 || clrUsed > PALETTE_ENTRIES) { + clrUsed = PALETTE_ENTRIES; + } + fseek(fp, palOffset, SEEK_SET); uint8_t bmpPal[PALETTE_ENTRIES * 4]; - if (fread(bmpPal, 4, PALETTE_ENTRIES, fp) != PALETTE_ENTRIES) { + if (fread(bmpPal, 4, clrUsed, fp) != clrUsed) { fprintf(stderr, "Cannot read palette\n"); fclose(fp); return 1; @@ -101,12 +110,19 @@ int main(int argc, char **argv) { // Convert palette: BMP is BGRA 8-bit, VGA is RGB 6-bit uint8_t vgaPal[PALETTE_ENTRIES * 3]; - for (int i = 0; i < PALETTE_ENTRIES; i++) { + for (uint32_t i = 0; i < clrUsed; i++) { vgaPal[i * 3 + 0] = bmpPal[i * 4 + 2] >> 2; // R vgaPal[i * 3 + 1] = bmpPal[i * 4 + 1] >> 2; // G vgaPal[i * 3 + 2] = bmpPal[i * 4 + 0] >> 2; // B } + // Zero any palette entries the file didn't define. + for (uint32_t i = clrUsed; i < PALETTE_ENTRIES; i++) { + vgaPal[i * 3 + 0] = 0; + vgaPal[i * 3 + 1] = 0; + vgaPal[i * 3 + 2] = 0; + } + // Read pixel data fseek(fp, pixelOffset, SEEK_SET); diff --git a/src/tools/dvxResWrite.h b/src/tools/dvxResWrite.h index 9a08773..41c6dd0 100644 --- a/src/tools/dvxResWrite.h +++ b/src/tools/dvxResWrite.h @@ -120,6 +120,22 @@ static int dvxResReadExisting(const char *path, long dxeSize, DvxResDirEntryT ** return 0; } + // Bound the directory size read from the footer before allocating: an + // entryCount that can't fit in the file would wrap the 32-bit multiply + // below and overrun the allocation. Treat a corrupt tail as 'no + // existing resources' (return 0) rather than aborting the append. + if (footer.entryCount > (uint32_t)((fileSize - (long)sizeof(DvxResFooterT)) / (long)sizeof(DvxResDirEntryT))) { + fclose(f); + return 0; + } + + long maxDirOffset = fileSize - (long)sizeof(DvxResFooterT); + + if (maxDirOffset < 0 || footer.dirOffset > (uint32_t)maxDirOffset) { + fclose(f); + return 0; + } + fseek(f, footer.dirOffset, SEEK_SET); DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); @@ -185,6 +201,14 @@ static int dvxResReadExisting(const char *path, long dxeSize, DvxResDirEntryT ** // Truncates the file to dxeSize and writes the resource block. static int dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) { + // Write the rebuilt file to a sibling temp file and atomically swap it + // in only after every write succeeds. Rewriting the DXE in place would + // truncate the original; a disk-full or power loss mid-write would then + // leave a corrupt, unrecoverable .app/.wgt. + char tmpPath[DVX_MAX_PATH]; + + snprintf(tmpPath, sizeof(tmpPath), "%s.tmp", path); + FILE *f = fopen(path, "rb"); if (!f) { @@ -206,32 +230,67 @@ static int dvxResWriteBlock(const char *path, long dxeSize, DvxResDirEntryT *ent fclose(f); - f = fopen(path, "wb"); + f = fopen(tmpPath, "wb"); if (!f) { free(dxeBuf); return -1; } - fwrite(dxeBuf, 1, dxeSize, f); + if (fwrite(dxeBuf, 1, dxeSize, f) != (size_t)dxeSize) { + free(dxeBuf); + fclose(f); + remove(tmpPath); + return -1; + } + free(dxeBuf); for (uint32_t i = 0; i < count; i++) { entries[i].offset = (uint32_t)ftell(f); - fwrite(data[i], 1, entries[i].size, f); + + if (fwrite(data[i], 1, entries[i].size, f) != entries[i].size) { + fclose(f); + remove(tmpPath); + return -1; + } } uint32_t dirOffset = (uint32_t)ftell(f); - fwrite(entries, sizeof(DvxResDirEntryT), count, f); + + if (fwrite(entries, sizeof(DvxResDirEntryT), count, f) != count) { + fclose(f); + remove(tmpPath); + return -1; + } DvxResFooterT footer; + footer.magic = DVX_RES_MAGIC; footer.dirOffset = dirOffset; footer.entryCount = count; footer.reserved = 0; - fwrite(&footer, sizeof(footer), 1, f); - fclose(f); + if (fwrite(&footer, sizeof(footer), 1, f) != 1) { + fclose(f); + remove(tmpPath); + return -1; + } + + if (fclose(f) != 0) { + remove(tmpPath); + return -1; + } + + // DOS rename cannot overwrite an existing file, so the original must be + // removed first. The original survives every error path above. + remove(path); + + if (rename(tmpPath, path) != 0) { + remove(tmpPath); + return -1; + } + return 0; } @@ -259,7 +318,13 @@ static int dvxResAppendEntry(const char *path, const char *name, uint32_t type, 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); + + // A read error has already freed any partial allocations and reset the + // outputs; bail rather than truncate the file to only the new entry. + if (readResult < 0) { + return -1; + } // Remove existing entry with same name for (uint32_t i = 0; i < count; i++) { @@ -312,6 +377,17 @@ static int dvxResAppendEntry(const char *path, const char *name, uint32_t type, entries[count].type = type; entries[count].size = resSize; data[count] = (uint8_t *)malloc(resSize); + + if (!data[count]) { + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return -1; + } + memcpy(data[count], resData, resSize); count++; diff --git a/src/tools/dvxhlpc.c b/src/tools/dvxhlpc.c index 1394b6b..2157ccd 100644 --- a/src/tools/dvxhlpc.c +++ b/src/tools/dvxhlpc.c @@ -26,11 +26,11 @@ // the .hlp binary format consumed by the DVX help viewer. // // Usage: -// dvxhlpc -o output.hlp [-w 76] [-i imagedir] input1.dvxhelp [...] +// dvxhlpc -o output.hlp [-i imagedir] [--html out.html] [--quiet] input.dhs [@filelist] [...] // // Five-pass algorithm: // 1. Parse source files, build topic/TOC/index/image arrays -// 2. Word-wrap text and list-item records +// 2. Join paragraph continuation lines (wrapping is done by the viewer) // 3. Build deduplicated string table // 4. Generate trigram search index // 5. Serialize binary .hlp file @@ -56,10 +56,12 @@ // --------------------------------------------------------------------------- #define MAX_LINE_LEN 1024 +#define MAX_PARA_LEN (MAX_LINE_LEN * 64) #define MAX_INCLUDE_DEPTH 16 #define MAX_SECTIONS 64 #define INITIAL_STRTAB_SIZE 65536 #define INITIAL_BUF_SIZE 65536 +#define MAX_INPUT_FILES 256 // cap on input files (incl. @filelist expansion) // --------------------------------------------------------------------------- // Compiler data structures @@ -100,9 +102,10 @@ typedef struct { int32_t fileSize; } ImageRefT; -// String table entry for deduplication +// String table entry for deduplication. Only the offset into strTab is +// stored, never an absolute pointer: strTab is grown with realloc and can +// move, which would dangle any cached pointer. typedef struct { - char *str; int32_t offset; } StrEntryT; @@ -185,6 +188,7 @@ static RecordT *addRecord(TopicT *topic, uint8_t type, uint8_t flags, const c static void addTocEntry(const char *title, int32_t topicIdx, int32_t depth); static TopicT *addTopic(const char *id); static void addTrigram(uint8_t a, uint8_t b, uint8_t c, uint16_t topicIdx); +static void appendPara(char *para, int32_t *paraLen, char sep, const char *src, int32_t srcLen); static void base64Encode(FILE *f, const uint8_t *data, int32_t len); static void bufAppend(BufT *buf, const void *data, int32_t len); static void bufInit(BufT *buf); @@ -263,6 +267,7 @@ static RecordT *addRecord(TopicT *topic, uint8_t type, uint8_t flags, const char r->dataLen = dataLen; if (data && dataLen > 0) { r->data = malloc(dataLen + 1); + if (!r->data) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } memcpy(r->data, data, dataLen); r->data[dataLen] = '\0'; } else { @@ -331,24 +336,50 @@ static void addTrigram(uint8_t a, uint8_t b, uint8_t c, uint16_t topicIdx) { tri->postingCap = 8; tri->postingCount = 0; tri->postings = malloc(sizeof(uint16_t) * tri->postingCap); + if (!tri->postings) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } } - // Check if topic already in posting list - for (int32_t i = 0; i < tri->postingCount; i++) { - if (tri->postings[i] == topicIdx) { - return; - } + // Topics are processed in ascending order (buildSearchIndex), so a repeat + // of this topic can only be the most recently appended posting. + if (tri->postingCount > 0 && tri->postings[tri->postingCount - 1] == topicIdx) { + return; } // Add to posting list if (tri->postingCount >= tri->postingCap) { tri->postingCap *= 2; tri->postings = realloc(tri->postings, sizeof(uint16_t) * tri->postingCap); + if (!tri->postings) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } } tri->postings[tri->postingCount++] = topicIdx; } +// Append src (optionally preceded by a separator) to the paragraph buffer, +// bounded to MAX_PARA_LEN so a runaway paragraph can't overflow the stack +// buffer. Over-long input is truncated with a diagnostic. +static void appendPara(char *para, int32_t *paraLen, char sep, const char *src, int32_t srcLen) { + if (sep != '\0' && *paraLen > 0 && *paraLen < MAX_PARA_LEN - 1) { + para[(*paraLen)++] = sep; + } + + int32_t avail = MAX_PARA_LEN - 1 - *paraLen; + + if (avail < 0) { + avail = 0; + } + + if (srcLen > avail) { + emitError("paragraph exceeds %d bytes; truncated", MAX_PARA_LEN); + srcLen = avail; + } + + memcpy(para + *paraLen, src, srcLen); + *paraLen += srcLen; + para[*paraLen] = '\0'; +} + + static void base64Encode(FILE *f, const uint8_t *data, int32_t len) { for (int32_t i = 0; i < len; i += 3) { uint32_t b = (uint32_t)data[i] << 16; @@ -644,7 +675,7 @@ int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *out bool inCode = false; bool inNote = false; uint8_t noteFlags = 0; - char para[MAX_LINE_LEN * 64]; + char para[MAX_PARA_LEN]; int32_t paraLen = 0; hlpcInfo("Pass 1: Parsing %d input file(s)...\n", inputCount); @@ -1007,8 +1038,24 @@ static void parseDirective(const char *line, TopicT **curTopic, bool *inList, bo emitError(".include requires a filename"); return; } - // Flush pending paragraph - flushParagraph(*curTopic, para, *paraLen, *inCode ? HLP_REC_CODE : (*inTable ? HLP_REC_TABLE : HLP_REC_TEXT), *inNote ? *noteFlags : 0); + // Flush pending paragraph using the same type/flags selection as the + // other flush sites, so a .include inside a .note/.list block doesn't + // emit the pending text as plain TEXT. + uint8_t flushType = HLP_REC_TEXT; + uint8_t flushFlags = 0; + + if (*inCode) { + flushType = HLP_REC_CODE; + } else if (*inTable) { + flushType = HLP_REC_TABLE; + } else if (*inNote) { + flushType = HLP_REC_NOTE; + flushFlags = *noteFlags; + } else if (*inList) { + flushType = HLP_REC_LIST_ITEM; + } + + flushParagraph(*curTopic, para, *paraLen, flushType, flushFlags); *paraLen = 0; // Build path relative to current file's directory @@ -1167,9 +1214,7 @@ static void parseDirective(const char *line, TopicT **curTopic, bool *inList, bo *paraLen = 0; // Start new item with the rest text if (*rest) { - int32_t len = strlen(rest); - memcpy(para, rest, len); - *paraLen = len; + appendPara(para, paraLen, '\0', rest, (int32_t)strlen(rest)); } } else if (strcmp(directive, "endlist") == 0) { @@ -1282,12 +1327,7 @@ static void parseFile(const char *path, TopicT **curTopic, bool *inList, bool *i if (*inCode || *inTable) { // In code/table blocks, preserve lines verbatim - if (*paraLen > 0) { - para[(*paraLen)++] = '\n'; - } - memcpy(para + *paraLen, line, len); - *paraLen += len; - para[*paraLen] = '\0'; + appendPara(para, paraLen, '\n', line, len); } else if (*inList) { // In list, accumulate continuation text for current item if (len == 0) { @@ -1295,12 +1335,7 @@ static void parseFile(const char *path, TopicT **curTopic, bool *inList, bool *i flushParagraph(*curTopic, para, *paraLen, HLP_REC_LIST_ITEM, 0); *paraLen = 0; } else { - if (*paraLen > 0) { - para[(*paraLen)++] = ' '; - } - memcpy(para + *paraLen, line, len); - *paraLen += len; - para[*paraLen] = '\0'; + appendPara(para, paraLen, ' ', line, len); } } else if (*inNote) { // In note block @@ -1309,12 +1344,7 @@ static void parseFile(const char *path, TopicT **curTopic, bool *inList, bool *i flushParagraph(*curTopic, para, *paraLen, HLP_REC_NOTE, *noteFlags); *paraLen = 0; } else { - if (*paraLen > 0) { - para[(*paraLen)++] = ' '; - } - memcpy(para + *paraLen, line, len); - *paraLen += len; - para[*paraLen] = '\0'; + appendPara(para, paraLen, ' ', line, len); } } else { // Normal text @@ -1323,12 +1353,7 @@ static void parseFile(const char *path, TopicT **curTopic, bool *inList, bool *i flushParagraph(*curTopic, para, *paraLen, HLP_REC_TEXT, 0); *paraLen = 0; } else { - if (*paraLen > 0) { - para[(*paraLen)++] = ' '; - } - memcpy(para + *paraLen, line, len); - *paraLen += len; - para[*paraLen] = '\0'; + appendPara(para, paraLen, ' ', line, len); } } } @@ -1456,6 +1481,8 @@ static int pass5Serialize(const char *outputPath) { uint32_t *topicContentOffsets = calloc((size_t)allocCount, sizeof(uint32_t)); uint32_t *topicContentSizes = calloc((size_t)allocCount, sizeof(uint32_t)); + if (!topicContentOffsets || !topicContentSizes) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } + for (int32_t t = 0; t < topicCount; t++) { TopicT *topic = &topics[t]; topicContentOffsets[t] = offset; @@ -1560,17 +1587,22 @@ static int pass5Serialize(const char *outputPath) { entry.trigram[0] = trigrams[i].trigram[0]; entry.trigram[1] = trigrams[i].trigram[1]; entry.trigram[2] = trigrams[i].trigram[2]; - entry.postingCount = (uint8_t)trigrams[i].postingCount; + // The serialized count is a uint8_t; clamp so a very common + // trigram (>255 postings) doesn't wrap the count and desync it + // from the posting data written below. + int32_t pc = trigrams[i].postingCount > UINT8_MAX ? UINT8_MAX : trigrams[i].postingCount; + entry.postingCount = (uint8_t)pc; entry.postingOffset = postingBase + postingOff; fwrite(&entry, sizeof(entry), 1, f); offset += sizeof(entry); - postingOff += sizeof(uint16_t) * trigrams[i].postingCount; + postingOff += sizeof(uint16_t) * pc; } - // Write posting lists + // Write posting lists (clamped to match the uint8_t count above). for (int32_t i = 0; i < trigramCount; i++) { - fwrite(trigrams[i].postings, sizeof(uint16_t), trigrams[i].postingCount, f); - offset += sizeof(uint16_t) * trigrams[i].postingCount; + int32_t pc = trigrams[i].postingCount > UINT8_MAX ? UINT8_MAX : trigrams[i].postingCount; + fwrite(trigrams[i].postings, sizeof(uint16_t), pc, f); + offset += sizeof(uint16_t) * pc; } } hdr.searchSize = offset - hdr.searchOffset; @@ -1584,6 +1616,7 @@ static int pass5Serialize(const char *outputPath) { // --- 7. Topic directory (sorted by topic ID) --- hdr.topicDirOffset = offset; HlpTopicDirT *topicDir = calloc(topicCount, sizeof(HlpTopicDirT)); + if (!topicDir) { fprintf(stderr, "fatal: out of memory\n"); exit(1); } for (int32_t i = 0; i < topicCount; i++) { topicDir[i].topicIdStr = strTableFind(topics[i].id); topicDir[i].titleStr = strTableFind(topics[i].title); @@ -1770,7 +1803,6 @@ static int32_t strTableAdd(const char *str) { memcpy(strTab + strTabSize, str, len + 1); strTabSize += len + 1; - strEntries[strEntryCount].str = strTab + offset; strEntries[strEntryCount].offset = offset; strEntryCount++; @@ -1780,7 +1812,9 @@ static int32_t strTableAdd(const char *str) { static int32_t strTableFind(const char *str) { for (int32_t i = 0; i < strEntryCount; i++) { - if (strcmp(strEntries[i].str, str) == 0) { + // Resolve through the current strTab base; a cached pointer would + // dangle after strTab is realloc'd. + if (strcmp(strTab + strEntries[i].offset, str) == 0) { return strEntries[i].offset; } } @@ -1799,7 +1833,7 @@ static void usage(void) { #ifndef HLPC_NO_MAIN int main(int argc, char **argv) { const char *outputPath = NULL; - char *inputFiles[256]; + char *inputFiles[MAX_INPUT_FILES]; int32_t inputCount = 0; // Parse command-line arguments @@ -1835,7 +1869,7 @@ int main(int argc, char **argv) { } #define RESP_LINE_MAX 260 - static char respLines[256][RESP_LINE_MAX]; + static char respLines[MAX_INPUT_FILES][RESP_LINE_MAX]; char rline[RESP_LINE_MAX]; while (fgets(rline, (int)sizeof(rline), rf)) { @@ -1849,7 +1883,7 @@ int main(int argc, char **argv) { continue; } - if (inputCount >= 256) { + if (inputCount >= MAX_INPUT_FILES) { fprintf(stderr, "error: too many input files\n"); fclose(rf); return 1; @@ -1862,7 +1896,7 @@ int main(int argc, char **argv) { fclose(rf); } else { - if (inputCount >= 256) { + if (inputCount >= MAX_INPUT_FILES) { fprintf(stderr, "error: too many input files\n"); return 1; } diff --git a/src/tools/dvxres.c b/src/tools/dvxres.c index d3d8023..34803de 100644 --- a/src/tools/dvxres.c +++ b/src/tools/dvxres.c @@ -429,6 +429,12 @@ static int cmdStrip(const char *dxePath) { // Read DXE content f = fopen(dxePath, "rb"); + + if (!f) { + fprintf(stderr, "Cannot open: %s\n", dxePath); + return 1; + } + uint8_t *buf = (uint8_t *)malloc(dxeSize); if (!buf) { diff --git a/src/tools/mktbicon.c b/src/tools/mktbicon.c index 857b1de..7a8cd39 100644 --- a/src/tools/mktbicon.c +++ b/src/tools/mktbicon.c @@ -34,6 +34,11 @@ #define ICON_W 16 #define ICON_H 16 +typedef struct { + const char *name; + void (*fn)(void); +} IconT; + static BmpCanvasT canvas; // ============================================================ @@ -53,6 +58,23 @@ static void iconStepOver(void); static void iconStop(void); +// Single source of truth for type name to draw function mapping. +static const IconT icons[] = { + {"open", iconOpen}, + {"save", iconSave}, + {"run", iconRun}, + {"stop", iconStop}, + {"code", iconCode}, + {"design", iconDesign}, + {"debug", iconDebug}, + {"stepinto", iconStepInto}, + {"stepover", iconStepOver}, + {"stepout", iconStepOut}, + {"runtocur", iconRunToCursor}, + {NULL, NULL} +}; + + // Code: text/code icon (page with angle brackets) static void iconCode(void) { bmpClear(&canvas, 192, 192, 192); @@ -258,7 +280,13 @@ static void iconStop(void) { int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "Usage: mktbicon \n"); - fprintf(stderr, "Types: open, save, run, stop, code, design\n"); + fprintf(stderr, "Types:"); + + for (int i = 0; icons[i].name; i++) { + fprintf(stderr, " %s", icons[i].name); + } + + fprintf(stderr, "\n"); return 1; } @@ -270,25 +298,17 @@ int main(int argc, char **argv) { return 1; } - if (strcmp(type, "open") == 0) { iconOpen(); } - else if (strcmp(type, "save") == 0) { iconSave(); } - else if (strcmp(type, "run") == 0) { iconRun(); } - else if (strcmp(type, "stop") == 0) { iconStop(); } - else if (strcmp(type, "code") == 0) { iconCode(); } - else if (strcmp(type, "design") == 0) { iconDesign(); } - else if (strcmp(type, "debug") == 0) { iconDebug(); } - else if (strcmp(type, "stepinto") == 0) { iconStepInto(); } - else if (strcmp(type, "stepover") == 0) { iconStepOver(); } - else if (strcmp(type, "stepout") == 0) { iconStepOut(); } - else if (strcmp(type, "runtocur") == 0) { iconRunToCursor(); } - else { - fprintf(stderr, "Unknown icon type: %s\n", type); - bmpFree(&canvas); - return 1; + for (int i = 0; icons[i].name; i++) { + if (strcmp(type, icons[i].name) == 0) { + icons[i].fn(); + bmpWrite(&canvas, path); + bmpFree(&canvas); + printf("Generated %s (%s)\n", path, type); + return 0; + } } - bmpWrite(&canvas, path); + fprintf(stderr, "Unknown icon type: %s\n", type); bmpFree(&canvas); - printf("Generated %s (%s)\n", path, type); - return 0; + return 1; } diff --git a/src/tools/proxy/Makefile b/src/tools/proxy/Makefile index 9ee7f14..6611216 100644 --- a/src/tools/proxy/Makefile +++ b/src/tools/proxy/Makefile @@ -49,16 +49,17 @@ $(OBJDIR)/sockShim.o: sockShim.c sockShim.h | $(OBJDIR) $(OBJDIR)/proxy.o: proxy.c sockShim.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -# Packet layer -- block real rs232.h, inject socket shim -$(OBJDIR)/packet.o: ../packet/packet.c sockShim.h | $(OBJDIR) +# Packet layer -- block real rs232.h, inject socket shim. Sources live +# under the shared serial library tree (moved there in the layout reorg). +$(OBJDIR)/packet.o: ../../libs/kpunch/serial/packet/packet.c sockShim.h | $(OBJDIR) $(CC) $(CFLAGS) -I. -Istubs/ -include sockShim.h -c -o $@ $< # Security layer -- stub DOS-specific headers -$(OBJDIR)/security.o: ../security/security.c | $(OBJDIR) +$(OBJDIR)/security.o: ../../libs/kpunch/serial/security/security.c | $(OBJDIR) $(CC) $(CFLAGS) -Istubs/ -c -o $@ $< # SecLink layer -- block real rs232.h, inject socket shim -$(OBJDIR)/secLink.o: ../seclink/secLink.c sockShim.h | $(OBJDIR) +$(OBJDIR)/secLink.o: ../../libs/kpunch/serial/seclink/secLink.c sockShim.h | $(OBJDIR) $(CC) $(CFLAGS) -I. -include sockShim.h -c -o $@ $< $(OBJDIR): diff --git a/src/tools/proxy/proxy.c b/src/tools/proxy/proxy.c index d3d1d53..0f17e16 100644 --- a/src/tools/proxy/proxy.c +++ b/src/tools/proxy/proxy.c @@ -58,8 +58,8 @@ #include #include "sockShim.h" -#include "../seclink/secLink.h" -#include "../security/security.h" +#include "../../libs/kpunch/serial/seclink/secLink.h" +#include "../../libs/kpunch/serial/security/security.h" // ======================================================================== @@ -213,6 +213,12 @@ static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t chann return; } + // The BBS socket may not be connected yet (bbsFd still -1); drop these + // bytes rather than writing to an invalid fd. + if (bbsFd < 0) { + return; + } + int sent = 0; while (sent < len) { ssize_t n = write(bbsFd, data + sent, len - sent); @@ -381,7 +387,7 @@ int main(int argc, char *argv[]) { int bbsPort = DEFAULT_BBS_PORT; int listenFd; int clientFd; - int bbsFd; + int bbsFd = -1; SecLinkT *link; struct sockaddr_in clientAddr; socklen_t clientLen; @@ -430,6 +436,14 @@ int main(int argc, char *argv[]) { break; } } + + // A Ctrl+C during the wait clears sRunning; don't fall into a blocking + // accept() that would then require a second signal to break out of. + if (!sRunning) { + close(listenFd); + return 0; + } + clientLen = sizeof(clientAddr); clientFd = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen); close(listenFd); @@ -495,9 +509,10 @@ int main(int argc, char *argv[]) { } printf("BBS connected. Proxying traffic.\n"); - // Set BBS socket non-blocking for the main loop - int flags = fcntl(bbsFd, F_GETFL, 0); - fcntl(bbsFd, F_SETFL, flags | O_NONBLOCK); + // Keep the BBS socket blocking. Reads are gated by poll(POLLIN), so a + // blocking fd never stalls the loop, and blocking writes guarantee the + // accumulating write loop in onRecvFromDos delivers the full payload + // under send-buffer back-pressure instead of silently truncating it. // Main proxy loop: poll both sockets with a short timeout so secLinkPoll // runs frequently enough for ACK processing and retransmits. Data flows: @@ -517,8 +532,8 @@ int main(int argc, char *argv[]) { // Read from BBS, filter telnet, send clean data to the DOS side if (fds[1].revents & POLLIN) { - uint8_t raw[64]; - uint8_t clean[64]; + uint8_t raw[SECLINK_MAX_PAYLOAD]; + uint8_t clean[SECLINK_MAX_PAYLOAD]; ssize_t n = read(bbsFd, raw, sizeof(raw)); if (n <= 0) { printf("BBS disconnected.\n"); @@ -530,7 +545,14 @@ int main(int argc, char *argv[]) { // Retry with ACK processing if the send window is full int rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false); while (rc != SECLINK_SUCCESS && sRunning) { - secLinkPoll(link); + // Bail if the DOS client dropped, otherwise this spins + // forever waiting for a window that never opens. + if (secLinkPoll(link) == PKT_ERR_DISCONNECTED) { + printf("DOS client disconnected.\n"); + sRunning = false; + break; + } + usleep(10000); rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false); } diff --git a/src/tools/proxy/sockShim.c b/src/tools/proxy/sockShim.c index 17d22b7..18aeb0b 100644 --- a/src/tools/proxy/sockShim.c +++ b/src/tools/proxy/sockShim.c @@ -38,6 +38,7 @@ #include #include #include +#include // ======================================================================== @@ -121,6 +122,18 @@ int rs232Write(int com, const char *data, int len) { if (errno == EINTR) { continue; } + + // Defensive: if the socket is non-blocking for any reason, + // wait for it to drain rather than returning after a partial + // write (which would put a truncated frame on the wire). + if (errno == EAGAIN || errno == EWOULDBLOCK) { + struct pollfd pfd; + pfd.fd = sFds[com]; + pfd.events = POLLOUT; + poll(&pfd, 1, -1); + continue; + } + return RS232_ERR_NOT_OPEN; } sent += (int)n; @@ -137,7 +150,15 @@ void sockShimSetFd(int com, int fd) { sFds[com] = fd; - // Set non-blocking so rs232Read returns immediately when empty + // Keep the socket BLOCKING. rs232Read already does a per-call + // non-blocking recv (MSG_DONTWAIT), so it needs no socket-wide flag; + // a non-blocking socket would instead make rs232Write's send() return + // EAGAIN when the TCP buffer fills, truncating an HDLC frame (the + // caller in packet.c ignores the return). Blocking matches the real + // rs232Write's busy-wait-until-complete contract. int flags = fcntl(fd, F_GETFL, 0); - fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + if (flags >= 0) { + fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); + } } diff --git a/src/tools/proxy/sockShim.h b/src/tools/proxy/sockShim.h index f9d447c..74934c7 100644 --- a/src/tools/proxy/sockShim.h +++ b/src/tools/proxy/sockShim.h @@ -44,6 +44,7 @@ #include // rs232-compatible constants (subset used by packet and secLink) +#define RS232_NUM_PORTS 4 #define RS232_COM1 0 #define RS232_COM2 1 #define RS232_COM3 2 diff --git a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c index b5f1bb9..d5a78cd 100644 --- a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c +++ b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c @@ -65,6 +65,10 @@ static int32_t sTypeId = -1; #define ANSI_BORDER 2 #define ANSI_MAX_PARAMS 8 +// Upper bound for an accumulated CSI parameter value. Caps the +// digit accumulator so a hostile sequence like ESC[999999999S can +// neither overflow the int nor drive a billion-iteration scroll loop. +#define ANSI_MAX_PARAM_VALUE 9999 #define ANSI_SB_W 14 #define ANSI_DEFAULT_SCROLLBACK 500 #define PARSE_NORMAL 0 @@ -86,18 +90,12 @@ static int32_t sTypeId = -1; #define ANSI_CLIPBOARD_BUF 4096 typedef struct { + // Fields are ordered by alignment to minimize struct padding. uint8_t *cells; int32_t cols; int32_t rows; int32_t cursorRow; int32_t cursorCol; - bool cursorVisible; - bool wrapMode; - bool bold; - bool originMode; - bool csiPrivate; - uint8_t curAttr; - uint8_t parseState; int32_t params[8]; int32_t paramCount; int32_t savedRow; @@ -109,24 +107,30 @@ typedef struct { int32_t scrollbackCount; int32_t scrollbackHead; int32_t scrollPos; - bool blinkVisible; clock_t blinkTime; - bool cursorOn; clock_t cursorTime; uint32_t dirtyRows; int32_t lastCursorRow; int32_t lastCursorCol; uint32_t packedPalette[16]; - bool paletteValid; - bool hasBlinkCells; // tracked to skip the per-blink O(rows*cols) scan int32_t selStartLine; int32_t selStartCol; int32_t selEndLine; int32_t selEndCol; - bool selecting; void *commCtx; int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); + bool cursorVisible; + bool wrapMode; + bool bold; + bool originMode; + bool csiPrivate; + uint8_t curAttr; + uint8_t parseState; + bool blinkVisible; + bool cursorOn; + bool paletteValid; + bool hasBlinkCells; // tracked to skip the per-blink O(rows*cols) scan } AnsiTermDataT; static const uint8_t sCgaPalette[16][3] = { @@ -217,7 +221,6 @@ static void ansiTermClearSelection(WidgetT *w) { at->selStartCol = -1; at->selEndLine = -1; at->selEndCol = -1; - at->selecting = false; } @@ -229,7 +232,7 @@ static void ansiTermCopySelection(WidgetT *w) { int32_t cols = at->cols; char buf[ANSI_CLIPBOARD_BUF]; int32_t pos = 0; - for (int32_t line = sLine; line <= eLine && pos < 4095; line++) { + for (int32_t line = sLine; line <= eLine && pos < (int32_t)sizeof(buf) - 1; line++) { const uint8_t *lineData = ansiTermGetLine(w, line); int32_t colStart = (line == sLine) ? sCol : 0; int32_t colEnd = (line == eLine) ? eCol : cols; @@ -237,10 +240,10 @@ static void ansiTermCopySelection(WidgetT *w) { for (int32_t c = colStart; c < colEnd; c++) { if (lineData[c * 2] != ' ') { lastNonSpace = c; } } - for (int32_t c = colStart; c <= lastNonSpace && pos < 4095; c++) { + for (int32_t c = colStart; c <= lastNonSpace && pos < (int32_t)sizeof(buf) - 1; c++) { buf[pos++] = (char)lineData[c * 2]; } - if (line < eLine && pos < 4095) { buf[pos++] = '\n'; } + if (line < eLine && pos < (int32_t)sizeof(buf) - 1) { buf[pos++] = '\n'; } } buf[pos] = '\0'; if (pos > 0) { clipboardCopy(buf, pos); } @@ -267,7 +270,7 @@ static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; int32_t startRow = startCell / at->cols; int32_t endRow = (startCell + count - 1) / at->cols; - for (int32_t r = startRow; r <= endRow && r < 32; r++) { + for (int32_t r = startRow; r <= endRow && r < ANSI_MAX_DIRTY_ROWS; r++) { at->dirtyRows |= (1U << r); } } @@ -343,12 +346,13 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'c': { if (at->commWrite) { const uint8_t reply[] = "\033[?1;2c"; - at->commWrite(at->commCtx, reply, 7); + at->commWrite(at->commCtx, reply, sizeof(reply) - 1); } break; } case 'E': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + if (count > at->rows) { count = at->rows; } for (int32_t i = 0; i < count; i++) { ansiTermNewline(w); } at->cursorCol = 0; break; @@ -386,11 +390,13 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { } case 'S': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + if (count > at->rows) { count = at->rows; } // more clears the screen anyway for (int32_t i = 0; i < count; i++) { ansiTermScrollUp(w); } break; } case 'T': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + if (count > at->rows) { count = at->rows; } for (int32_t i = 0; i < count; i++) { ansiTermScrollDown(w); } break; } @@ -601,9 +607,21 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { else if (ch >= '0' && ch <= '9') { if (at->paramCount == 0) { at->paramCount = 1; } int32_t idx = at->paramCount - 1; - if (idx < ANSI_MAX_PARAMS) { at->params[idx] = at->params[idx] * 10 + (ch - '0'); } + if (idx < ANSI_MAX_PARAMS) { + at->params[idx] = at->params[idx] * 10 + (ch - '0'); + if (at->params[idx] > ANSI_MAX_PARAM_VALUE) { at->params[idx] = ANSI_MAX_PARAM_VALUE; } + } + } + else if (ch == ';') { + // A leading ';' means an omitted (default) first parameter, + // so reserve slot 0 as well as the slot for what follows; + // otherwise the next digit overwrites params[0] and every + // later parameter is shifted one slot left (e.g. ESC[;5H). + // params[] was zeroed on entering PARSE_CSI, so slot 0 reads + // as the default value 0. + if (at->paramCount == 0) { at->paramCount = 2; } + else if (at->paramCount < ANSI_MAX_PARAMS) { at->paramCount++; } } - else if (ch == ';') { if (at->paramCount < ANSI_MAX_PARAMS) { at->paramCount++; } } else if (ch >= 0x40 && ch <= 0x7E) { ansiTermDispatchCsi(w, ch); at->parseState = PARSE_NORMAL; } else { at->parseState = PARSE_NORMAL; } break; @@ -701,15 +719,19 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { if (rows <= 0) { rows = 25; } WidgetT *w = widgetAlloc(parent, sTypeId); if (!w) { return NULL; } + // Use wgtDestroy (not bare free) on every failure path: widgetAlloc + // already linked w into the parent's child list and the poll list, so + // freeing it directly leaves dangling pointers there. The destroy + // method is NULL-safe and frees any partially-allocated data. AnsiTermDataT *at = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT)); - if (!at) { free(w); return NULL; } + if (!at) { wgtDestroy(w); return NULL; } w->data = at; int32_t cellCount = cols * rows; at->cells = (uint8_t *)malloc(cellCount * 2); - if (!at->cells) { free(at); free(w); return NULL; } + if (!at->cells) { wgtDestroy(w); return NULL; } int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; at->scrollback = (uint8_t *)malloc(sbMax * cols * 2); - if (!at->scrollback) { free(at->cells); free(at); free(w); return NULL; } + if (!at->scrollback) { wgtDestroy(w); return NULL; } at->cols = cols; at->rows = rows; at->cursorVisible = true; at->wrapMode = true; at->curAttr = ANSI_DEFAULT_ATTR; at->parseState = PARSE_NORMAL; @@ -736,6 +758,9 @@ void wgtAnsiTermClear(WidgetT *w) { for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } at->cursorRow = 0; at->cursorCol = 0; at->curAttr = ANSI_DEFAULT_ATTR; at->bold = false; at->parseState = PARSE_NORMAL; + // Mark every row dirty or the row-skipping paint leaves stale cells + // until each happens to be re-dirtied by later output. + at->dirtyRows = ANSI_DIRTY_ALL_ROWS; } @@ -806,8 +831,8 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { int32_t prevRow = at->lastCursorRow; int32_t curRow = at->cursorRow; if (prevRow != curRow || at->lastCursorCol != at->cursorCol) { - if (prevRow >= 0 && prevRow < at->rows) { at->dirtyRows |= (1U << prevRow); } - if (curRow >= 0 && curRow < at->rows) { at->dirtyRows |= (1U << curRow); } + if (prevRow >= 0 && prevRow < ANSI_MAX_DIRTY_ROWS) { at->dirtyRows |= (1U << prevRow); } + if (curRow >= 0 && curRow < ANSI_MAX_DIRTY_ROWS) { at->dirtyRows |= (1U << curRow); } } uint32_t dirty = at->dirtyRows; if (dirty == 0) { return 0; } @@ -828,7 +853,10 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { bool viewingLive = (at->scrollPos == at->scrollbackCount); int32_t repainted = 0; int32_t minRow = rows; int32_t maxRow = -1; for (int32_t row = 0; row < rows; row++) { - if (!(dirty & (1U << row))) { continue; } + // Rows >= 32 carry no dirty bit (dirtyRows is a 32-bit mask), so + // force-repaint them whenever the function runs; the dirty==0 guard + // above still short-circuits the fully-idle case. + if (row < ANSI_MAX_DIRTY_ROWS && !(dirty & (1U << row))) { continue; } int32_t lineIndex = at->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); int32_t curCol2 = -1; @@ -896,7 +924,6 @@ static bool widgetAnsiTermClearSelection(WidgetT *w) { at->selStartCol = -1; at->selEndLine = -1; at->selEndCol = -1; - at->selecting = false; at->dirtyRows = ANSI_DIRTY_ALL_ROWS; return true; } @@ -947,10 +974,10 @@ static void widgetAnsiTermOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, in void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { AnsiTermDataT *at = (AnsiTermDataT *)w->data; - if (key == 0x03 && (mod & KEY_MOD_CTRL)) { + if (key == KEY_CTRL_C && (mod & KEY_MOD_CTRL)) { if (ansiTermHasSelection(w)) { ansiTermCopySelection(w); ansiTermClearSelection(w); wgtInvalidatePaint(w); return; } } - if (key == 0x16 && (mod & KEY_MOD_CTRL)) { ansiTermPasteToComm(w); wgtInvalidatePaint(w); return; } + if (key == KEY_CTRL_V && (mod & KEY_MOD_CTRL)) { ansiTermPasteToComm(w); wgtInvalidatePaint(w); return; } if (!at->commWrite) { return; } if (ansiTermHasSelection(w)) { ansiTermClearSelection(w); } uint8_t buf[8]; @@ -1002,7 +1029,7 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) if (clicks >= 3) { at->selStartLine = lineIndex; at->selStartCol = 0; at->selEndLine = lineIndex; at->selEndCol = cols; - at->selecting = false; sDragWidget = NULL; + sDragWidget = NULL; } else if (clicks == 2) { const uint8_t *lineData = ansiTermGetLine(hit, lineIndex); int32_t ws = clickCol; int32_t we = clickCol; @@ -1010,11 +1037,11 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) while (we < cols && isWordChar((char)lineData[we * 2])) { we++; } at->selStartLine = lineIndex; at->selStartCol = ws; at->selEndLine = lineIndex; at->selEndCol = we; - at->selecting = false; sDragWidget = NULL; + sDragWidget = NULL; } else { at->selStartLine = lineIndex; at->selStartCol = clickCol; at->selEndLine = lineIndex; at->selEndCol = clickCol; - at->selecting = true; sDragWidget = hit; + sDragWidget = hit; } at->dirtyRows = ANSI_DIRTY_ALL_ROWS; return; @@ -1039,6 +1066,9 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) } if (at->scrollPos < 0) { at->scrollPos = 0; } if (at->scrollPos > maxScroll) { at->scrollPos = maxScroll; } + // scrollPos changed every visible row's content; mark all dirty or + // the row-skipping paint shows a stale mixed display. + at->dirtyRows = ANSI_DIRTY_ALL_ROWS; } @@ -1046,7 +1076,7 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit AnsiTermDataT *at = (AnsiTermDataT *)w->data; BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; - bevel.face = 0; bevel.width = ANSI_BORDER; + bevel.face = BEVEL_NO_FILL; bevel.width = ANSI_BORDER; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); ansiTermBuildPalette(w, d); const uint32_t *palette = at->packedPalette; diff --git a/src/widgets/kpunch/box/widgetBox.c b/src/widgets/kpunch/box/widgetBox.c index 4a8b4ef..986da16 100644 --- a/src/widgets/kpunch/box/widgetBox.c +++ b/src/widgets/kpunch/box/widgetBox.c @@ -147,9 +147,9 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap inner.shadow = colors->windowHighlight; } - outer.face = 0; + outer.face = BEVEL_NO_FILL; outer.width = 1; - inner.face = 0; + inner.face = BEVEL_NO_FILL; inner.width = 1; drawBevel(d, ops, w->x, boxY, w->w, boxH, &outer); drawBevel(d, ops, w->x + 1, boxY + 1, w->w - 2, boxH - 2, &inner); diff --git a/src/widgets/kpunch/canvas/widgetCanvas.c b/src/widgets/kpunch/canvas/widgetCanvas.c index 91c74e9..04bfe02 100644 --- a/src/widgets/kpunch/canvas/widgetCanvas.c +++ b/src/widgets/kpunch/canvas/widgetCanvas.c @@ -49,6 +49,7 @@ static int32_t sTypeId = -1; #define CANVAS_BORDER 2 +#define CANVAS_SQRT_ITERATIONS 8 // integer-sqrt Newton iterations for span fill typedef struct { uint8_t *pixelData; // pixel buffer in display format @@ -58,8 +59,6 @@ typedef struct { int32_t canvasBpp; // cached bytes per pixel uint32_t penColor; int32_t penSize; - int32_t lastX; - int32_t lastY; void (*onMouse)(struct WidgetT *w, int32_t cx, int32_t cy, bool drag); } CanvasDataT; @@ -78,6 +77,7 @@ static void basSetPenColor(WidgetT *w, int32_t color); static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color); static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy); static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1); +static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius); static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp); static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp); static uint32_t rgbToPacked(WidgetT *w, uint32_t rgb); @@ -179,62 +179,8 @@ static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy) { return; } - // Filled circle via per-row horizontal span. For each row (dy offset from - // center), compute the horizontal extent using the circle equation - // dx^2 + dy^2 <= r^2. The horizontal half-span is floor(sqrt(r^2 - dy^2)). - // This approach is faster than checking each pixel individually because - // the inner loop just fills a horizontal run -- no per-pixel distance check. - int32_t r2 = rad * rad; - - for (int32_t dy = -rad; dy <= rad; dy++) { - int32_t py = cy + dy; - - if (py < 0 || py >= ch) { - continue; - } - - // Compute horizontal half-span: dx^2 <= r^2 - dy^2 - int32_t dy2 = dy * dy; - int32_t rem = r2 - dy2; - int32_t hspan = 0; - - // Integer sqrt via Newton's method (Babylonian method). 8 iterations - // is more than enough to converge for any radius that fits in int32_t. - // Using integer sqrt avoids pulling in the FPU which may not be - // present on 486SX systems, and avoids the float-to-int conversion - // overhead even on systems with an FPU. - if (rem > 0) { - hspan = rad; - - for (int32_t i = 0; i < 8; i++) { - hspan = (hspan + rem / hspan) / 2; - } - - if (hspan * hspan > rem) { - hspan--; - } - } - - int32_t x0 = cx - hspan; - int32_t x1 = cx + hspan; - - if (x0 < 0) { - x0 = 0; - } - - if (x1 >= cw) { - x1 = cw - 1; - } - - if (x0 <= x1) { - uint8_t *dst = data + py * pitch + x0 * bpp; - - for (int32_t px = x0; px <= x1; px++) { - canvasPutPixel(dst, color, bpp); - dst += bpp; - } - } - } + // Filled circle via per-row horizontal spans (shared helper). + canvasFillCircleSpans(cd, cx, cy, rad); } @@ -277,6 +223,65 @@ static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, } +// Fill a circle of the given radius centered at (cx, cy) using per-row +// horizontal spans. The half-span for each row is floor(sqrt(r^2 - dy^2)), +// computed with integer Newton's method so no FPU is required (486SX-safe). +// Shared by canvasDrawDot (pen dot) and wgtCanvasFillCircle. +static void canvasFillCircleSpans(CanvasDataT *cd, int32_t cx, int32_t cy, int32_t radius) { + int32_t bpp = cd->canvasBpp; + int32_t pitch = cd->canvasPitch; + uint8_t *data = cd->pixelData; + int32_t cw = cd->canvasW; + int32_t ch = cd->canvasH; + uint32_t color = cd->penColor; + int32_t r2 = radius * radius; + + for (int32_t dy = -radius; dy <= radius; dy++) { + int32_t py = cy + dy; + + if (py < 0 || py >= ch) { + continue; + } + + int32_t dy2 = dy * dy; + int32_t rem = r2 - dy2; + int32_t hspan = 0; + + if (rem > 0) { + hspan = radius; + + for (int32_t i = 0; i < CANVAS_SQRT_ITERATIONS; i++) { + hspan = (hspan + rem / hspan) / 2; + } + + if (hspan * hspan > rem) { + hspan--; + } + } + + int32_t x0 = cx - hspan; + int32_t x1 = cx + hspan; + + if (x0 < 0) { + x0 = 0; + } + + if (x1 >= cw) { + x1 = cw - 1; + } + + if (x0 <= x1) { + uint8_t *dst = data + py * pitch + x0 * bpp; + + for (int32_t px = x0; px <= x1; px++) { + canvasPutPixel(dst, color, bpp); + dst += bpp; + } + } + } +} + + // Read/write a single pixel at the given address, respecting the display's // bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor). // These are inline because they're called per-pixel in tight loops (circle fill, @@ -322,13 +327,7 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { } // Find the AppContextT to get display format - WidgetT *root = parent; - - while (root->parent) { - root = root->parent; - } - - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = wgtGetContext(parent); if (!ctx) { return NULL; @@ -378,8 +377,6 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { cd->canvasBpp = bpp; cd->penColor = packColor(d, 0, 0, 0); cd->penSize = 1; - cd->lastX = -1; - cd->lastY = -1; wgt->data = cd; return wgt; } @@ -397,11 +394,7 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) { } // Find BlitOps for span fill - WidgetT *root = w; - while (root->parent) { - root = root->parent; - } - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = wgtGetContext(w); if (!ctx) { return; } @@ -413,6 +406,8 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) { for (int32_t y = 0; y < ch; y++) { ctx->blitOps.spanFill(cd->pixelData + y * pitch, color, cw); } + + wgtInvalidatePaint(w); } @@ -430,6 +425,7 @@ void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y } canvasDrawLine(cd, x0, y0, x1, y1); + wgtInvalidatePaint(w); } @@ -498,6 +494,8 @@ void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t canvasPutPixel(dst, color, bpp); } } + + wgtInvalidatePaint(w); } @@ -592,59 +590,9 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) { return; } - int32_t bpp = cd->canvasBpp; - int32_t pitch = cd->canvasPitch; - uint8_t *data = cd->pixelData; - int32_t cw = cd->canvasW; - int32_t ch = cd->canvasH; - uint32_t color = cd->penColor; - int32_t r2 = radius * radius; + canvasFillCircleSpans(cd, cx, cy, radius); - for (int32_t dy = -radius; dy <= radius; dy++) { - int32_t py = cy + dy; - - if (py < 0 || py >= ch) { - continue; - } - - // Compute horizontal half-span: dx^2 <= r^2 - dy^2 - int32_t dy2 = dy * dy; - int32_t rem = r2 - dy2; - int32_t hspan = 0; - - // Integer sqrt via Newton's method - if (rem > 0) { - hspan = radius; - - for (int32_t i = 0; i < 8; i++) { - hspan = (hspan + rem / hspan) / 2; - } - - if (hspan * hspan > rem) { - hspan--; - } - } - - int32_t x0 = cx - hspan; - int32_t x1 = cx + hspan; - - if (x0 < 0) { - x0 = 0; - } - - if (x1 >= cw) { - x1 = cw - 1; - } - - if (x0 <= x1) { - uint8_t *dst = data + py * pitch + x0 * bpp; - - for (int32_t px = x0; px <= x1; px++) { - canvasPutPixel(dst, color, bpp); - dst += bpp; - } - } - } + wgtInvalidatePaint(w); } @@ -684,11 +632,7 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t } // Find BlitOps for span fill - WidgetT *root = w; - while (root->parent) { - root = root->parent; - } - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = wgtGetContext(w); // Use the optimized spanFill (which uses rep stosl on x86) when available, // falling back to per-pixel writes if the AppContextT can't be reached. @@ -705,6 +649,8 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t } } } + + wgtInvalidatePaint(w); } @@ -759,6 +705,9 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path) { cd->canvasPitch = pitch; cd->canvasBpp = ctx->display.format.bytesPerPixel; + // Load changes canvas dimensions (canvasW/canvasH drive the min-size + // calc), so a full relayout is needed, not just a repaint. + wgtInvalidate(w); return 0; } @@ -876,6 +825,7 @@ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) { uint8_t *dst = cd->pixelData + y * cd->canvasPitch + x * bpp; canvasPutPixel(dst, color, bpp); + wgtInvalidatePaint(w); } @@ -911,9 +861,6 @@ static void widgetCanvasOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int3 int32_t cx = vx - w->x - CANVAS_BORDER; int32_t cy = vy - w->y - CANVAS_BORDER; - cd->lastX = cx; - cd->lastY = cy; - cd->onMouse(w, cx, cy, true); wgtInvalidatePaint(w); } @@ -937,11 +884,6 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { int32_t cy = vy - hit->y - CANVAS_BORDER; sDragWidget = hit; - cd->lastX = -1; - cd->lastY = -1; - - cd->lastX = cx; - cd->lastY = cy; cd->onMouse(hit, cx, cy, false); wgtInvalidatePaint(hit); @@ -973,7 +915,7 @@ void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma BevelStyleT sunken; sunken.highlight = colors->windowShadow; sunken.shadow = colors->windowHighlight; - sunken.face = 0; + sunken.face = BEVEL_NO_FILL; sunken.width = CANVAS_BORDER; drawBevel(d, ops, w->x, w->y, bevelW, bevelH, &sunken); diff --git a/src/widgets/kpunch/checkbox/widgetCheckbox.c b/src/widgets/kpunch/checkbox/widgetCheckbox.c index e490354..d630105 100644 --- a/src/widgets/kpunch/checkbox/widgetCheckbox.c +++ b/src/widgets/kpunch/checkbox/widgetCheckbox.c @@ -134,6 +134,12 @@ void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { sFocusedWidget = w; d->checked = !d->checked; + // Repaint the toggled check mark / focus ring. The mouse dispatcher + // only invalidates the previous focus owner, not the hit widget. + // Mark dirty before onChange so it lands even if the handler + // destroys the widget. + wgtInvalidatePaint(w); + if (w->onChange) { w->onChange(w); } diff --git a/src/widgets/kpunch/comboBox/widgetComboBox.c b/src/widgets/kpunch/comboBox/widgetComboBox.c index eebfcea..b8f0c6b 100644 --- a/src/widgets/kpunch/comboBox/widgetComboBox.c +++ b/src/widgets/kpunch/comboBox/widgetComboBox.c @@ -51,6 +51,7 @@ static int32_t sTypeId = -1; typedef struct { + // Fields are ordered by alignment to minimize struct padding. char *buf; int32_t bufSize; int32_t len; @@ -64,12 +65,12 @@ typedef struct { const char **items; int32_t itemCount; int32_t selectedIdx; - bool open; int32_t hoverIdx; int32_t listScrollPos; int32_t maxItemLen; // Owned item storage for AddItem/RemoveItem/Clear char **ownedItems; // stb_ds dynamic array of strdup'd strings + bool open; bool ownsItems; // true if items[] points to ownedItems } ComboBoxDataT; diff --git a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c index a3cd9e3..436f40b 100644 --- a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c +++ b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c @@ -36,6 +36,7 @@ #include "../../../libs/kpunch/sql/dvxSql.h" #include "thirdparty/stb_ds_wrap.h" +#include #include #include #include @@ -48,7 +49,6 @@ #define DATA_HEIGHT 24 #define DATA_BTN_W 20 #define DATA_BORDER 2 -#define DATA_MAX_COLS 64 #define DATA_MAX_FIELD 256 // ============================================================ @@ -87,6 +87,7 @@ typedef struct { bool eof; bool dirty; // current row has unsaved changes bool isNewRow; // current row was created by AddNew + bool refreshing; // guards against re-entrant Refresh recursion } DataCtrlDataT; static int32_t sTypeId = -1; @@ -139,6 +140,7 @@ void dataCtrlUpdateRow(WidgetT *w); static int32_t findKeyCol(const DataCtrlDataT *d); static void fireReposition(WidgetT *w); static void freeCache(DataCtrlDataT *d); +static int32_t sqlAppend(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...); void wgtRegister(void); @@ -174,9 +176,7 @@ void dataCtrlAddNew(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; // Auto-save current row if dirty - if (d->dirty) { - dataCtrlUpdate(w); - } + autoSave(w); if (d->colCount == 0) { return; @@ -255,10 +255,18 @@ void dataCtrlDelete(WidgetT *w) { int32_t keyCol = findKeyCol(d); const char *keyVal = d->rows[d->currentRow].fields[keyCol]; - char sql[512]; - snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s=%s", - d->recordSource, d->colNames[keyCol], keyVal); - dvxSqlExec(db, sql); + // Escape and quote the key value (injection / malformed-SQL fix). + // Skip the exec if the key is too long to escape (the buffer + // would be left unterminated) rather than run garbage SQL. + char keyEsc[DATA_MAX_FIELD * 2]; + + if (dvxSqlEscape(keyVal, keyEsc, sizeof(keyEsc)) >= 0) { + char sql[512]; + snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s='%s'", + d->recordSource, d->colNames[keyCol], keyEsc); + dvxSqlExec(db, sql); + } + dvxSqlClose(db); } } @@ -590,16 +598,31 @@ static void dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const void dataCtrlRefresh(WidgetT *w) { DataCtrlDataT *d = (DataCtrlDataT *)w->data; + // fireReposition (below) fires onChange, which for a detail control + // cascades back into Refresh; a cyclic/self master-detail link or a + // Reposition handler that calls .Refresh on this control would otherwise + // recurse without bound. Block re-entry on the same control. + if (d->refreshing) { + return; + } + + d->refreshing = true; freeCache(d); if (!d->databaseName[0] || !d->recordSource[0]) { + // Unconfigured: still fire Reposition so bound controls clear instead + // of keeping the previous record's stale values. + fireReposition(w); + d->refreshing = false; return; } int32_t db = dvxSqlOpen(d->databaseName); if (db <= 0) { + fireReposition(w); + d->refreshing = false; return; } @@ -625,6 +648,8 @@ void dataCtrlRefresh(WidgetT *w) { if (rs <= 0) { dvxSqlClose(db); + fireReposition(w); + d->refreshing = false; return; } @@ -666,7 +691,11 @@ void dataCtrlRefresh(WidgetT *w) { snprintf(d->caption, sizeof(d->caption), "%s", d->recordSource); } - wgtInvalidatePaint(w); + // Refresh moved the cursor to row 0; fire Reposition like every other + // cursor move so bound controls update and the user handler runs. + // fireReposition also invalidates the paint, so no separate call is needed. + fireReposition(w); + d->refreshing = false; } @@ -689,9 +718,7 @@ void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) { } // Auto-save before moving - if (d->dirty) { - dataCtrlUpdate(w); - } + autoSave(w); d->currentRow = row; d->bof = (row == 0); @@ -775,6 +802,37 @@ static void dataCtrlSetRecordSource(WidgetT *w, const char *val) { // dataCtrlUpdate -- save the current row to the database +// Bounded formatted append into a fixed SQL buffer. Returns pos +// unchanged once pos has reached bufSize; otherwise size-clamps the +// write and returns the new position, capped at bufSize-1 on +// truncation. Avoids the snprintf-accumulation overrun where +// bufSize-pos wraps to a huge size_t and the next write runs off the end. +static int32_t sqlAppend(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; + } + + 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) { + return bufSize - 1; + } + + return pos + written; +} + + // // If the current row was created by AddNew, executes an INSERT. // Otherwise executes an UPDATE using the KeyColumn to identify the row. @@ -814,25 +872,33 @@ void dataCtrlUpdate(WidgetT *w) { for (int32_t i = 0; i < d->colCount; i++) { if (i > 0) { - pos += snprintf(sql + pos, sizeof(sql) - pos, ", "); + pos = sqlAppend(sql, sizeof(sql), pos,", "); } - pos += snprintf(sql + pos, sizeof(sql) - pos, "%s", d->colNames[i]); + pos = sqlAppend(sql, sizeof(sql), pos,"%s", d->colNames[i]); } - pos += snprintf(sql + pos, sizeof(sql) - pos, ") VALUES ("); + pos = sqlAppend(sql, sizeof(sql), pos, ") VALUES ("); for (int32_t i = 0; i < d->colCount; i++) { if (i > 0) { - pos += snprintf(sql + pos, sizeof(sql) - pos, ", "); + pos = sqlAppend(sql, sizeof(sql), pos,", "); } char escaped[DATA_MAX_FIELD * 2]; - dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)); - pos += snprintf(sql + pos, sizeof(sql) - pos, "'%s'", escaped); + + // A field too long to escape would leave 'escaped' without a + // NUL terminator (dvxSqlEscape returns -1 and bails); abort + // rather than read an unterminated buffer into the SQL. + if (dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)) < 0) { + dvxSqlClose(db); + return; + } + + pos = sqlAppend(sql, sizeof(sql), pos,"'%s'", escaped); } - snprintf(sql + pos, sizeof(sql) - pos, ")"); + sqlAppend(sql, sizeof(sql), pos, ")"); } else { // UPDATE table SET col1='val1', ... WHERE keyCol=keyVal int32_t keyCol = findKeyCol(d); @@ -848,16 +914,30 @@ void dataCtrlUpdate(WidgetT *w) { } if (!first) { - pos += snprintf(sql + pos, sizeof(sql) - pos, ", "); + pos = sqlAppend(sql, sizeof(sql), pos,", "); } char escaped[DATA_MAX_FIELD * 2]; - dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)); - pos += snprintf(sql + pos, sizeof(sql) - pos, "%s='%s'", d->colNames[i], escaped); + + if (dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)) < 0) { + dvxSqlClose(db); + return; + } + + pos = sqlAppend(sql, sizeof(sql), pos,"%s='%s'", d->colNames[i], escaped); first = false; } - snprintf(sql + pos, sizeof(sql) - pos, " WHERE %s=%s", d->colNames[keyCol], keyVal); + // Escape and quote the key value: an unquoted/unescaped key + // produces malformed SQL for text keys and is an injection vector. + char keyEsc[DATA_MAX_FIELD * 2]; + + if (dvxSqlEscape(keyVal, keyEsc, sizeof(keyEsc)) < 0) { + dvxSqlClose(db); + return; + } + + sqlAppend(sql, sizeof(sql), pos, " WHERE %s='%s'", d->colNames[keyCol], keyEsc); } dvxSqlExec(db, sql); @@ -920,6 +1000,12 @@ static void freeCache(DataCtrlDataT *d) { d->currentRow = -1; d->bof = true; d->eof = true; + + // Reset the pending-write flags too: a Refresh after AddNew left + // isNewRow set, so the next autoSave would re-INSERT row 0's values + // as a duplicate. + d->dirty = false; + d->isNewRow = false; } diff --git a/src/widgets/kpunch/dbGrid/dbgrid.bhs b/src/widgets/kpunch/dbGrid/dbgrid.bhs index c257dac..0345b97 100644 --- a/src/widgets/kpunch/dbGrid/dbgrid.bhs +++ b/src/widgets/kpunch/dbGrid/dbgrid.bhs @@ -39,14 +39,18 @@ A data-bound grid that displays records from a Data control in a tabular format. ---------- ------- ------------------------------------------- DataSource String Name of the Data control that supplies records. GridLines Boolean Show or hide grid lines between cells (default: True). + SelectedRow Integer Index of the currently selected row (-1 = no selection). .endtable .h2 Type-Specific Methods .table - Method Parameters Description - ------- ---------- ------------------------------------------- - Refresh (none) Reload and redraw the grid from the Data control. + Method Parameters Description + ---------------- ------------------------------- ------------------------------------------- + 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. .endtable .h2 Type-Specific Events diff --git a/src/widgets/kpunch/dbGrid/widgetDbGrid.c b/src/widgets/kpunch/dbGrid/widgetDbGrid.c index 631d717..78a7ea2 100644 --- a/src/widgets/kpunch/dbGrid/widgetDbGrid.c +++ b/src/widgets/kpunch/dbGrid/widgetDbGrid.c @@ -54,8 +54,10 @@ #define DBGRID_PAD 3 #define DBGRID_SORT_W 10 #define DBGRID_MIN_ROWS 3 +#define DBGRID_MIN_COLS 20 // minimum visible width in characters #define DBGRID_MAX_NAME 64 #define DBGRID_RESIZE_ZONE 3 +#define DBGRID_HSTEP 20 // horizontal scroll step (px) for arrow clicks // Vertical padding added to font->charHeight for header and rows. #define DBGRID_HEADER_VPAD 4 @@ -63,7 +65,7 @@ // Alternating-row background shade: darken the content bg by this per- // channel amount so every other row is subtly distinct. -#define DBGRID_ALT_ROW_SHADE 0x080808 +#define DBGRID_ALT_ROW_SHADE 8 // Horizontal cull margin: columns whose rect is within this many pixels // of the visible area are still rendered. Gives scroll movement a bit @@ -109,7 +111,6 @@ typedef struct { int32_t scrollPosH; // horizontal scroll (pixel units) int32_t *resolvedW; // heap-allocated, sized to colCap int32_t totalColW; // 0 = needs recalc - bool gridLines; // Sort state int32_t sortCol; // visible-column index (-1 = none) @@ -121,11 +122,14 @@ typedef struct { int32_t resizeCol; int32_t resizeStartX; int32_t resizeOrigW; - bool resizeDragging; // Scrollbar drag state int32_t sbDragOrient; // 0=vert, 1=horiz, -1=col resize int32_t sbDragOff; + + // Flags (grouped last to minimize struct padding) + bool gridLines; + bool resizeDragging; } DbGridDataT; static int32_t sTypeId = -1; @@ -141,6 +145,7 @@ static int32_t dataRowToDisplay(const DbGridDataT *d, int32_t dataRow); static void dbGridBuildSortIndex(WidgetT *w); static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font); static WidgetT *dbGridCreate(WidgetT *parent); +static uint32_t dbGridDarkenColor(const DisplayT *disp, uint32_t packed, uint8_t amount); static void dbGridDestroy(WidgetT *w); static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my); static bool dbGridGetGridLines(const WidgetT *w); @@ -271,7 +276,13 @@ static void dbGridBuildSortIndex(WidgetT *w) { return; } - d->sortIndex = (int32_t *)malloc(rowCount * sizeof(int32_t)); + d->sortIndex = (int32_t *)malloc((size_t)rowCount * sizeof(int32_t)); + + if (!d->sortIndex) { + d->sortCount = 0; + return; + } + d->sortCount = rowCount; for (int32_t i = 0; i < rowCount; i++) { @@ -307,7 +318,7 @@ static void dbGridBuildSortIndex(WidgetT *w) { static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = font->charWidth * 20 + DBGRID_BORDER * 2; + w->calcMinW = font->charWidth * DBGRID_MIN_COLS + DBGRID_BORDER * 2; w->calcMinH = (font->charHeight + DBGRID_ROW_VPAD) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2; } @@ -336,6 +347,22 @@ static WidgetT *dbGridCreate(WidgetT *parent) { } +static uint32_t dbGridDarkenColor(const DisplayT *disp, uint32_t packed, uint8_t amount) { + uint8_t r; + uint8_t g; + uint8_t b; + + // contentBg is a packColor() result, not an RGB888 triple, so subtracting + // a packed constant underflows across channels (and is a palette index in + // 8bpp). Unpack, saturate per channel, then repack. + unpackColor(disp, packed, &r, &g, &b); + r = (r > amount) ? (uint8_t)(r - amount) : 0; + g = (g > amount) ? (uint8_t)(g - amount) : 0; + b = (b > amount) ? (uint8_t)(b - amount) : 0; + return packColor(disp, r, g, b); +} + + static void dbGridDestroy(WidgetT *w) { DbGridDataT *d = (DbGridDataT *)w->data; @@ -355,7 +382,7 @@ static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my) { return 0; } - int32_t headerH = ctx->font.charHeight + 4; + int32_t headerH = ctx->font.charHeight + DBGRID_HEADER_VPAD; int32_t headerTop = w->y + DBGRID_BORDER; if (my >= headerTop && my < headerTop + headerH) { @@ -466,22 +493,15 @@ static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) return; } - int32_t sbY = w->y + DBGRID_BORDER + headerH; - int32_t sbH = innerH; - int32_t relY = y - sbY - d->sbDragOff; - int32_t track = sbH - WGT_SB_W * 2; + int32_t track = innerH - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(track, rowCount, visRows, d->scrollPos, &thumbPos, &thumbSize); - if (track > 0) { - d->scrollPos = (relY - WGT_SB_W) * maxScroll / track; - - if (d->scrollPos < 0) { - d->scrollPos = 0; - } - - if (d->scrollPos > maxScroll) { - d->scrollPos = maxScroll; - } - } + int32_t sbY = w->y + DBGRID_BORDER + headerH; + int32_t relMouse = y - sbY - WGT_SB_W - d->sbDragOff; + int32_t newScroll = (track > thumbSize) ? (maxScroll * relMouse) / (track - thumbSize) : 0; + d->scrollPos = clampInt(newScroll, 0, maxScroll); wgtInvalidatePaint(w); return; @@ -505,22 +525,15 @@ static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) return; } - int32_t sbX = w->x + DBGRID_BORDER; - int32_t sbW = innerW; - int32_t relX = x - sbX - d->sbDragOff; - int32_t track = sbW - WGT_SB_W * 2; + int32_t track = innerW - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(track, d->totalColW, innerW, d->scrollPosH, &thumbPos, &thumbSize); - if (track > 0) { - d->scrollPosH = (relX - WGT_SB_W) * maxScroll / track; - - if (d->scrollPosH < 0) { - d->scrollPosH = 0; - } - - if (d->scrollPosH > maxScroll) { - d->scrollPosH = maxScroll; - } - } + int32_t sbX = w->x + DBGRID_BORDER; + int32_t relMouse = x - sbX - WGT_SB_W - d->sbDragOff; + int32_t newScroll = (track > thumbSize) ? (maxScroll * relMouse) / (track - thumbSize) : 0; + d->scrollPosH = clampInt(newScroll, 0, maxScroll); wgtInvalidatePaint(w); } @@ -635,7 +648,7 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { bool needV, needH; computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH); - int32_t rowH = ctx->font.charHeight + 2; + int32_t rowH = ctx->font.charHeight + DBGRID_ROW_VPAD; int32_t headerTop = w->y + DBGRID_BORDER; int32_t dataTop = headerTop + headerH; int32_t rowCount = getDataRowCount(d); @@ -651,36 +664,34 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { } int32_t relY = vy - sbY; - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(sbH, WGT_SB_W, maxScroll, d->scrollPos, &thumbPos, &thumbSize); + ScrollHitE sh = widgetScrollbarHitTest(sbH, relY, rowCount, visRows, d->scrollPos); - if (relY < WGT_SB_W) { - // Up arrow + if (sh == ScrollHitArrowDecE) { if (d->scrollPos > 0) { d->scrollPos--; } - } else if (relY >= sbH - WGT_SB_W) { - // Down arrow + } else if (sh == ScrollHitArrowIncE) { if (d->scrollPos < maxScroll) { d->scrollPos++; } - } else if (relY >= thumbPos && relY < thumbPos + thumbSize) { - // Thumb drag - d->sbDragOrient = 0; - d->sbDragOff = relY - thumbPos; - } else if (relY < thumbPos) { + } else if (sh == ScrollHitPageDecE) { d->scrollPos -= visRows; - - if (d->scrollPos < 0) { - d->scrollPos = 0; - } - } else { + d->scrollPos = clampInt(d->scrollPos, 0, maxScroll); + } else if (sh == ScrollHitPageIncE) { d->scrollPos += visRows; + d->scrollPos = clampInt(d->scrollPos, 0, maxScroll); + } else if (sh == ScrollHitThumbE) { + int32_t trackLen = sbH - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, rowCount, visRows, d->scrollPos, &thumbPos, &thumbSize); - if (d->scrollPos > maxScroll) { - d->scrollPos = maxScroll; - } + d->sbDragOrient = 0; + d->sbDragOff = relY - WGT_SB_W - thumbPos; + + // Register the drag with the WM so it dispatches drag-update/end + // to dbGridOnDragUpdate/dbGridOnDragEnd while the button is held. + sDragWidget = w; } wgtInvalidatePaint(w); @@ -698,27 +709,31 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { } int32_t relX = vx - sbX; - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(sbW, WGT_SB_W, maxScroll, d->scrollPosH, &thumbPos, &thumbSize); + ScrollHitE sh = widgetScrollbarHitTest(sbW, relX, d->totalColW, innerW, d->scrollPosH); - if (relX < WGT_SB_W) { - d->scrollPosH -= 20; + if (sh == ScrollHitArrowDecE) { + d->scrollPosH -= DBGRID_HSTEP; + } else if (sh == ScrollHitArrowIncE) { + d->scrollPosH += DBGRID_HSTEP; + } else if (sh == ScrollHitPageDecE) { + d->scrollPosH -= innerW; + } else if (sh == ScrollHitPageIncE) { + d->scrollPosH += innerW; + } else if (sh == ScrollHitThumbE) { + int32_t trackLen = sbW - WGT_SB_W * 2; + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, d->totalColW, innerW, d->scrollPosH, &thumbPos, &thumbSize); - if (d->scrollPosH < 0) { - d->scrollPosH = 0; - } - } else if (relX >= sbW - WGT_SB_W) { - d->scrollPosH += 20; - - if (d->scrollPosH > maxScroll) { - d->scrollPosH = maxScroll; - } - } else if (relX >= thumbPos && relX < thumbPos + thumbSize) { d->sbDragOrient = 1; - d->sbDragOff = relX - thumbPos; + d->sbDragOff = relX - WGT_SB_W - thumbPos; + + // Register the drag with the WM (see vertical-thumb case above). + sDragWidget = w; } + d->scrollPosH = clampInt(d->scrollPosH, 0, maxScroll); + wgtInvalidatePaint(w); return; } @@ -736,6 +751,9 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { d->resizeOrigW = d->resolvedW[borderCol]; d->resizeDragging = false; d->sbDragOrient = -1; + + // Register the drag with the WM (see vertical-thumb case above). + sDragWidget = w; return; } @@ -876,7 +894,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B // Alternating row background if (i % 2 == 1) { - uint32_t altBg = colors->contentBg - DBGRID_ALT_ROW_SHADE; + uint32_t altBg = dbGridDarkenColor(disp, colors->contentBg, DBGRID_ALT_ROW_SHADE); rectFill(disp, ops, contentX, ry, innerW, rowH, altBg); } @@ -888,7 +906,7 @@ static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B } uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg; - uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? colors->contentBg - DBGRID_ALT_ROW_SHADE : colors->contentBg); + uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? dbGridDarkenColor(disp, colors->contentBg, DBGRID_ALT_ROW_SHADE) : colors->contentBg); // Draw cells int32_t cx = contentX - d->scrollPosH; diff --git a/src/widgets/kpunch/dropdown/widgetDropdown.c b/src/widgets/kpunch/dropdown/widgetDropdown.c index a98eb38..4ea14c3 100644 --- a/src/widgets/kpunch/dropdown/widgetDropdown.c +++ b/src/widgets/kpunch/dropdown/widgetDropdown.c @@ -64,7 +64,6 @@ typedef struct { int32_t maxItemLen; // Owned item storage for AddItem/RemoveItem/Clear char **ownedItems; // stb_ds dynamic array of strdup'd strings - bool ownsItems; // true if items[] points to ownedItems } DropdownDataT; @@ -100,7 +99,6 @@ static void dropdownSyncOwned(WidgetT *w) { d->items = (const char **)d->ownedItems; d->itemCount = (int32_t)arrlen(d->ownedItems); d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount); - d->ownsItems = true; wgtInvalidate(w); } @@ -211,7 +209,19 @@ void widgetDropdownAccelActivate(WidgetT *w, WidgetT *root) { DropdownDataT *d = (DropdownDataT *)w->data; d->open = true; d->hoverIdx = d->selectedIdx; - sOpenPopup = w; + sOpenPopup = w; + + // Scroll the selected item into view, mirroring the keyboard-open path. + if (d->hoverIdx >= 0) { + if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) { + d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1; + } + + if (d->hoverIdx < d->scrollPos) { + d->scrollPos = d->hoverIdx; + } + } + wgtInvalidatePaint(w); } @@ -423,6 +433,23 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { d->open = true; d->hoverIdx = d->selectedIdx; sOpenPopup = w; + + // Scroll the selected item into view, mirroring the keyboard-open path. + // Guard against hoverIdx == -1 (no selection) so scrollPos is never + // driven negative. + if (d->hoverIdx >= 0) { + if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) { + d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1; + } + + if (d->hoverIdx < d->scrollPos) { + d->scrollPos = d->hoverIdx; + } + } + + // Repaint so the popup overlay appears immediately; the mouse + // dispatcher does not invalidate the hit widget on open. + wgtInvalidatePaint(w); } @@ -433,11 +460,7 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const // Sunken text area int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH; - BevelStyleT bevel; - bevel.highlight = colors->windowShadow; - bevel.shadow = colors->windowHighlight; - bevel.face = bg; - bevel.width = 2; + BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2); drawBevel(disp, ops, w->x, w->y, textAreaW, w->h, &bevel); // Draw selected item text diff --git a/src/widgets/kpunch/image/widgetImage.c b/src/widgets/kpunch/image/widgetImage.c index a03467f..0f87642 100644 --- a/src/widgets/kpunch/image/widgetImage.c +++ b/src/widgets/kpunch/image/widgetImage.c @@ -44,16 +44,16 @@ static int32_t sTypeId = -1; typedef struct { + // Fields are ordered by alignment to minimize struct padding. uint8_t *pixelData; uint8_t *grayData; int32_t imgW; int32_t imgH; int32_t imgPitch; - bool pressed; - bool hasTransparency; uint32_t keyColor; - char picturePath[DVX_MAX_PATH]; + bool hasTransparency; bool stretch; // true = scale to fit widget bounds + char picturePath[DVX_MAX_PATH]; } ImageDataT; @@ -82,12 +82,15 @@ WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int WidgetT *wgt = widgetAlloc(parent, sTypeId); if (!wgt) { + // The widget owns pixelData; free it on every failure path. + free(pixelData); return NULL; } ImageDataT *d = calloc(1, sizeof(ImageDataT)); if (!d) { + free(pixelData); widgetAllocRollback(wgt); return NULL; } @@ -96,7 +99,6 @@ WidgetT *wgtImage(WidgetT *parent, uint8_t *pixelData, int32_t w, int32_t h, int d->imgW = w; d->imgH = h; d->imgPitch = pitch; - d->pressed = false; wgt->data = d; return wgt; } @@ -247,24 +249,17 @@ void widgetImageDestroy(WidgetT *w) { } -// Mouse click: briefly sets pressed=true (for the 1px offset effect), -// invalidates to show the press, fires onClick, then immediately clears -// pressed. The press is purely visual -- there's no release tracking like -// Button has, since Image clicks are meant for instant actions (e.g., -// clicking a logo or icon area). +// Mouse click: invalidates and fires onClick. Image clicks are instant +// actions (e.g. clicking a logo or icon area) with no press/release tracking. void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vx; (void)vy; - ImageDataT *d = (ImageDataT *)w->data; - d->pressed = true; wgtInvalidatePaint(w); if (w->onClick) { w->onClick(w); } - - d->pressed = false; } @@ -306,11 +301,6 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const Bit int32_t dx = w->x + (wgtW - fitW) / 2; int32_t dy = w->y + (wgtH - fitH) / 2; - if (d->pressed) { - dx++; - dy++; - } - // If image fits at 1:1, blit directly (no scaling needed) if (fitW == imgW && fitH == imgH) { uint8_t *src = w->enabled ? d->pixelData : d->grayData; diff --git a/src/widgets/kpunch/imageButton/widgetImageButton.c b/src/widgets/kpunch/imageButton/widgetImageButton.c index e7aea54..9d4d2f1 100644 --- a/src/widgets/kpunch/imageButton/widgetImageButton.c +++ b/src/widgets/kpunch/imageButton/widgetImageButton.c @@ -44,6 +44,8 @@ // 2px bevel on every side -- min size and draw inset both derive from this. #define IMAGEBUTTON_BEVEL_W 2 +#define IMAGEBUTTON_FOCUS_INSET 3 // focus-rect inset from the button edge +#define IMAGEBUTTON_PRESS_OFFSET 1 // px shift of content while pressed static int32_t sTypeId = -1; @@ -187,6 +189,13 @@ void wgtImageButtonSetData(WidgetT *w, uint8_t *pixelData, int32_t imgW, int32_t d->imgW = imgW; d->imgH = imgH; d->imgPitch = pitch; + + // Drop the cached grayscale (disabled) image: it was sized for the + // old dimensions, so the disabled-paint path would otherwise reuse a + // stale, wrongly-sized buffer and over-read it. + free(d->grayData); + d->grayData = NULL; + wgtInvalidate(w); } @@ -272,8 +281,8 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, con if (w == sFocusedWidget) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; - int32_t off = pressed ? 1 : 0; - drawFocusRect(disp, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg); + int32_t off = pressed ? IMAGEBUTTON_PRESS_OFFSET : 0; + drawFocusRect(disp, ops, w->x + IMAGEBUTTON_FOCUS_INSET + off, w->y + IMAGEBUTTON_FOCUS_INSET + off, w->w - IMAGEBUTTON_FOCUS_INSET * 2, w->h - IMAGEBUTTON_FOCUS_INSET * 2, fg); } } diff --git a/src/widgets/kpunch/listBox/widgetListBox.c b/src/widgets/kpunch/listBox/widgetListBox.c index fedd5e0..5d485d2 100644 --- a/src/widgets/kpunch/listBox/widgetListBox.c +++ b/src/widgets/kpunch/listBox/widgetListBox.c @@ -58,22 +58,22 @@ static int32_t sTypeId = -1; typedef struct { + // Fields are ordered by alignment to minimize struct padding. const char **items; int32_t itemCount; int32_t selectedIdx; int32_t scrollPos; int32_t maxItemLen; - bool multiSelect; int32_t anchorIdx; uint8_t *selBits; - bool reorderable; int32_t dragIdx; int32_t dropIdx; int32_t sbDragOrient; int32_t sbDragOff; // Owned item storage for AddItem/RemoveItem/Clear char **ownedItems; // stb_ds dynamic array of strdup'd strings - bool ownsItems; // true if items[] points to ownedItems + bool multiSelect; + bool reorderable; } ListBoxDataT; #include @@ -93,6 +93,7 @@ typedef struct { static void basSetItems(WidgetT *w, const char *spec); static void ensureScrollVisible(WidgetT *w, int32_t idx); +static void listBoxAdoptItems(WidgetT *w); static void listBoxAllocSelBits(WidgetT *w); static void listBoxSyncOwned(WidgetT *w); static void selectRange(WidgetT *w, int32_t from, int32_t to); @@ -204,12 +205,41 @@ static void listBoxSyncOwned(WidgetT *w) { d->items = (const char **)d->ownedItems; d->itemCount = (int32_t)arrlen(d->ownedItems); d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount); - d->ownsItems = true; listBoxAllocSelBits(w); wgtInvalidate(w); } +// Copy the current (external) item strings into the owned storage so a +// reorderable list box can rearrange them without mutating a caller-owned +// SetItems array (which may be const/shared). Mirrors listViewAdoptCells. +// No-op when already owned. Preserves the current item order and selection. +static void listBoxAdoptItems(WidgetT *w) { + ListBoxDataT *d = (ListBoxDataT *)w->data; + + if (d->ownedItems && d->items == (const char **)d->ownedItems) { + return; + } + + int32_t count = d->itemCount; + + for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) { + free(d->ownedItems[i]); + } + + arrsetlen(d->ownedItems, 0); + + for (int32_t i = 0; i < count; i++) { + const char *src = d->items ? d->items[i] : NULL; + arrput(d->ownedItems, strdup(src ? src : "")); + } + + d->items = (const char **)d->ownedItems; + d->itemCount = count; + d->maxItemLen = widgetMaxItemLen(d->items, count); +} + + // Set selection bits for all items in the range [from, to] (inclusive, // order-independent). Used for shift-click and shift-arrow range selection. static void selectRange(WidgetT *w, int32_t from, int32_t to) { @@ -243,7 +273,8 @@ static void selectRange(WidgetT *w, int32_t from, int32_t to) { ListBoxDataT *d = (ListBoxDataT *)calloc(1, sizeof(ListBoxDataT)); if (!d) { - return w; + widgetAllocRollback(w); + return NULL; } w->data = d; @@ -390,6 +421,11 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { d->selBits[d->selectedIdx] = 1; } + // A reorderable list box must own its items (see listBoxAdoptItems). + if (d->reorderable) { + listBoxAdoptItems(w); + } + wgtInvalidate(w); } @@ -428,6 +464,12 @@ void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) { ListBoxDataT *d = (ListBoxDataT *)w->data; d->reorderable = reorderable; + + // Own the items so drag-reorder can rearrange them even when supplied + // externally via wgtListBoxSetItems. + if (reorderable) { + listBoxAdoptItems(w); + } } @@ -520,9 +562,13 @@ static void widgetListBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int3 // navigation. This shared helper ensures consistent keyboard navigation // across ListBox, ListView, and other scrollable widgets. void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { + if (!w || w->type != sTypeId || !w->data) { + return; + } + ListBoxDataT *d = (ListBoxDataT *)w->data; - if (!w || w->type != sTypeId || d->itemCount == 0) { + if (d->itemCount == 0) { return; } @@ -784,7 +830,17 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm rectFill(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, ibg); } - drawText(d, ops, font, innerX, iy, lb->items[idx], ifg, ibg, false); + // Clamp item text to the content width so long items don't overpaint + // the scrollbar and neighboring widgets (no per-widget paint clip). + int32_t availW = contentW - LISTBOX_PAD; + int32_t maxChars = availW > 0 ? availW / font->charWidth : 0; + int32_t itemLen = (int32_t)strlen(lb->items[idx]); + + if (itemLen > maxChars) { + itemLen = maxChars; + } + + drawTextN(d, ops, font, innerX, iy, lb->items[idx], itemLen, ifg, ibg, false); // Draw cursor indicator in multi-select (dotted focus rect on cursor item) if (multi && idx == lb->selectedIdx && w == sFocusedWidget) { @@ -826,11 +882,7 @@ static void widgetListBoxReorderDrop(WidgetT *w) { d->dragIdx = -1; d->dropIdx = -1; - if (from < 0 || to < 0 || from == to || from == to - 1) { - return; - } - - if (from < 0 || from >= d->itemCount) { + if (from < 0 || to < 0 || from >= d->itemCount || from == to || from == to - 1) { return; } diff --git a/src/widgets/kpunch/listView/widgetListView.c b/src/widgets/kpunch/listView/widgetListView.c index ca772f9..a3b9872 100644 --- a/src/widgets/kpunch/listView/widgetListView.c +++ b/src/widgets/kpunch/listView/widgetListView.c @@ -97,12 +97,13 @@ static int32_t sTypeId = -1; // Sort-indicator triangle glyph (centered on column header). #define LISTVIEW_SORT_TRI_HALF_W 3 // half-width of the widest row #define LISTVIEW_SORT_TRI_HEIGHT 4 // rows in triangle glyph -#define LISTVIEW_SORT_TRI_BASE 7 // base-row width (2*HALF_W + 1) +#define LISTVIEW_SORT_TRI_BASE (2 * LISTVIEW_SORT_TRI_HALF_W + 1) // base-row width // ListView private data -- heap-allocated per widget, stored in w->data. // This replaces the old as.listView union pointer. The struct includes // column resize drag state that was previously in core globals. typedef struct { + // Fields are ordered by alignment to minimize struct padding. const ListViewColT *cols; int32_t colCount; const char **cellData; @@ -116,10 +117,8 @@ typedef struct { int32_t resolvedColCap; // allocated capacity int32_t totalColW; int32_t *sortIndex; - bool multiSelect; int32_t anchorIdx; uint8_t *selBits; - bool reorderable; int32_t dragIdx; int32_t dropIdx; void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); @@ -127,17 +126,20 @@ typedef struct { int32_t resizeCol; int32_t resizeStartX; int32_t resizeOrigW; - bool resizeDragging; int32_t sbDragOrient; int32_t sbDragOff; // Owned cell storage for AddItem/RemoveRow/Clear/SetCell char **ownedCells; // stb_ds dynamic array of strdup'd strings - bool ownsCells; // true if cellData points to ownedCells int32_t nextCell; // next cell position for AddItem (row-major index) // Owned column definitions (deep copies for BASIC callers) ListViewColT *ownedColDefs; // heap array of column defs (NULL if not owned) char **ownedColTitles; // heap array of strdup'd title strings int32_t ownedColCount; + // Flags (grouped last to minimize struct padding) + bool multiSelect; + bool reorderable; + bool resizeDragging; + bool ownsCells; // true if cellData points to ownedCells } ListViewDataT; // BASIC wrapper: SetColumns "Name,100|Description,200" @@ -150,6 +152,7 @@ typedef struct { static void allocListViewSelBits(WidgetT *w); static void basSetColumns(WidgetT *w, const char *spec); +static void listViewAdoptCells(WidgetT *w); static void listViewBuildSortIndex(WidgetT *w); static void listViewSyncOwned(WidgetT *w); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); @@ -222,6 +225,11 @@ static void basSetColumns(WidgetT *w, const char *spec) { lv->ownedColDefs = NULL; lv->ownedColCount = 0; + // lv->cols was aliased to the just-freed ownedColDefs by a prior + // wgtListViewSetColumns, so clear the live alias to avoid a dangling read. + lv->cols = NULL; + lv->colCount = 0; + // Parse spec into a temporary buffer char buf[1024]; snprintf(buf, sizeof(buf), "%s", spec); @@ -320,19 +328,18 @@ static void listViewBuildSortIndex(WidgetT *w) { return; } - // Allocate or reuse - int32_t *idx = lv->sortIndex; + // Always size to the current rowCount. The buffer is fully + // re-initialized below, but it must be (re)grown when rows were + // added since the last build, or the identity-init and insertion + // sort would write past an undersized allocation (heap overflow). + int32_t *idx = (int32_t *)realloc(lv->sortIndex, (size_t)rowCount * sizeof(int32_t)); if (!idx) { - idx = (int32_t *)malloc(rowCount * sizeof(int32_t)); - - if (!idx) { - return; - } - - lv->sortIndex = idx; + return; } + lv->sortIndex = idx; + // Initialize identity for (int32_t i = 0; i < rowCount; i++) { idx[i] = i; @@ -400,6 +407,37 @@ static void listViewSyncOwned(WidgetT *w) { } +// Copy the current (application-owned, external) cell data into the internally +// owned storage so the list view may rearrange it. A reorderable list view +// MUST own its cells: drag-reorder rewrites the cell array in place and must +// not mutate a caller-owned SetData array (which may be const/shared), so +// widgetListViewReorderDrop refuses to move rows unless ownsCells is true. +// No-op when already owned. Preserves the current row order and selection. +static void listViewAdoptCells(WidgetT *w) { + ListViewDataT *lv = (ListViewDataT *)w->data; + + if (lv->ownsCells) { + return; + } + + int32_t cellCount = lv->rowCount * lv->colCount; + + for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) { + free(lv->ownedCells[i]); + } + + arrsetlen(lv->ownedCells, 0); + + for (int32_t i = 0; i < cellCount; i++) { + const char *src = lv->cellData ? lv->cellData[i] : NULL; + arrput(lv->ownedCells, strdup(src ? src : "")); + } + + lv->cellData = (const char **)lv->ownedCells; + lv->ownsCells = true; +} + + // Resolve tagged column widths to actual pixel values. Column widths in the // ListViewColT definition can be specified as: // 0 = auto (scan data for widest string, include header) @@ -460,7 +498,10 @@ WidgetT *wgtListView(WidgetT *parent) { ListViewDataT *lv = (ListViewDataT *)calloc(1, sizeof(ListViewDataT)); if (!lv) { - free(w); + // Detach from the parent child list + clear core references before + // freeing, matching slider/canvas/listBox; a plain free(w) would + // leave a dangling child pointer in the parent. + widgetAllocRollback(w); return NULL; } @@ -599,6 +640,13 @@ void wgtListViewRemoveRow(WidgetT *w, int32_t row) { return; } + // Rows backed by external SetData (ownsCells == false) have no + // ownedCells array; removing would deref NULL. Refuse rather than + // crash -- callers should manage externally-supplied data themselves. + if (!lv->ownsCells || !lv->ownedCells) { + return; + } + // Remove colCount cells starting at row * colCount int32_t base = row * lv->colCount; @@ -714,6 +762,7 @@ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { lv->cellData = cellData; lv->rowCount = rowCount; lv->totalColW = 0; + lv->ownsCells = false; if (lv->selectedIdx >= rowCount) { lv->selectedIdx = rowCount > 0 ? 0 : -1; @@ -740,6 +789,12 @@ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { } } + // A reorderable list view must own its cells so drag-reorder can rearrange + // them (see listViewAdoptCells); copy the just-set external data in. + if (lv->reorderable) { + listViewAdoptCells(w); + } + wgtInvalidate(w); } @@ -792,6 +847,10 @@ void wgtListViewSetReorderable(WidgetT *w, bool reorderable) { free(lv->sortIndex); lv->sortIndex = NULL; } + + // Own the cells so drag-reorder can rearrange them even when the data + // was supplied externally via wgtListViewSetData. + listViewAdoptCells(w); } } @@ -951,9 +1010,13 @@ static void widgetListViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int // to the data-row selBits. This means the selected data rows may not be // contiguous in the original data order. void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { + if (!w || w->type != sTypeId) { + return; + } + ListViewDataT *lv = (ListViewDataT *)w->data; - if (!w || w->type != sTypeId || lv->rowCount == 0) { + if (lv->rowCount == 0) { return; } @@ -1293,7 +1356,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) int32_t cw = lv->resolvedColW[c]; int32_t border = colX + cw; - if (vx >= border - LISTVIEW_RESIZE_ZONE && vx <= border + LISTVIEW_RESIZE_ZONE && c < lv->colCount) { + if (vx >= border - LISTVIEW_RESIZE_ZONE && vx <= border + LISTVIEW_RESIZE_ZONE) { if (multiClickDetect(vx, vy) >= 2) { // Double-click on column border: auto-size to fit content int32_t maxLen = (int32_t)strlen(lv->cols[c].title); @@ -1774,6 +1837,14 @@ static void widgetListViewReorderDrop(WidgetT *w) { return; } + // Reorder rewrites cellData in place, which is only safe for the + // internally-owned ownedCells array. When cellData points at an + // application-owned array (SetData-fed lists, ownsCells == false) the + // contract forbids rearranging it, so drag-reorder is a no-op. + if (!lv->ownsCells) { + return; + } + // Move row data by shifting the cellData pointers. // cellData is row-major: cellData[row * colCount + col]. int32_t cols = lv->colCount; @@ -2068,9 +2139,9 @@ static const WgtMethodDescT sMethods[] = { static const WgtIfaceT sIface = { .basName = "ListView", .props = sProps, - .propCount = 1, + .propCount = sizeof(sProps) / sizeof(sProps[0]), .methods = sMethods, - .methodCount = 14, + .methodCount = sizeof(sMethods) / sizeof(sMethods[0]), .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, diff --git a/src/widgets/kpunch/progressBar/widgetProgressBar.c b/src/widgets/kpunch/progressBar/widgetProgressBar.c index 9d1104f..abee776 100644 --- a/src/widgets/kpunch/progressBar/widgetProgressBar.c +++ b/src/widgets/kpunch/progressBar/widgetProgressBar.c @@ -43,6 +43,10 @@ // much on each axis, so 2*PROGRESSBAR_BEVEL_W total. #define PROGRESSBAR_BEVEL_W 2 +// Main-axis minimum length expressed in character widths, so the bar is wide +// enough to show meaningful progress granularity. +#define PROGRESSBAR_MIN_CHARS 12 + static int32_t sTypeId = -1; typedef struct { @@ -79,9 +83,8 @@ WidgetT *wgtProgressBar(WidgetT *parent) { return NULL; } - d->value = 0; + // calloc already zeroed value (0) and vertical (false). d->maxValue = 100; - d->vertical = false; w->data = d; return w; } @@ -126,7 +129,7 @@ WidgetT *wgtProgressBarV(WidgetT *parent) { return NULL; } - d->value = 0; + // calloc already zeroed value (0). d->maxValue = 100; d->vertical = true; w->data = d; @@ -190,11 +193,11 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) { ProgressBarDataT *d = (ProgressBarDataT *)w->data; if (d->vertical) { - w->calcMinW = font->charHeight + 4; - w->calcMinH = font->charWidth * 12; + w->calcMinW = font->charHeight + 2 * PROGRESSBAR_BEVEL_W; + w->calcMinH = font->charWidth * PROGRESSBAR_MIN_CHARS; } else { - w->calcMinW = font->charWidth * 12; - w->calcMinH = font->charHeight + 4; + w->calcMinW = font->charWidth * PROGRESSBAR_MIN_CHARS; + w->calcMinH = font->charHeight + 2 * PROGRESSBAR_BEVEL_W; } } diff --git a/src/widgets/kpunch/radio/widgetRadio.c b/src/widgets/kpunch/radio/widgetRadio.c index a39c4a7..180ee20 100644 --- a/src/widgets/kpunch/radio/widgetRadio.c +++ b/src/widgets/kpunch/radio/widgetRadio.c @@ -103,7 +103,10 @@ static void invalidateOldSelection(WidgetT *group, int32_t oldIdx) { WidgetT *wgtRadio(WidgetT *parent, const char *text) { WidgetT *w = widgetAllocWithText(parent, sRadioTypeId, sizeof(RadioDataT), text); - if (w && w->data) { + // widgetAllocWithText permits a NULL parent; the index loop below + // dereferences parent, so only run it when there is one (index stays 0, + // the calloc default, otherwise). + if (w && w->data && parent) { RadioDataT *d = (RadioDataT *)w->data; // Auto-assign index based on position in parent @@ -345,42 +348,9 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)vy; sFocusedWidget = w; - RadioDataT *rd = (RadioDataT *)w->data; - - if (w->parent && w->parent->type == sRadioGroupTypeId) { - RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data; - - if (gd->selectedIdx != rd->index) { - invalidateOldSelection(w->parent, gd->selectedIdx); - } - - gd->selectedIdx = rd->index; - - if (w->parent->onChange) { - w->parent->onChange(w->parent); - } - } else if (w->parent) { - // .frm-loaded OptionButton: no enclosing RadioGroup. Walk - // sibling OptionButtons, clear theirs, mark this one selected. - for (WidgetT *c = w->parent->firstChild; c; c = c->nextSibling) { - if (c->type == sRadioTypeId && c != w) { - RadioDataT *sd = (RadioDataT *)c->data; - - if (sd && sd->selected) { - sd->selected = false; - wgtInvalidatePaint(c); - } - } - } - - if (rd) { - rd->selected = true; - } - - if (w->onChange) { - w->onChange(w); - } - } + // selectRadio handles both parent types (RadioGroup vs plain layout + // container) and the onChange dispatch; this was a verbatim copy of it. + selectRadio(w->parent, w); // Always repaint the clicked radio: its selection dot or focus rect // may need to appear. The mouse dispatcher invalidates the previous diff --git a/src/widgets/kpunch/scrollPane/widgetScrollPane.c b/src/widgets/kpunch/scrollPane/widgetScrollPane.c index 74797cd..c456675 100644 --- a/src/widgets/kpunch/scrollPane/widgetScrollPane.c +++ b/src/widgets/kpunch/scrollPane/widgetScrollPane.c @@ -478,6 +478,26 @@ void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) { c->y = pos; c->w = childW; c->h = ms; + + // Apply the same maxW/maxH cap as the first placement loop; + // omitting it stretched constrained children to full size in + // exactly the frames where the scrollbar set toggled (flicker). + if (c->maxW) { + int32_t maxPx = wgtResolveSize(c->maxW, childW, font->charWidth); + + if (c->w > maxPx) { + c->w = maxPx; + } + } + + if (c->maxH) { + int32_t maxPx = wgtResolveSize(c->maxH, ms, font->charWidth); + + if (c->h > maxPx) { + c->h = maxPx; + } + } + pos += ms + gap; widgetLayoutChildren(c, font); diff --git a/src/widgets/kpunch/slider/widgetSlider.c b/src/widgets/kpunch/slider/widgetSlider.c index 8440f0d..e11ee26 100644 --- a/src/widgets/kpunch/slider/widgetSlider.c +++ b/src/widgets/kpunch/slider/widgetSlider.c @@ -205,15 +205,21 @@ static void widgetSliderOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, return; } + // Guard the divisor: a slider sized at or below the thumb width has + // no travel and would divide by zero (or negative) here. + int32_t thumbRange = (d->vertical ? w->h : w->w) - SLIDER_THUMB_W; + + if (thumbRange <= 0) { + return; + } + int32_t newVal; if (d->vertical) { - int32_t thumbRange = w->h - SLIDER_THUMB_W; - int32_t relY = mouseY - w->y - d->dragOffset; + int32_t relY = mouseY - w->y - d->dragOffset; newVal = d->minValue + (relY * range) / thumbRange; } else { - int32_t thumbRange = w->w - SLIDER_THUMB_W; - int32_t relX = mouseX - w->x - d->dragOffset; + int32_t relX = mouseX - w->x - d->dragOffset; newVal = d->minValue + (relX * range) / thumbRange; } @@ -311,6 +317,11 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (d->vertical) { thumbRange = hit->h - SLIDER_THUMB_W; + + if (thumbRange <= 0) { + return; // no travel; nothing to position + } + thumbPos = ((d->value - d->minValue) * thumbRange) / range; mousePos = vy - hit->y; @@ -326,6 +337,7 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (newVal > d->maxValue) { newVal = d->maxValue; } d->value = newVal; + wgtInvalidatePaint(hit); if (hit->onChange) { hit->onChange(hit); @@ -333,6 +345,11 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { } } else { thumbRange = hit->w - SLIDER_THUMB_W; + + if (thumbRange <= 0) { + return; // no travel; nothing to position + } + thumbPos = ((d->value - d->minValue) * thumbRange) / range; mousePos = vx - hit->x; @@ -348,6 +365,7 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (newVal > d->maxValue) { newVal = d->maxValue; } d->value = newVal; + wgtInvalidatePaint(hit); if (hit->onChange) { hit->onChange(hit); diff --git a/src/widgets/kpunch/spinner/widgetSpinner.c b/src/widgets/kpunch/spinner/widgetSpinner.c index fb6893c..f20b4f7 100644 --- a/src/widgets/kpunch/spinner/widgetSpinner.c +++ b/src/widgets/kpunch/spinner/widgetSpinner.c @@ -94,18 +94,18 @@ typedef struct { double realMax; double realStep; int32_t decimals; - bool useReal; - // Text editing state - char buf[SPINNER_BUF_SIZE]; + // Text editing state (fields ordered by alignment to minimize padding) int32_t len; int32_t cursorPos; int32_t scrollOff; int32_t selStart; int32_t selEnd; - char undoBuf[SPINNER_BUF_SIZE]; int32_t undoLen; int32_t undoCursor; + char buf[SPINNER_BUF_SIZE]; + char undoBuf[SPINNER_BUF_SIZE]; + bool useReal; bool editing; } SpinnerDataT; @@ -141,6 +141,7 @@ void wgtSpinnerSetStep(WidgetT *w, int32_t step); void wgtSpinnerSetValue(WidgetT *w, int32_t value); static void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font); static const char *widgetSpinnerGetText(const WidgetT *w); +static void widgetSpinnerOnBlur(WidgetT *w); static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod); static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); static void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -309,6 +310,7 @@ static const WidgetClassT sClassSpinner = { [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetSpinnerCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetSpinnerOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetSpinnerOnKey, + [WGT_METHOD_ON_BLUR] = (void *)widgetSpinnerOnBlur, [WGT_METHOD_GET_TEXT] = (void *)widgetSpinnerGetText, [WGT_METHOD_SET_TEXT] = (void *)widgetSpinnerSetText, } @@ -381,7 +383,8 @@ WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t ste SpinnerDataT *d = (SpinnerDataT *)calloc(1, sizeof(SpinnerDataT)); if (!d) { - return w; + widgetAllocRollback(w); + return NULL; } w->data = d; @@ -421,6 +424,12 @@ int32_t wgtSpinnerGetValue(const WidgetT *w) { spinnerCommitEdit(d); } + // In real mode the integer accessor must reflect the real value + // (truncated toward zero), not the stale d->value slot. + if (d->useReal) { + return (int32_t)d->realValue; + } + return d->value; } @@ -650,8 +659,35 @@ static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { memcpy(d->buf, d->undoBuf, sizeof(d->buf)); d->len = d->undoLen; d->cursorPos = d->undoCursor; - d->selStart = 0; - d->selEnd = 0; + d->selStart = -1; + d->selEnd = -1; + } + + wgtInvalidatePaint(w); +} + + +// Focus loss: commit and clamp any in-progress edit, so typed-but-uncommitted +// text (e.g. an out-of-range number the user never pressed Enter on) is parsed +// and clamped to [min,max] instead of lingering. Fires the Change callback only +// when the committed value actually differs, so a focus-out with no real edit +// does not spuriously trigger the app's handler. +static void widgetSpinnerOnBlur(WidgetT *w) { + SpinnerDataT *d = (SpinnerDataT *)w->data; + + if (!d->editing) { + return; + } + + int32_t oldValue = d->value; + double oldRealValue = d->realValue; + + spinnerCommitEdit(d); + + bool changed = d->useReal ? (d->realValue != oldRealValue) : (d->value != oldValue); + + if (changed && w->onChange) { + w->onChange(w); } wgtInvalidatePaint(w); diff --git a/src/widgets/kpunch/statusBar/widgetStatusBar.c b/src/widgets/kpunch/statusBar/widgetStatusBar.c index 47aa065..9b8e817 100644 --- a/src/widgets/kpunch/statusBar/widgetStatusBar.c +++ b/src/widgets/kpunch/statusBar/widgetStatusBar.c @@ -45,8 +45,8 @@ // (VB-style). The per-child sunken bevel already adds 1px of visual // inset around each panel, so additional container padding just makes // the bar look chunky. -#define TOOLBAR_PAD 1 -#define TOOLBAR_GAP 2 +#define STATUSBAR_PAD 1 +#define STATUSBAR_GAP 2 static int32_t sTypeId = -1; @@ -103,22 +103,18 @@ void wgtRegister(void) { WidgetT *wgtStatusBar(WidgetT *parent) { - WidgetT *w = widgetAlloc(parent, sTypeId); - - if (w) { - w->padding = wgtPixels(2); - w->spacing = wgtPixels(2); - } - - return w; + // Layout padding/spacing come from widgetStatusBarGetLayoutMetrics, which + // overrides any w->padding/spacing for metrics-bearing widgets, so no + // create-time field writes are needed. + return widgetAlloc(parent, sTypeId); } void widgetStatusBarGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) { (void)w; (void)font; - *pad = TOOLBAR_PAD; - *gap = TOOLBAR_GAP; + *pad = STATUSBAR_PAD; + *gap = STATUSBAR_GAP; *extraTop = 0; *borderW = 0; } @@ -139,7 +135,7 @@ void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; - bevel.face = 0; + bevel.face = BEVEL_NO_FILL; bevel.width = 1; drawBevel(d, ops, c->x - 1, c->y - 1, c->w + 2, c->h + 2, &bevel); } diff --git a/src/widgets/kpunch/tabControl/widgetTabControl.c b/src/widgets/kpunch/tabControl/widgetTabControl.c index 4c78250..f7bcc83 100644 --- a/src/widgets/kpunch/tabControl/widgetTabControl.c +++ b/src/widgets/kpunch/tabControl/widgetTabControl.c @@ -656,9 +656,19 @@ void widgetTabPageAccelActivate(WidgetT *w, WidgetT *root) { } if (c == w) { - tabClosePopup(); - d->activeTab = idx; - wgtInvalidate(w->parent); + if (d->activeTab != idx) { + tabClosePopup(); + d->activeTab = idx; + + // Fire the tab control's Change handler, matching the + // mouse and arrow-key tab-switch paths. + if (w->parent->onChange) { + w->parent->onChange(w->parent); + } + + wgtInvalidate(w->parent); + } + return; } diff --git a/src/widgets/kpunch/textInput/textInpt.h b/src/widgets/kpunch/textInput/textInpt.h index 4c642bf..67ee26e 100644 --- a/src/widgets/kpunch/textInput/textInpt.h +++ b/src/widgets/kpunch/textInput/textInpt.h @@ -49,11 +49,13 @@ typedef struct { void (*setUseTabChar)(WidgetT *w, bool useChar); bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); + bool (*replaceSelection)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward); void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); int32_t (*getCursorLine)(const WidgetT *w); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count); + void (*cursorToEnd)(WidgetT *w); } TextInputApiT; static inline const TextInputApiT *dvxTextInputApi(void) { @@ -75,10 +77,12 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaSetUseTabChar(w, useChar) dvxTextInputApi()->setUseTabChar(w, useChar) #define wgtTextAreaFindNext(w, needle, caseSens, fwd) dvxTextInputApi()->findNext(w, needle, caseSens, fwd) #define wgtTextAreaReplaceAll(w, needle, repl, caseSens) dvxTextInputApi()->replaceAll(w, needle, repl, caseSens) +#define wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) dvxTextInputApi()->replaceSelection(w, needle, repl, caseSens, fwd) #define wgtTextAreaSetLineDecorator(w, fn, ctx) dvxTextInputApi()->setLineDecorator(w, fn, ctx) #define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w) #define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn) #define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz) #define wgtTextAreaSetSyntaxColors(w, colors, count) dvxTextInputApi()->setSyntaxColors(w, colors, count) +#define wgtTextAreaCursorToEnd(w) dvxTextInputApi()->cursorToEnd(w) #endif // TEXTINPT_H diff --git a/src/widgets/kpunch/textInput/textinpt.dhs b/src/widgets/kpunch/textInput/textinpt.dhs index 6cc0b61..da6b1ef 100644 --- a/src/widgets/kpunch/textInput/textinpt.dhs +++ b/src/widgets/kpunch/textInput/textinpt.dhs @@ -57,6 +57,8 @@ Header: widgets/textInpt.h .index wgtTextAreaSetShowLineNumbers .index wgtTextAreaFindNext .index wgtTextAreaReplaceAll +.index wgtTextAreaReplaceSelection +.index wgtTextAreaCursorToEnd .table Function Description @@ -71,11 +73,13 @@ Header: widgets/textInpt.h 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. + void wgtTextAreaCursorToEnd(w) Move the cursor to the very end of the buffer (past the last character). .endtable .h3 API Struct (wgtRegisterApi "textinput") diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c index e1ac02c..4967af6 100644 --- a/src/widgets/kpunch/textInput/widgetTextInput.c +++ b/src/widgets/kpunch/textInput/widgetTextInput.c @@ -180,11 +180,13 @@ WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask); WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen); WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen); void wgtTextAreaAppendText(WidgetT *w, const char *text); +void wgtTextAreaCursorToEnd(WidgetT *w); bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t wgtTextAreaGetCursorLine(const WidgetT *w); int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize); void wgtTextAreaGoToLine(WidgetT *w, int32_t line); int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); +bool wgtTextAreaReplaceSelection(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward); void wgtTextAreaSetAutoIndent(WidgetT *w, bool enable); void wgtTextAreaSetCaptureTabs(WidgetT *w, bool capture); void wgtTextAreaSetColorize(WidgetT *w, void (*fn)(const char *, int32_t, uint8_t *, void *), void *ctx); @@ -516,7 +518,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)shift; // Ctrl+A -- select all - if (key == 1) { + if (key == KEY_CTRL_A) { ti->selStart = 0; ti->selEnd = maskLen; *pCur = maskLen; @@ -668,7 +670,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) { w->onChange(w); } } - } else if (key == 8) { + } else if (key == KEY_BACKSPACE) { // Backspace -- clear previous slot int32_t prev = maskPrevSlot(mask, *pCur); @@ -998,7 +1000,25 @@ void wgtTextAreaAppendText(WidgetT *w, const char *text) { ta->te.len += addLen; ta->te.buf[ta->te.len] = '\0'; textAreaDirtyCache(w); - wgtInvalidate(w); + wgtInvalidatePaint(w); +} + + +// wgtTextAreaCursorToEnd -- place the cursor at the very end of the buffer +// (offset == len), past the last character. A backward search anchored +// here scans every match including those on the final line, which a line- +// start anchor (e.g. wgtTextAreaGoToLine) would miss. +void wgtTextAreaCursorToEnd(WidgetT *w) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + + textEditOffToRowCol(&ta->te.lines, ta->te.buf, ta->te.len, ta->te.len, &ta->te.cursorRow, &ta->te.cursorCol); + ta->te.desiredCol = ta->te.cursorCol; + ta->te.selAnchor = -1; + ta->te.selCursor = -1; } @@ -1018,7 +1038,9 @@ bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, boo int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; - int32_t innerH = w->h - TEXTAREA_BORDER * 2; + int32_t maxLL = textAreaGetMaxLineLen(w); + bool needHSb = (maxLL > visCols); + int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = innerH / font->charHeight; if (visCols < 1) { @@ -1071,7 +1093,12 @@ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) { if (ctx) { const BitmapFontT *font = &ctx->font; - int32_t innerH = w->h - TEXTAREA_BORDER * 2; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; + int32_t visCols = innerW / font->charWidth; + int32_t maxLL = textAreaGetMaxLineLen(w); + bool needHSb = (maxLL > visCols); + int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); visRows = innerH / font->charHeight; if (visRows < 1) { @@ -1105,6 +1132,33 @@ int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replac } +// wgtTextAreaReplaceSelection -- if the editor's current selection +// matches needle, replace it, fire onChange, and refresh the gutter; +// then advance to the next match in the requested direction. Returns +// true if anything was replaced or a following match was found. +bool wgtTextAreaReplaceSelection(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward) { + if (!w || w->type != sTextAreaTypeId) { + return false; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + bool replaced = textEditReplaceSelectionMulti(&ta->te.lines, ta->te.buf, ta->te.bufSize, &ta->te.len, needle, replacement, caseSensitive, forward, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selAnchor, &ta->te.selCursor, ta->te.undoBuf, &ta->te.undoLen, &ta->te.undoCursor); + + if (replaced) { + ta->cachedGutterW = 0; + wgtInvalidatePaint(w); + + if (w->onChange) { + w->onChange(w); + } + } + + bool found = wgtTextAreaFindNext(w, needle, caseSensitive, forward); + + return replaced || found; +} + + void wgtTextAreaSetAutoIndent(WidgetT *w, bool enable) { if (!w || w->type != sTextAreaTypeId) { return; @@ -1230,6 +1284,16 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { } ti->undoBuf = (char *)malloc(bufSize); + + // Zero the whole undo buffer so a Ctrl+Z before any edit copies a + // defined (empty) buffer into the field instead of uninitialized + // heap (the masked path memcpy's maskLen+1 bytes from it). + if (ti->undoBuf) { + memset(ti->undoBuf, 0, bufSize); + ti->undoLen = 0; + ti->undoCursor = 0; + } + ti->selStart = -1; ti->selEnd = -1; w->weight = WGT_WEIGHT_FILL; @@ -1280,7 +1344,11 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3 int32_t gutterW = textAreaGutterWidth(w, font); int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; int32_t textY = w->y + TEXTAREA_BORDER; - int32_t visRows = (w->h - TEXTAREA_BORDER * 2) / font->charHeight; + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; + int32_t visCols = innerW / font->charWidth; + int32_t maxLL = textAreaGetMaxLineLen(w); + bool needHSb = (maxLL > visCols); + int32_t visRows = (w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0)) / font->charHeight; widgetTextEditMultiDragUpdateArea(&ta->te.lines, ta->te.buf, ta->te.len, vx, vy, textX, textY, font, &ta->te.scrollRow, ta->te.scrollCol, visRows, ta->tabWidth, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selCursor); } @@ -1643,7 +1711,8 @@ static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d TextAreaDataT *ta = (TextAreaDataT *)w->data; AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; - int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); bool needHSb = (maxLL > visCols); @@ -1855,8 +1924,6 @@ void widgetTextInputSetText(WidgetT *w, const char *text) { } -// From position pos, skip non-word chars left, then skip word chars left. -// Returns the position at the start of the word (or 0). static const WidgetClassT sClassTextInput = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE, @@ -1904,11 +1971,13 @@ static const struct { void (*setUseTabChar)(WidgetT *w, bool useChar); bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); + bool (*replaceSelection)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive, bool forward); void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); int32_t (*getCursorLine)(const WidgetT *w); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count); + void (*cursorToEnd)(WidgetT *w); } sApi = { .create = wgtTextInput, .password = wgtPasswordInput, @@ -1923,11 +1992,13 @@ static const struct { .setUseTabChar = wgtTextAreaSetUseTabChar, .findNext = wgtTextAreaFindNext, .replaceAll = wgtTextAreaReplaceAll, + .replaceSelection = wgtTextAreaReplaceSelection, .setLineDecorator = wgtTextAreaSetLineDecorator, .getCursorLine = wgtTextAreaGetCursorLine, .setGutterClick = wgtTextAreaSetGutterClickCallback, .getWordAtCursor = wgtTextAreaGetWordAtCursor, - .setSyntaxColors = wgtTextAreaSetSyntaxColors + .setSyntaxColors = wgtTextAreaSetSyntaxColors, + .cursorToEnd = wgtTextAreaCursorToEnd }; // Per-type APIs for the designer @@ -1970,9 +2041,9 @@ static const WgtIfaceT sIfaceTextInput = { static const WgtIfaceT sIfaceTextArea = { .basName = "TextArea", .props = sTextAreaProps, - .propCount = 1, + .propCount = sizeof(sTextAreaProps) / sizeof(sTextAreaProps[0]), .methods = sTextAreaMethods, - .methodCount = 11, + .methodCount = sizeof(sTextAreaMethods) / sizeof(sTextAreaMethods[0]), .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, diff --git a/src/widgets/kpunch/timer/widgetTimer.c b/src/widgets/kpunch/timer/widgetTimer.c index d82e953..fcd41b9 100644 --- a/src/widgets/kpunch/timer/widgetTimer.c +++ b/src/widgets/kpunch/timer/widgetTimer.c @@ -208,6 +208,30 @@ void wgtUpdateTimers(void) { if (w->onChange) { w->onChange(w); + + // The callback may have destroyed this timer (or its + // whole form), freeing w and d. Every destroy path + // removes the widget from sActiveTimers, so re-locate + // it: missing means freed -- stop touching it. The + // list may also have shifted under us; refresh i. + int32_t idx = -1; + + for (int32_t j = (int32_t)arrlen(sActiveTimers) - 1; j >= 0; j--) { + if (sActiveTimers[j] == w) { + idx = j; + break; + } + } + + if (idx < 0) { + if (i > (int32_t)arrlen(sActiveTimers)) { + i = (int32_t)arrlen(sActiveTimers); + } + + continue; + } + + i = idx; } if (!d->repeat) { diff --git a/src/widgets/kpunch/treeView/treeview.dep b/src/widgets/kpunch/treeView/treeview.dep index 458a3a6..147de15 100644 --- a/src/widgets/kpunch/treeView/treeview.dep +++ b/src/widgets/kpunch/treeView/treeview.dep @@ -1 +1,3 @@ -listhelp +# treeView has no load-order dependencies: it imports no listhelp (or other +# module) symbols. Kept (empty) so the build's DEPFILES copy rule still has a +# source; readDeps skips comment/blank lines. diff --git a/src/widgets/kpunch/treeView/widgetTreeView.c b/src/widgets/kpunch/treeView/widgetTreeView.c index a252cb1..3af412a 100644 --- a/src/widgets/kpunch/treeView/widgetTreeView.c +++ b/src/widgets/kpunch/treeView/widgetTreeView.c @@ -82,21 +82,22 @@ static int32_t sTreeViewTypeId = -1; static int32_t sTreeItemTypeId = -1; typedef struct { + // Fields are ordered by alignment to minimize struct padding. int32_t scrollPos; int32_t scrollPosH; WidgetT *selectedItem; WidgetT *anchorItem; - bool multiSelect; - bool reorderable; WidgetT *dragItem; WidgetT *dropTarget; - bool dropAfter; - bool dropInto; int32_t cachedTotalHeight; int32_t cachedMaxWidth; - bool dimsValid; int32_t sbDragOrient; int32_t sbDragOff; + bool multiSelect; + bool reorderable; + bool dropAfter; + bool dropInto; + bool dimsValid; } TreeViewDataT; typedef struct { @@ -135,6 +136,7 @@ static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to); static void setSelectedItem(WidgetT *treeView, WidgetT *item); static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb); static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font); +static bool treeItemInSubtree(WidgetT *item, WidgetT *candidate); static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font); static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *curY, const BitmapFontT *font); WidgetT *wgtTreeItem(WidgetT *parent, const char *text); @@ -333,7 +335,7 @@ static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, i TreeItemDataT *ti = (TreeItemDataT *)c->data; int32_t indent = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP; - int32_t textW = (int32_t)strlen(ti->text) * font->charWidth; + int32_t textW = (int32_t)(ti->text ? strlen(ti->text) : 0) * font->charWidth; int32_t itemW = indent + textW; if (itemW > maxW) { @@ -505,8 +507,9 @@ static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, // In multi-select mode, a focus rect is drawn around the cursor item // (selectedItem) to distinguish it from selected-but-not-cursor items. static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView) { - TreeViewDataT *tv = (TreeViewDataT *)treeView->data; - bool multi = tv->multiSelect; + TreeViewDataT *tv = (TreeViewDataT *)treeView->data; + bool multi = tv->multiSelect; + int32_t fillX = treeView->x + TREE_BORDER; for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != sTreeItemTypeId || !c->visible) { @@ -543,7 +546,7 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co if (isSelected) { fg = colors->menuHighlightFg; bg = colors->menuHighlightBg; - rectFill(d, ops, baseX, iy, d->clipW, font->charHeight, bg); + rectFill(d, ops, fillX, iy, d->clipW, font->charHeight, bg); } else { fg = c->fgColor ? c->fgColor : colors->contentFg; bg = c->bgColor ? c->bgColor : colors->contentBg; @@ -585,13 +588,14 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co textX += TREE_EXPAND_SIZE + TREE_ICON_GAP; } - // Draw text - drawText(d, ops, font, textX, iy, ti->text, fg, bg, isSelected); + // Draw text -- ti->text may be NULL (item created with no label) + const char *label = ti->text ? ti->text : ""; + drawText(d, ops, font, textX, iy, label, fg, bg, isSelected); // Draw focus rectangle around cursor item in multi-select mode if (multi && c == tv->selectedItem && treeView == sFocusedWidget) { uint32_t focusFg = isSelected ? colors->menuHighlightFg : colors->contentFg; - drawFocusRect(d, ops, baseX, iy, d->clipW, font->charHeight, focusFg); + drawFocusRect(d, ops, fillX, iy, d->clipW, font->charHeight, focusFg); } // Recurse into expanded children @@ -1093,10 +1097,47 @@ WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) { } +// True if item is candidate or any descendant of candidate. Used to +// detect when a removed subtree contains a tracked selection pointer. +// onChildChanged fires before wgtDestroy unlinks the child, so the +// parent chain is still intact here. +static bool treeItemInSubtree(WidgetT *item, WidgetT *candidate) { + for (WidgetT *p = item; p; p = p->parent) { + if (p == candidate) { + return true; + } + } + + return false; +} + + void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child) { - (void)child; TreeViewDataT *tv = (TreeViewDataT *)parent->data; tv->dimsValid = false; + + // A child (and its subtree) being destroyed must not stay referenced + // by the selection/drag pointers, or they dangle after the items are + // freed -- a later paint/key/mouse would dereference freed memory. + // wgtDestroy fires this before unlinking, so the subtree check is + // valid. Covers both BASIC TreeView.Clear and IDE tree rebuilds. + if (child) { + if (tv->selectedItem && treeItemInSubtree(tv->selectedItem, child)) { + tv->selectedItem = NULL; + } + + if (tv->anchorItem && treeItemInSubtree(tv->anchorItem, child)) { + tv->anchorItem = NULL; + } + + if (tv->dragItem && treeItemInSubtree(tv->dragItem, child)) { + tv->dragItem = NULL; + } + + if (tv->dropTarget && treeItemInSubtree(tv->dropTarget, child)) { + tv->dropTarget = NULL; + } + } } @@ -1290,23 +1331,6 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { cur = firstVisibleItem(w); } } - - // Check the start item itself if we wrapped all the way around - if (cur == start && sel) { - TreeItemDataT *ti = (TreeItemDataT *)start->data; - - if (ti->text && ti->text[0]) { - char first = ti->text[0]; - - if (first >= 'a' && first <= 'z') { - first -= 32; - } - - if (first == upper && cur != sel) { - setSelectedItem(w, cur); - } - } - } } } else { return; @@ -1706,6 +1730,16 @@ static void widgetTreeViewReorderDrop(WidgetT *w) { tv->dropTarget = NULL; if (!dragItem || !dropTarget || dragItem == dropTarget) { + tv->dropInto = false; + return; + } + + // Reject dropping an item onto (or into) its own subtree: reparenting + // dragItem under one of its descendants makes dragItem both an + // ancestor and a child of dropTarget -- a parent-pointer cycle that + // hangs the next tree traversal (nextVisibleItem walks parents). + if (treeItemInSubtree(dropTarget, dragItem)) { + tv->dropInto = false; return; } @@ -1811,9 +1845,12 @@ static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, in tv->dropInto = false; } } else { - // Below all items -- drop after the last item + // Below all items -- drop after the last item. Must clear + // dropInto too, or a leftover true from a prior dropInto hover + // would reparent the dragged item under the last item on release. tv->dropTarget = w->lastChild; tv->dropAfter = true; + tv->dropInto = false; } // Auto-scroll when dragging near edges