No more hard coded limits. All dynamic.

This commit is contained in:
Scott Duensing 2026-03-26 18:33:32 -05:00
parent a793941357
commit 97503080a5
20 changed files with 387 additions and 310 deletions

View file

@ -478,7 +478,7 @@ static void compositeAndFlush(AppContextT *ctx) {
// Pre-filter visible, non-minimized windows once to avoid // Pre-filter visible, non-minimized windows once to avoid
// re-checking visibility in the inner dirty-rect loop // re-checking visibility in the inner dirty-rect loop
int32_t visibleIdx[MAX_WINDOWS]; int32_t visibleIdx[ws->count > 0 ? ws->count : 1];
int32_t visibleCount = 0; int32_t visibleCount = 0;
for (int32_t j = 0; j < ws->count; j++) { for (int32_t j = 0; j < ws->count; j++) {
@ -1446,10 +1446,22 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w,
// integer compare per entry. // integer compare per entry.
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) { void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
if (!table || table->count >= MAX_ACCEL_ENTRIES) { if (!table) {
return; return;
} }
if (table->count >= table->cap) {
int32_t newCap = table->cap ? table->cap * 2 : 8;
AccelEntryT *newBuf = (AccelEntryT *)realloc(table->entries, newCap * sizeof(AccelEntryT));
if (!newBuf) {
return;
}
table->entries = newBuf;
table->cap = newCap;
}
int32_t normKey = key; int32_t normKey = key;
if (normKey >= 'a' && normKey <= 'z') { if (normKey >= 'a' && normKey <= 'z') {
@ -1877,6 +1889,10 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) {
// ============================================================ // ============================================================
void dvxFreeAccelTable(AccelTableT *table) { void dvxFreeAccelTable(AccelTableT *table) {
if (table) {
free(table->entries);
}
free(table); free(table);
} }
@ -2755,6 +2771,20 @@ void dvxShutdown(AppContextT *ctx) {
wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]); wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]);
} }
free(ctx->stack.windows);
ctx->stack.windows = NULL;
ctx->stack.count = 0;
ctx->stack.cap = 0;
free(ctx->popup.parentStack);
ctx->popup.parentStack = NULL;
ctx->popup.parentCap = 0;
free(ctx->dirty.rects);
ctx->dirty.rects = NULL;
ctx->dirty.count = 0;
ctx->dirty.cap = 0;
free(ctx->wallpaperBuf); free(ctx->wallpaperBuf);
ctx->wallpaperBuf = NULL; ctx->wallpaperBuf = NULL;
arrfree(ctx->videoModes); arrfree(ctx->videoModes);
@ -3434,8 +3464,6 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
// Pushes the current popup state onto parentStack and opens the submenu // Pushes the current popup state onto parentStack and opens the submenu
// as the new current level. The submenu is positioned at the right edge // as the new current level. The submenu is positioned at the right edge
// of the current popup, vertically aligned with the hovered item. // of the current popup, vertically aligned with the hovered item.
// MAX_SUBMENU_DEPTH prevents runaway nesting from overflowing the stack.
static void openSubMenu(AppContextT *ctx) { static void openSubMenu(AppContextT *ctx) {
if (!ctx->popup.active || !ctx->popup.menu) { if (!ctx->popup.active || !ctx->popup.menu) {
return; return;
@ -3453,10 +3481,19 @@ static void openSubMenu(AppContextT *ctx) {
return; return;
} }
if (ctx->popup.depth >= MAX_SUBMENU_DEPTH) { // Grow parent stack if needed
if (ctx->popup.depth >= ctx->popup.parentCap) {
int32_t newCap = ctx->popup.parentCap ? ctx->popup.parentCap * 2 : 4;
PopupLevelT *newBuf = (PopupLevelT *)realloc(ctx->popup.parentStack, newCap * sizeof(PopupLevelT));
if (!newBuf) {
return; return;
} }
ctx->popup.parentStack = newBuf;
ctx->popup.parentCap = newCap;
}
// Push current state to parent stack // Push current state to parent stack
PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth]; PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth];
pl->menu = ctx->popup.menu; pl->menu = ctx->popup.menu;
@ -4256,15 +4293,14 @@ static void pollKeyboard(AppContextT *ctx) {
if (win->widgetRoot) { if (win->widgetRoot) {
// Find currently focused widget // Find currently focused widget
WidgetT *current = NULL; WidgetT *current = NULL;
WidgetT *fstack[64]; WidgetT **fstack = NULL;
int32_t ftop = 0; arrput(fstack, win->widgetRoot);
fstack[ftop++] = win->widgetRoot;
while (ftop > 0) { while (arrlen(fstack) > 0) {
WidgetT *w = fstack[--ftop]; WidgetT *w = fstack[arrlen(fstack) - 1];
arrsetlen(fstack, arrlen(fstack) - 1);
if (w->focused && widgetIsFocusable(w->type)) { if (w->focused && widgetIsFocusable(w->type)) {
// Don't tab out of widgets that swallow Tab
if (w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) { if (w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) {
current = NULL; current = NULL;
break; break;
@ -4275,21 +4311,21 @@ static void pollKeyboard(AppContextT *ctx) {
} }
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible && ftop < 64) { if (c->visible) {
fstack[ftop++] = c; arrput(fstack, c);
} }
} }
} }
// Terminal swallowed Tab -- send to widget system instead // Terminal swallowed Tab -- send to widget system instead
if (current == NULL) { if (current == NULL) {
// Check if a terminal is focused arrsetlen(fstack, 0);
ftop = 0; arrput(fstack, win->widgetRoot);
fstack[ftop++] = win->widgetRoot;
bool termFocused = false; bool termFocused = false;
while (ftop > 0) { while (arrlen(fstack) > 0) {
WidgetT *w = fstack[--ftop]; WidgetT *w = fstack[arrlen(fstack) - 1];
arrsetlen(fstack, arrlen(fstack) - 1);
if (w->focused && w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) { if (w->focused && w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) {
termFocused = true; termFocused = true;
@ -4297,8 +4333,8 @@ static void pollKeyboard(AppContextT *ctx) {
} }
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible && ftop < 64) { if (c->visible) {
fstack[ftop++] = c; arrput(fstack, c);
} }
} }
} }
@ -4309,6 +4345,7 @@ static void pollKeyboard(AppContextT *ctx) {
win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags); win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags);
} }
arrfree(fstack);
continue; continue;
} }
} }
@ -4366,6 +4403,8 @@ static void pollKeyboard(AppContextT *ctx) {
wgtInvalidate(win->widgetRoot); wgtInvalidate(win->widgetRoot);
} }
arrfree(fstack);
} }
} }

View file

@ -20,7 +20,9 @@
#include "dvxComp.h" #include "dvxComp.h"
#include "dvxPlatform.h" #include "dvxPlatform.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "dvxMem.h"
// Rects within this many pixels of each other get merged even if they don't // Rects within this many pixels of each other get merged even if they don't
// overlap. A small gap tolerance absorbs jitter from mouse movement and // overlap. A small gap tolerance absorbs jitter from mouse movement and
@ -42,32 +44,24 @@ static inline void rectUnion(const RectT *a, const RectT *b, RectT *result);
// dirtyListAdd // dirtyListAdd
// ============================================================ // ============================================================
// //
// Appends a dirty rect to the list. Uses a fixed-size array (MAX_DIRTY_RECTS // Appends a dirty rect to the list. The array grows dynamically but a
// = 128) rather than a dynamic allocation -- this is called on every UI // merge pass fires at 128 entries to keep the list short. If the list
// mutation (drag, repaint, focus change) so allocation overhead must be zero. // is still long after merging (pathological scatter), everything collapses
// // into one bounding box. In practice the merge pass almost always frees
// When the list fills up, an eager merge pass tries to consolidate rects. // enough slots because GUI mutations tend to cluster spatially.
// If the list is STILL full after merging (pathological scatter), the
// nuclear option collapses everything into one bounding box. This guarantees #define DIRTY_SOFT_CAP 128
// the list never overflows, at the cost of potentially over-painting a large
// rect. In practice the merge pass almost always frees enough slots because
// GUI mutations tend to cluster spatially.
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) { void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
// Branch hint: degenerate rects are rare -- callers usually validate first
if (__builtin_expect(w <= 0 || h <= 0, 0)) { if (__builtin_expect(w <= 0 || h <= 0, 0)) {
return; return;
} }
// Overflow path: try merging, then fall back to a single bounding rect // Soft cap: merge when we accumulate too many rects to keep composite fast
if (__builtin_expect(dl->count >= MAX_DIRTY_RECTS, 0)) { if (__builtin_expect(dl->count >= DIRTY_SOFT_CAP, 0)) {
dirtyListMerge(dl); dirtyListMerge(dl);
if (dl->count >= MAX_DIRTY_RECTS) { if (dl->count >= DIRTY_SOFT_CAP) {
// Still full -- collapse the entire list plus the new rect into one
// bounding box. This is a last resort; it means the next flush will
// repaint a potentially large region, but at least we won't lose
// dirty information or crash.
RectT merged = dl->rects[0]; RectT merged = dl->rects[0];
for (int32_t i = 1; i < dl->count; i++) { for (int32_t i = 1; i < dl->count; i++) {
@ -83,6 +77,19 @@ void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
} }
} }
// Grow array if needed
if (dl->count >= dl->cap) {
int32_t newCap = dl->cap ? dl->cap * 2 : DIRTY_SOFT_CAP;
RectT *newBuf = (RectT *)realloc(dl->rects, newCap * sizeof(RectT));
if (!newBuf) {
return;
}
dl->rects = newBuf;
dl->cap = newCap;
}
dl->rects[dl->count].x = x; dl->rects[dl->count].x = x;
dl->rects[dl->count].y = y; dl->rects[dl->count].y = y;
dl->rects[dl->count].w = w; dl->rects[dl->count].w = w;

View file

@ -25,8 +25,7 @@
// Zero the dirty rect count. Called at the start of each frame. // Zero the dirty rect count. Called at the start of each frame.
void dirtyListInit(DirtyListT *dl); void dirtyListInit(DirtyListT *dl);
// Enqueue a dirty rectangle. If the list is full (MAX_DIRTY_RECTS), // Enqueue a dirty rectangle. Grows dynamically; merges at a soft cap.
// this should degrade gracefully (e.g. expand an existing rect).
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h); void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h);
// Consolidate the dirty list by merging overlapping and adjacent rects. // Consolidate the dirty list by merging overlapping and adjacent rects.

View file

@ -629,7 +629,6 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t
// listbox to distinguish them from files, following the DOS convention. // listbox to distinguish them from files, following the DOS convention.
// FD_MAX_PATH is 260 to match DOS MAX_PATH (including null terminator) // FD_MAX_PATH is 260 to match DOS MAX_PATH (including null terminator)
#define FD_MAX_ENTRIES 512
#define FD_MAX_PATH 260 #define FD_MAX_PATH 260
#define FD_NAME_LEN 64 #define FD_NAME_LEN 64
@ -642,10 +641,10 @@ typedef struct {
const FileFilterT *filters; // caller-provided filter list const FileFilterT *filters; // caller-provided filter list
int32_t filterCount; int32_t filterCount;
int32_t activeFilter; // index into filters[] int32_t activeFilter; // index into filters[]
char *entryNames[FD_MAX_ENTRIES]; // heap-allocated, freed by fdFreeEntries char **entryNames; // dynamic array of heap-allocated strings
bool entryIsDir[FD_MAX_ENTRIES]; bool *entryIsDir; // dynamic array, parallel to entryNames
int32_t entryCount; int32_t entryCount;
const char *listItems[FD_MAX_ENTRIES]; // pointers into entryNames, for listbox API const char **listItems; // dynamic array, pointers into entryNames
WidgetT *fileList; WidgetT *fileList;
WidgetT *pathInput; WidgetT *pathInput;
WidgetT *nameInput; WidgetT *nameInput;
@ -745,9 +744,14 @@ static bool fdFilterMatch(const char *name, const char *pattern) {
static void fdFreeEntries(void) { static void fdFreeEntries(void) {
for (int32_t i = 0; i < sFd.entryCount; i++) { for (int32_t i = 0; i < sFd.entryCount; i++) {
free(sFd.entryNames[i]); free(sFd.entryNames[i]);
sFd.entryNames[i] = NULL;
} }
free(sFd.entryNames);
free(sFd.entryIsDir);
free(sFd.listItems);
sFd.entryNames = NULL;
sFd.entryIsDir = NULL;
sFd.listItems = NULL;
sFd.entryCount = 0; sFd.entryCount = 0;
} }
@ -803,7 +807,7 @@ static void fdLoadDir(void) {
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(dir)) != NULL && sFd.entryCount < FD_MAX_ENTRIES) { while ((ent = readdir(dir)) != NULL) {
// Skip "." // Skip "."
if (strcmp(ent->d_name, ".") == 0) { if (strcmp(ent->d_name, ".") == 0) {
continue; continue;
@ -826,7 +830,15 @@ static void fdLoadDir(void) {
continue; continue;
} }
// Grow arrays
int32_t idx = sFd.entryCount; 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));
if (!sFd.entryNames || !sFd.entryIsDir) {
break;
}
sFd.entryIsDir[idx] = isDir; sFd.entryIsDir[idx] = isDir;
if (isDir) { if (isDir) {
@ -843,8 +855,17 @@ static void fdLoadDir(void) {
closedir(dir); closedir(dir);
if (sFd.entryCount == 0) {
wgtListBoxSetItems(sFd.fileList, NULL, 0);
return;
}
// Sort: build index array, sort, reorder // Sort: build index array, sort, reorder
int32_t sortIdx[FD_MAX_ENTRIES]; int32_t *sortIdx = (int32_t *)malloc(sFd.entryCount * sizeof(int32_t));
if (!sortIdx) {
return;
}
for (int32_t i = 0; i < sFd.entryCount; i++) { for (int32_t i = 0; i < sFd.entryCount; i++) {
sortIdx[i] = i; sortIdx[i] = i;
@ -853,9 +874,10 @@ static void fdLoadDir(void) {
qsort(sortIdx, sFd.entryCount, sizeof(int32_t), fdEntryCompare); qsort(sortIdx, sFd.entryCount, sizeof(int32_t), fdEntryCompare);
// Rebuild arrays in sorted order // Rebuild arrays in sorted order
char *tmpNames[FD_MAX_ENTRIES]; char **tmpNames = (char **)malloc(sFd.entryCount * sizeof(char *));
bool tmpIsDir[FD_MAX_ENTRIES]; bool *tmpIsDir = (bool *)malloc(sFd.entryCount * sizeof(bool));
if (tmpNames && tmpIsDir) {
for (int32_t i = 0; i < sFd.entryCount; i++) { for (int32_t i = 0; i < sFd.entryCount; i++) {
tmpNames[i] = sFd.entryNames[sortIdx[i]]; tmpNames[i] = sFd.entryNames[sortIdx[i]];
tmpIsDir[i] = sFd.entryIsDir[sortIdx[i]]; tmpIsDir[i] = sFd.entryIsDir[sortIdx[i]];
@ -863,8 +885,15 @@ static void fdLoadDir(void) {
memcpy(sFd.entryNames, tmpNames, sizeof(char *) * sFd.entryCount); memcpy(sFd.entryNames, tmpNames, sizeof(char *) * sFd.entryCount);
memcpy(sFd.entryIsDir, tmpIsDir, sizeof(bool) * sFd.entryCount); memcpy(sFd.entryIsDir, tmpIsDir, sizeof(bool) * sFd.entryCount);
}
free(sortIdx);
free(tmpNames);
free(tmpIsDir);
// Build listItems pointer array for the listbox // Build listItems pointer array for the listbox
sFd.listItems = (const char **)realloc(sFd.listItems, sFd.entryCount * sizeof(const char *));
for (int32_t i = 0; i < sFd.entryCount; i++) { for (int32_t i = 0; i < sFd.entryCount; i++) {
sFd.listItems[i] = sFd.entryNames[i]; sFd.listItems[i] = sFd.entryNames[i];
} }

View file

@ -52,7 +52,9 @@
#include "dvxDraw.h" #include "dvxDraw.h"
#include "dvxPlatform.h" #include "dvxPlatform.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "dvxMem.h"
// ============================================================ // ============================================================
// Prototypes // Prototypes

View file

@ -30,3 +30,6 @@
#include "thirdparty/stb_image.h" #include "thirdparty/stb_image.h"
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
#include <stdlib.h>
#include "dvxMem.h"

View file

@ -18,3 +18,6 @@
#include "thirdparty/stb_image_write.h" #include "thirdparty/stb_image_write.h"
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
#include <stdlib.h>
#include "dvxMem.h"

View file

@ -26,6 +26,7 @@
#ifndef DVX_MEM_H #ifndef DVX_MEM_H
#define DVX_MEM_H #define DVX_MEM_H
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
extern int32_t *dvxMemAppIdPtr; extern int32_t *dvxMemAppIdPtr;
@ -38,11 +39,10 @@ void dvxMemSnapshotLoad(int32_t appId);
uint32_t dvxMemGetAppUsage(int32_t appId); uint32_t dvxMemGetAppUsage(int32_t appId);
void dvxMemResetApp(int32_t appId); void dvxMemResetApp(int32_t appId);
// Redirect standard allocator calls to tracking wrappers. // The dvxMalloc/dvxFree functions are passthrough wrappers.
// This MUST appear after <stdlib.h> so the real prototypes exist. // Header-based per-allocation tracking was attempted but is unsafe
#define malloc(s) dvxMalloc(s) // in the DXE3 environment (loader-compiled code like stb_ds uses
#define calloc(n, s) dvxCalloc((n), (s)) // libc malloc while DXE code would use the wrapped version).
#define realloc(p, s) dvxRealloc((p), (s)) // Per-app memory is tracked via DPMI snapshots instead.
#define free(p) dvxFree(p)
#endif // DVX_MEM_H #endif // DVX_MEM_H

View file

@ -10,6 +10,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "dvxMem.h"
// stb_ds dynamic arrays (implementation lives in libtasks.a) // stb_ds dynamic arrays (implementation lives in libtasks.a)
#include "thirdparty/stb_ds.h" #include "thirdparty/stb_ds.h"

View file

@ -259,11 +259,10 @@ typedef struct {
// cache-friendly at this size. If the list fills up, the compositor // cache-friendly at this size. If the list fills up, the compositor
// can merge aggressively or fall back to a full-screen repaint. // can merge aggressively or fall back to a full-screen repaint.
#define MAX_DIRTY_RECTS 128
typedef struct { typedef struct {
RectT rects[MAX_DIRTY_RECTS]; RectT *rects; // dynamic array (realloc'd on demand)
int32_t count; int32_t count;
int32_t cap;
} DirtyListT; } DirtyListT;
// ============================================================ // ============================================================
@ -305,8 +304,6 @@ typedef struct {
// The forward declaration of MenuT is needed because MenuItemT contains // The forward declaration of MenuT is needed because MenuItemT contains
// a pointer to its parent type (circular reference between item and menu). // a pointer to its parent type (circular reference between item and menu).
#define MAX_MENU_ITEMS 16
#define MAX_MENUS 8
#define MAX_MENU_LABEL 32 #define MAX_MENU_LABEL 32
// Forward declaration for submenu pointers // Forward declaration for submenu pointers
@ -333,8 +330,9 @@ typedef struct {
// needs a forward pointer to it for cascading submenus. // needs a forward pointer to it for cascading submenus.
struct MenuT { struct MenuT {
char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File") char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File")
MenuItemT items[MAX_MENU_ITEMS]; MenuItemT *items; // dynamic array (realloc'd on demand)
int32_t itemCount; int32_t itemCount;
int32_t itemCap;
int32_t barX; // computed position on menu bar int32_t barX; // computed position on menu bar
int32_t barW; // computed width on menu bar int32_t barW; // computed width on menu bar
char accelKey; // lowercase accelerator character, 0 if none char accelKey; // lowercase accelerator character, 0 if none
@ -344,8 +342,9 @@ struct MenuT {
// menu bar is actually drawn, avoiding redundant work when multiple // menu bar is actually drawn, avoiding redundant work when multiple
// menus are added in sequence during window setup. // menus are added in sequence during window setup.
typedef struct { typedef struct {
MenuT menus[MAX_MENUS]; MenuT *menus; // dynamic array (realloc'd on demand)
int32_t menuCount; int32_t menuCount;
int32_t menuCap;
int32_t activeIdx; // menu bar item with open popup (-1 = none) int32_t activeIdx; // menu bar item with open popup (-1 = none)
bool positionsDirty; // true = barX/barW need recomputation bool positionsDirty; // true = barX/barW need recomputation
} MenuBarT; } MenuBarT;
@ -394,7 +393,6 @@ typedef struct {
// match in O(n) with just two integer comparisons per entry, avoiding // match in O(n) with just two integer comparisons per entry, avoiding
// case-folding and modifier normalization on every keystroke. // case-folding and modifier normalization on every keystroke.
#define MAX_ACCEL_ENTRIES 32
// Modifier flags for accelerators (match BIOS shift state bits) // Modifier flags for accelerators (match BIOS shift state bits)
#define ACCEL_SHIFT 0x03 #define ACCEL_SHIFT 0x03
@ -431,8 +429,9 @@ typedef struct {
} AccelEntryT; } AccelEntryT;
typedef struct { typedef struct {
AccelEntryT entries[MAX_ACCEL_ENTRIES]; AccelEntryT *entries; // dynamic array (realloc'd on demand)
int32_t count; int32_t count;
int32_t cap;
} AccelTableT; } AccelTableT;
// ============================================================ // ============================================================
@ -453,7 +452,6 @@ typedef struct {
// RESIZE_xxx flags are bitfields so that corner resizes can combine two // RESIZE_xxx flags are bitfields so that corner resizes can combine two
// edges (e.g. RESIZE_LEFT | RESIZE_TOP for the top-left corner). // edges (e.g. RESIZE_LEFT | RESIZE_TOP for the top-left corner).
#define MAX_WINDOWS 64
#define MAX_TITLE_LEN 128 #define MAX_TITLE_LEN 128
#define RESIZE_NONE 0 #define RESIZE_NONE 0
@ -577,8 +575,9 @@ typedef struct WindowT {
// you can't drag two windows simultaneously with a single mouse. // you can't drag two windows simultaneously with a single mouse.
typedef struct { typedef struct {
WindowT *windows[MAX_WINDOWS]; WindowT **windows; // dynamic array (realloc'd on demand)
int32_t count; int32_t count;
int32_t cap;
int32_t focusedIdx; int32_t focusedIdx;
int32_t dragWindow; int32_t dragWindow;
int32_t dragOffX; int32_t dragOffX;
@ -629,15 +628,14 @@ typedef struct {
// Only one popup chain can be active at a time (menus are modal). // Only one popup chain can be active at a time (menus are modal).
// Cascading submenus are tracked as a stack of PopupLevelT frames, // Cascading submenus are tracked as a stack of PopupLevelT frames,
// where each level saves the parent menu's state so it can be restored // where each level saves the parent menu's state so it can be restored
// when the submenu closes. MAX_SUBMENU_DEPTH of 4 allows File > Recent > // when the submenu closes. The stack grows dynamically to support
// Category > Item style nesting, which is deeper than any sane DOS UI needs. // arbitrary nesting depth.
// //
// The popup's screen coordinates are stored directly rather than computed // The popup's screen coordinates are stored directly rather than computed
// relative to the window, because the popup is painted on top of // relative to the window, because the popup is painted on top of
// everything during the compositor's overlay pass and needs absolute // everything during the compositor's overlay pass and needs absolute
// screen positioning. // screen positioning.
#define MAX_SUBMENU_DEPTH 4
// Saved parent popup state when a submenu is open // Saved parent popup state when a submenu is open
typedef struct { typedef struct {
@ -662,7 +660,8 @@ typedef struct {
int32_t hoverItem; // highlighted item in current popup (-1 = none) int32_t hoverItem; // highlighted item in current popup (-1 = none)
MenuT *menu; // direct pointer to current menu (avoids lookup for submenus) MenuT *menu; // direct pointer to current menu (avoids lookup for submenus)
int32_t depth; // 0 = top-level only, 1+ = submenu depth int32_t depth; // 0 = top-level only, 1+ = submenu depth
PopupLevelT parentStack[MAX_SUBMENU_DEPTH]; PopupLevelT *parentStack; // dynamic array (realloc'd on demand)
int32_t parentCap;
} PopupStateT; } PopupStateT;
// ============================================================ // ============================================================

View file

@ -45,7 +45,9 @@
#include "dvxPlatform.h" #include "dvxPlatform.h"
#include "dvxPalette.h" #include "dvxPalette.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "dvxMem.h"
// ============================================================ // ============================================================

View file

@ -11,7 +11,7 @@
// = front). This was chosen over a linked list because hit-testing walks // = front). This was chosen over a linked list because hit-testing walks
// the stack front-to-back every mouse event, and array iteration has // the stack front-to-back every mouse event, and array iteration has
// better cache behavior on 486/Pentium. Raising a window is O(N) shift // better cache behavior on 486/Pentium. Raising a window is O(N) shift
// but N is bounded by MAX_WINDOWS=64 and raise is infrequent. // but N is bounded by the window count and raise is infrequent.
// //
// - Window chrome uses a Motif/GEOS Ensemble visual style with fixed 4px // - Window chrome uses a Motif/GEOS Ensemble visual style with fixed 4px
// outer bevels and 2px inner bevels. The fixed bevel widths avoid per- // outer bevels and 2px inner bevels. The fixed bevel widths avoid per-
@ -39,6 +39,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include "dvxMem.h"
// ============================================================ // ============================================================
// Constants // Constants
@ -712,6 +713,11 @@ static void freeMenuRecursive(MenuT *menu) {
menu->items[i].subMenu = NULL; menu->items[i].subMenu = NULL;
} }
} }
free(menu->items);
menu->items = NULL;
menu->itemCount = 0;
menu->itemCap = 0;
} }
@ -861,7 +867,7 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// ============================================================ // ============================================================
// //
// Adds a top-level menu to a window's menu bar. The MenuT struct is // Adds a top-level menu to a window's menu bar. The MenuT struct is
// stored inline in the MenuBarT's fixed-size array (MAX_MENUS=8) rather // stored inline in the MenuBarT's dynamic array rather
// than heap-allocated, since menus are created once at window setup and // 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 // the count is small. The positionsDirty flag is set so that the next
// paint triggers computeMenuBarPositions to lay out all labels. // paint triggers computeMenuBarPositions to lay out all labels.
@ -871,11 +877,37 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// stored on the MenuT so the event loop can match keyboard shortcuts // stored on the MenuT so the event loop can match keyboard shortcuts
// without re-parsing the label string on every keypress. // without re-parsing the label string on every keypress.
static bool menuGrowItems(MenuT *menu) {
if (menu->itemCount < menu->itemCap) {
return true;
}
int32_t newCap = menu->itemCap ? menu->itemCap * 2 : 8;
MenuItemT *newBuf = (MenuItemT *)realloc(menu->items, newCap * sizeof(MenuItemT));
if (!newBuf) {
return false;
}
menu->items = newBuf;
menu->itemCap = newCap;
return true;
}
MenuT *wmAddMenu(MenuBarT *bar, const char *label) { MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
if (bar->menuCount >= MAX_MENUS) { if (bar->menuCount >= bar->menuCap) {
int32_t newCap = bar->menuCap ? bar->menuCap * 2 : 8;
MenuT *newBuf = (MenuT *)realloc(bar->menus, newCap * sizeof(MenuT));
if (!newBuf) {
return NULL; return NULL;
} }
bar->menus = newBuf;
bar->menuCap = newCap;
}
MenuT *menu = &bar->menus[bar->menuCount]; MenuT *menu = &bar->menus[bar->menuCount];
memset(menu, 0, sizeof(*menu)); memset(menu, 0, sizeof(*menu));
strncpy(menu->label, label, MAX_MENU_LABEL - 1); strncpy(menu->label, label, MAX_MENU_LABEL - 1);
@ -895,7 +927,7 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
// Allocates and attaches a menu bar to a window. The menu bar is heap- // Allocates and attaches a menu bar to a window. The menu bar is heap-
// allocated separately from the window because most windows don't have // allocated separately from the window because most windows don't have
// one, and keeping it out of WindowT saves ~550 bytes per window // one, and keeping it out of WindowT saves ~550 bytes per window
// (MAX_MENUS * sizeof(MenuT)). wmUpdateContentRect is called to shrink // wmUpdateContentRect is called to shrink
// the content area by CHROME_MENU_HEIGHT to make room for the bar. // the content area by CHROME_MENU_HEIGHT to make room for the bar.
MenuBarT *wmAddMenuBar(WindowT *win) { MenuBarT *wmAddMenuBar(WindowT *win) {
@ -918,7 +950,7 @@ MenuBarT *wmAddMenuBar(WindowT *win) {
// ============================================================ // ============================================================
void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) { void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) {
if (menu->itemCount >= MAX_MENU_ITEMS) { if (!menuGrowItems(menu)) {
return; return;
} }
@ -940,7 +972,7 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) {
// ============================================================ // ============================================================
void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) { void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) {
if (menu->itemCount >= MAX_MENU_ITEMS) { if (!menuGrowItems(menu)) {
return; return;
} }
@ -962,7 +994,7 @@ void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked
// ============================================================ // ============================================================
void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) { void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) {
if (menu->itemCount >= MAX_MENU_ITEMS) { if (!menuGrowItems(menu)) {
return; return;
} }
@ -984,7 +1016,7 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked
// ============================================================ // ============================================================
void wmAddMenuSeparator(MenuT *menu) { void wmAddMenuSeparator(MenuT *menu) {
if (menu->itemCount >= MAX_MENU_ITEMS) { if (!menuGrowItems(menu)) {
return; return;
} }
@ -1007,7 +1039,7 @@ void wmAddMenuSeparator(MenuT *menu) {
// item opens the child rather than firing a command. // item opens the child rather than firing a command.
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) { MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) {
if (parentMenu->itemCount >= MAX_MENU_ITEMS) { if (!menuGrowItems(parentMenu)) {
return NULL; return NULL;
} }
@ -1084,11 +1116,19 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
// and wmRaiseWindow if desired. // and wmRaiseWindow if desired.
WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
if (stack->count >= MAX_WINDOWS) { if (stack->count >= stack->cap) {
fprintf(stderr, "WM: Maximum windows (%d) reached\n", MAX_WINDOWS); int32_t newCap = stack->cap ? stack->cap * 2 : 16;
WindowT **newBuf = (WindowT **)realloc(stack->windows, newCap * sizeof(WindowT *));
if (!newBuf) {
fprintf(stderr, "WM: Failed to grow window stack\n");
return NULL; return NULL;
} }
stack->windows = newBuf;
stack->cap = newCap;
}
WindowT *win = (WindowT *)malloc(sizeof(WindowT)); WindowT *win = (WindowT *)malloc(sizeof(WindowT));
if (!win) { if (!win) {
@ -1188,6 +1228,7 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
freeMenuRecursive(&win->menuBar->menus[i]); freeMenuRecursive(&win->menuBar->menus[i]);
} }
free(win->menuBar->menus);
free(win->menuBar); free(win->menuBar);
} }
@ -2626,6 +2667,7 @@ void wmFreeMenu(MenuT *menu) {
} }
} }
free(menu->items);
free(menu); free(menu);
} }

View file

@ -80,7 +80,7 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked
void wmAddMenuSeparator(MenuT *menu); void wmAddMenuSeparator(MenuT *menu);
// Create a cascading submenu attached to the parent menu. Returns the // Create a cascading submenu attached to the parent menu. Returns the
// child MenuT to populate, or NULL if MAX_MENU_ITEMS is exhausted. // child MenuT to populate, or NULL on allocation failure.
// The child MenuT is heap-allocated and freed when the parent window // The child MenuT is heap-allocated and freed when the parent window
// is destroyed. // is destroyed.
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label); MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label);

View file

@ -1113,147 +1113,91 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) {
// ============================================================ // ============================================================
// Per-app memory tracking (header-based) // Per-app memory tracking (DPMI snapshot)
// ============================================================ // ============================================================
// //
// Every DXE .c file includes dvxMem.h which #defines malloc/free/ // Tracks per-app memory by taking DPMI free-memory snapshots at
// calloc/realloc to dvxMalloc/dvxFree/dvxCalloc/dvxRealloc. This // app load time. The difference between the snapshot and current
// file does NOT include dvxMem.h, so calls to malloc/free here go // free memory is a coarse estimate of the app's heap footprint.
// directly to libc with no recursion.
// //
// Each tracked allocation has a 16-byte header prepended. dvxFree // Header-based malloc wrapping (prepending a tracking header to
// checks the magic before adjusting the pointer -- if it doesn't // each allocation) was attempted but is unsafe in the DJGPP/DXE3
// match (pointer came from libc internals or loader code), it // environment: loader-compiled code (stb_ds internals, libc
// falls through to the real free() unchanged. // functions like strdup/localtime) allocates with libc's malloc,
// but DXE code frees with the wrapped free. Reading 16 bytes
#define DVX_ALLOC_MAGIC 0xDEADBEEFUL // before an arbitrary libc pointer to check for a tracking magic
// value is undefined behavior that causes faults on 86Box/DPMI.
typedef struct { //
uint32_t magic; // dvxMalloc/dvxFree/etc. are still exported for dvxMem.h compat
int32_t appId; // but pass straight through to libc.
uint32_t size;
uint32_t pad;
} DvxAllocHeaderT;
int32_t *dvxMemAppIdPtr = NULL; int32_t *dvxMemAppIdPtr = NULL;
static uint32_t *sAppMemUsed = NULL; static uint32_t *sAppMemAtLoad = NULL;
static int32_t sAppMemCap = 0; static int32_t sAppMemCap = 0;
// Passthrough allocators (dvxMem.h #defines redirect here)
void *dvxMalloc(size_t size) {
return malloc(size);
}
void *dvxCalloc(size_t nmemb, size_t size) {
return calloc(nmemb, size);
}
void dvxFree(void *ptr) {
free(ptr);
}
void *dvxRealloc(void *ptr, size_t size) {
return realloc(ptr, size);
}
static uint32_t dpmiGetFreeKb(void) {
__dpmi_free_mem_info memInfo;
if (__dpmi_get_free_memory_information(&memInfo) != 0) {
return 0;
}
if (memInfo.total_number_of_free_pages == 0xFFFFFFFFUL) {
return 0;
}
return memInfo.total_number_of_free_pages * 4;
}
static void dvxMemGrow(int32_t appId) { static void dvxMemGrow(int32_t appId) {
if (appId < sAppMemCap) { if (appId < sAppMemCap) {
return; return;
} }
int32_t newCap = appId + 16; int32_t newCap = appId + 16;
uint32_t *newArr = (uint32_t *)realloc(sAppMemUsed, newCap * sizeof(uint32_t)); uint32_t *newArr = (uint32_t *)realloc(sAppMemAtLoad, newCap * sizeof(uint32_t));
if (!newArr) { if (!newArr) {
return; return;
} }
memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t)); memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t));
sAppMemUsed = newArr; sAppMemAtLoad = newArr;
sAppMemCap = newCap; sAppMemCap = newCap;
} }
void *dvxMalloc(size_t size) { void dvxMemSnapshotLoad(int32_t appId) {
int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)malloc(sizeof(DvxAllocHeaderT) + size);
if (!hdr) {
return NULL;
}
hdr->magic = DVX_ALLOC_MAGIC;
hdr->appId = appId;
hdr->size = (uint32_t)size;
hdr->pad = 0;
if (appId >= 0) { if (appId >= 0) {
dvxMemGrow(appId); dvxMemGrow(appId);
if (appId < sAppMemCap) { sAppMemUsed[appId] += (uint32_t)size; }
if (appId < sAppMemCap) {
sAppMemAtLoad[appId] = dpmiGetFreeKb();
} }
return hdr + 1;
}
void *dvxCalloc(size_t nmemb, size_t size) {
size_t total = nmemb * size;
void *ptr = dvxMalloc(total);
if (ptr) {
memset(ptr, 0, total);
} }
return ptr;
}
void dvxFree(void *ptr) {
if (!ptr) {
return;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1;
if (hdr->magic != DVX_ALLOC_MAGIC) {
// Not a tracked allocation (libc, stb_ds, loader) -- pass through
free(ptr);
return;
}
int32_t appId = hdr->appId;
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] -= hdr->size;
}
hdr->magic = 0;
free(hdr);
}
void *dvxRealloc(void *ptr, size_t size) {
if (!ptr) {
return dvxMalloc(size);
}
if (size == 0) {
dvxFree(ptr);
return NULL;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1;
if (hdr->magic != DVX_ALLOC_MAGIC) {
// Not tracked -- pass through
return realloc(ptr, size);
}
int32_t appId = hdr->appId;
uint32_t oldSize = hdr->size;
DvxAllocHeaderT *newHdr = (DvxAllocHeaderT *)realloc(hdr, sizeof(DvxAllocHeaderT) + size);
if (!newHdr) {
return NULL;
}
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] -= oldSize;
sAppMemUsed[appId] += (uint32_t)size;
}
newHdr->size = (uint32_t)size;
return newHdr + 1;
}
void dvxMemSnapshotLoad(int32_t appId) {
(void)appId;
} }
@ -1262,13 +1206,25 @@ uint32_t dvxMemGetAppUsage(int32_t appId) {
return 0; return 0;
} }
return sAppMemUsed[appId]; uint32_t atLoad = sAppMemAtLoad[appId];
if (atLoad == 0) {
return 0;
}
uint32_t nowFree = dpmiGetFreeKb();
if (nowFree >= atLoad) {
return 0;
}
return (atLoad - nowFree) * 1024;
} }
void dvxMemResetApp(int32_t appId) { void dvxMemResetApp(int32_t appId) {
if (appId >= 0 && appId < sAppMemCap) { if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] = 0; sAppMemAtLoad[appId] = 0;
} }
} }

View file

@ -49,9 +49,9 @@ WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
WidgetT *sDragWidget = NULL; // widget being dragged (any drag type) WidgetT *sDragWidget = NULL; // widget being dragged (any drag type)
// Shared clipboard -- process-wide, not per-widget. // Shared clipboard -- process-wide, not per-widget.
#define CLIPBOARD_MAX 4096 static char *sClipboard = NULL;
static char sClipboard[CLIPBOARD_MAX];
static int32_t sClipboardLen = 0; static int32_t sClipboardLen = 0;
static int32_t sClipboardCap = 0;
// Multi-click state (used by widgetEvent.c for universal dbl-click detection) // Multi-click state (used by widgetEvent.c for universal dbl-click detection)
static clock_t sLastClickTime = 0; static clock_t sLastClickTime = 0;
@ -69,8 +69,16 @@ void clipboardCopy(const char *text, int32_t len) {
return; return;
} }
if (len > CLIPBOARD_MAX - 1) { if (len + 1 > sClipboardCap) {
len = CLIPBOARD_MAX - 1; int32_t newCap = len + 1;
char *newBuf = (char *)realloc(sClipboard, newCap);
if (!newBuf) {
return;
}
sClipboard = newBuf;
sClipboardCap = newCap;
} }
memcpy(sClipboard, text, len); memcpy(sClipboard, text, len);
@ -97,7 +105,7 @@ const char *clipboardGet(int32_t *outLen) {
// ============================================================ // ============================================================
int32_t clipboardMaxLen(void) { int32_t clipboardMaxLen(void) {
return CLIPBOARD_MAX - 1; return 65536;
} }
@ -378,59 +386,47 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
// ============================================================ // ============================================================
// //
// Shift+Tab navigation: finds the previous focusable widget. // Shift+Tab navigation: finds the previous focusable widget.
// Unlike findNextFocusable which can short-circuit during traversal, // Collects all focusable widgets via DFS, then returns the one
// finding the PREVIOUS widget requires knowing the full order. // before 'before' (with wraparound). Uses stb_ds dynamic arrays
// So this collects all focusable widgets into an array, finds the // so there's no fixed limit on widget count or tree depth.
// target's index, and returns index-1 (with wraparound).
//
// The explicit stack-based DFS (rather than recursion) is used here
// because we need to push children in reverse order to get the same
// left-to-right depth-first ordering as the recursive version.
// Fixed-size arrays (128 widgets, 64 stack depth) are adequate for
// any reasonable dialog layout and avoid dynamic allocation.
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) { WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
WidgetT *list[128]; WidgetT **list = NULL;
int32_t count = 0; WidgetT **stack = NULL;
// Collect all focusable widgets via depth-first traversal arrput(stack, root);
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
while (top > 0) { while (arrlen(stack) > 0) {
WidgetT *w = stack[--top]; WidgetT *w = stack[arrlen(stack) - 1];
arrsetlen(stack, arrlen(stack) - 1);
if (!w->visible || !w->enabled) { if (!w->visible || !w->enabled) {
continue; continue;
} }
if (widgetIsFocusable(w->type) && count < 128) { if (widgetIsFocusable(w->type)) {
list[count++] = w; arrput(list, w);
} }
// Push children in reverse order so first child is processed first // Push children in reverse order so first child is processed first
WidgetT *children[64]; // Walk to end of sibling list, then push backwards
int32_t childCount = 0; WidgetT **children = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (childCount < 64) { arrput(children, c);
children[childCount++] = c;
}
} }
for (int32_t i = childCount - 1; i >= 0; i--) { for (int32_t i = arrlen(children) - 1; i >= 0; i--) {
if (top < 64) { arrput(stack, children[i]);
stack[top++] = children[i];
}
}
} }
if (count == 0) { arrfree(children);
return NULL;
} }
// Find 'before' in the list WidgetT *result = NULL;
int32_t count = arrlen(list);
if (count > 0) {
int32_t idx = -1; int32_t idx = -1;
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
@ -440,11 +436,12 @@ WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
} }
} }
if (idx <= 0) { result = (idx <= 0) ? list[count - 1] : list[idx - 1];
return list[count - 1]; // Wrap to last
} }
return list[idx - 1]; arrfree(list);
arrfree(stack);
return result;
} }

View file

@ -172,14 +172,8 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return; return;
} }
// Attribute allocations during event handling to the owning app // Dispatch to per-widget onKey handler via vtable
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
wclsOnKey(focus, key, mod); wclsOnKey(focus, key, mod);
ctx->currentAppId = prevAppId;
} }
@ -201,8 +195,6 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
// are also in content-buffer space (set during layout), so no // are also in content-buffer space (set during layout), so no
// coordinate transform is needed for hit testing. // coordinate transform is needed for hit testing.
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons);
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
WidgetT *root = win->widgetRoot; WidgetT *root = win->widgetRoot;
sClosedPopup = NULL; sClosedPopup = NULL;
@ -210,19 +202,6 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
if (!root) { if (!root) {
return; return;
} }
// Attribute allocations during event handling to the owning app
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
widgetOnMouseInner(win, root, x, y, buttons);
ctx->currentAppId = prevAppId;
}
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) {
// Close popups from other windows // Close popups from other windows
if (sOpenPopup && sOpenPopup->window != win) { if (sOpenPopup && sOpenPopup->window != win) {
wclsClosePopup(sOpenPopup); wclsClosePopup(sOpenPopup);

View file

@ -82,11 +82,15 @@ static int32_t allocSlot(void) {
// this function never completes -- the task is killed externally via // this function never completes -- the task is killed externally via
// shellForceKillApp + tsKill. // shellForceKillApp + tsKill.
static void appTaskWrapper(void *arg) { static void appTaskWrapper(void *arg) {
ShellAppT *app = (ShellAppT *)arg; // Look up the app by ID, not by pointer. The sApps array may have
// been reallocated between tsCreate and the first time this task runs
app->dxeCtx.shellCtx->currentAppId = app->appId; // (e.g. another app was loaded in the meantime), which would
app->entryFn(&app->dxeCtx); // invalidate a direct ShellAppT pointer.
app->dxeCtx.shellCtx->currentAppId = 0; int32_t appId = (int32_t)(intptr_t)arg;
ShellAppT *app = &sApps[appId];
app->dxeCtx->shellCtx->currentAppId = app->appId;
app->entryFn(app->dxeCtx);
app->dxeCtx->shellCtx->currentAppId = 0;
// App returned from its main loop -- mark for reaping // App returned from its main loop -- mark for reaping
app->state = AppStateTerminatingE; app->state = AppStateTerminatingE;
@ -261,6 +265,8 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
} }
cleanupTempFile(app); cleanupTempFile(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId); dvxMemResetApp(app->appId);
app->state = AppStateFreeE; app->state = AppStateFreeE;
dvxLog("Shell: force-killed app '%s'", app->name); dvxLog("Shell: force-killed app '%s'", app->name);
@ -399,29 +405,39 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
app->shutdownFn = shutdown; app->shutdownFn = shutdown;
app->state = AppStateLoadedE; app->state = AppStateLoadedE;
// Set up the context passed to appMain // Heap-allocate the DxeAppContextT so its address is stable across
app->dxeCtx.shellCtx = ctx; // sApps reallocs. Apps save this pointer in their static globals.
app->dxeCtx.appId = id; app->dxeCtx = (DxeAppContextT *)calloc(1, sizeof(DxeAppContextT));
if (!app->dxeCtx) {
ctx->currentAppId = 0;
dlclose(handle);
app->state = AppStateFreeE;
return -1;
}
app->dxeCtx->shellCtx = ctx;
app->dxeCtx->appId = id;
// Derive app directory from path (everything up to the last separator). // Derive app directory from path (everything up to the last separator).
// This lets apps load resources relative to their own location rather // This lets apps load resources relative to their own location rather
// than the shell's working directory. // than the shell's working directory.
snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path); snprintf(app->dxeCtx->appDir, sizeof(app->dxeCtx->appDir), "%s", path);
char *sep = platformPathDirEnd(app->dxeCtx.appDir); char *sep = platformPathDirEnd(app->dxeCtx->appDir);
if (sep) { if (sep) {
*sep = '\0'; *sep = '\0';
} else { } else {
app->dxeCtx.appDir[0] = '.'; app->dxeCtx->appDir[0] = '.';
app->dxeCtx.appDir[1] = '\0'; app->dxeCtx->appDir[1] = '\0';
} }
// Derive config directory: replace the APPS/ prefix with CONFIG/. // Derive config directory: replace the APPS/ prefix with CONFIG/.
// e.g. "APPS/GAMES/TETRIS" -> "CONFIG/GAMES/TETRIS" // e.g. "APPS/GAMES/TETRIS" -> "CONFIG/GAMES/TETRIS"
// If the path doesn't start with "apps/" or "APPS/", fall back to // If the path doesn't start with "apps/" or "APPS/", fall back to
// "CONFIG/<appname>" using the descriptor name. // "CONFIG/<appname>" using the descriptor name.
const char *appDirStr = app->dxeCtx.appDir; const char *appDirStr = app->dxeCtx->appDir;
const char *appsPrefix = NULL; const char *appsPrefix = NULL;
if (strncasecmp(appDirStr, "apps/", 5) == 0 || strncasecmp(appDirStr, "apps\\", 5) == 0) { if (strncasecmp(appDirStr, "apps/", 5) == 0 || strncasecmp(appDirStr, "apps\\", 5) == 0) {
@ -431,9 +447,9 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
} }
if (appsPrefix && appsPrefix[0]) { if (appsPrefix && appsPrefix[0]) {
snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", appsPrefix); snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", appsPrefix);
} else { } else {
snprintf(app->dxeCtx.configDir, sizeof(app->dxeCtx.configDir), "CONFIG/%s", desc->name); snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", desc->name);
} }
// Launch. Set currentAppId before any app code runs so that // Launch. Set currentAppId before any app code runs so that
@ -444,12 +460,14 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE; uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE;
int32_t priority = desc->priority; int32_t priority = desc->priority;
int32_t taskId = tsCreate(desc->name, appTaskWrapper, app, stackSize, priority); int32_t taskId = tsCreate(desc->name, appTaskWrapper, (void *)(intptr_t)id, stackSize, priority);
if (taskId < 0) { if (taskId < 0) {
ctx->currentAppId = 0; ctx->currentAppId = 0;
dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR); dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR);
dlclose(handle); dlclose(handle);
free(app->dxeCtx);
app->dxeCtx = NULL;
app->state = AppStateFreeE; app->state = AppStateFreeE;
return -1; return -1;
} }
@ -460,7 +478,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
// The app creates its windows and returns. From this point on, // The app creates its windows and returns. From this point on,
// the app lives entirely through event callbacks dispatched by // the app lives entirely through event callbacks dispatched by
// the shell's dvxUpdate loop. No separate task or stack needed. // the shell's dvxUpdate loop. No separate task or stack needed.
app->entryFn(&app->dxeCtx); app->entryFn(app->dxeCtx);
} }
ctx->currentAppId = 0; ctx->currentAppId = 0;
@ -511,6 +529,8 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
} }
cleanupTempFile(app); cleanupTempFile(app);
free(app->dxeCtx);
app->dxeCtx = NULL;
dvxMemResetApp(app->appId); dvxMemResetApp(app->appId);
dvxLog("Shell: reaped app '%s'", app->name); dvxLog("Shell: reaped app '%s'", app->name);
app->state = AppStateFreeE; app->state = AppStateFreeE;

View file

@ -93,7 +93,7 @@ typedef struct {
uint32_t mainTaskId; // task ID if hasMainLoop, else 0 uint32_t mainTaskId; // task ID if hasMainLoop, else 0
int32_t (*entryFn)(DxeAppContextT *); int32_t (*entryFn)(DxeAppContextT *);
void (*shutdownFn)(void); // may be NULL void (*shutdownFn)(void); // may be NULL
DxeAppContextT dxeCtx; // context passed to appMain DxeAppContextT *dxeCtx; // heap-allocated; address stable across sApps realloc
} ShellAppT; } ShellAppT;
// ============================================================ // ============================================================

View file

@ -374,7 +374,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
// Ctrl+Z -- undo // Ctrl+Z -- undo
if (key == 26 && undoBuf && pUndoLen && pUndoCursor) { if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
// Swap current and undo // Swap current and undo
char tmpBuf[clipboardMaxLen() + 1]; char tmpBuf[*pLen + 1];
int32_t tmpLen = *pLen; int32_t tmpLen = *pLen;
int32_t tmpCursor = *pCursor; int32_t tmpCursor = *pCursor;
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1; int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;

View file

@ -111,7 +111,6 @@ typedef struct {
#define TEXTAREA_SB_W 14 #define TEXTAREA_SB_W 14
#define TEXTAREA_MIN_ROWS 4 #define TEXTAREA_MIN_ROWS 4
#define TEXTAREA_MIN_COLS 20 #define TEXTAREA_MIN_COLS 20
#define CLIPBOARD_MAX 4096
// Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c) // Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c)
#define CURSOR_BLINK_MS 250 #define CURSOR_BLINK_MS 250
@ -950,7 +949,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == 26) { if (key == 26) {
if (ta->undoBuf && ta->undoLen >= 0) { if (ta->undoBuf && ta->undoLen >= 0) {
// Swap current and undo // Swap current and undo
char tmpBuf[CLIPBOARD_MAX]; char tmpBuf[*pLen + 1];
int32_t tmpLen = *pLen; int32_t tmpLen = *pLen;
int32_t tmpCursor = CUR_OFF(); int32_t tmpCursor = CUR_OFF();
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1; int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;