From bf610ba95b8f9ae9efb5472008c787b0b03ac792 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 26 Mar 2026 21:15:20 -0500 Subject: [PATCH] Code and docs cleanup. --- .gitignore | 1 + README.md | 35 +- core/README.md | 251 ++- core/dvxApp.c | 3682 +++++++++++++++++++++++---------------------- loader/README.md | 25 +- shell/README.md | 62 +- taskmgr/README.md | 75 + tools/README.md | 117 ++ widgets/README.md | 41 +- 9 files changed, 2372 insertions(+), 1917 deletions(-) create mode 100644 taskmgr/README.md create mode 100644 tools/README.md diff --git a/.gitignore b/.gitignore index 4a62d30..defbee0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ lib/ .gitignore~ .gitattributes~ *.SWP +.claude/ diff --git a/README.md b/README.md index b4ff93d..2c7f7ba 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ dvx.exe (loader) +-- libs/libdvx.lib core GUI (draw, comp, wm, app, widget infra) +-- libs/texthelp.lib shared text editing helpers +-- libs/listhelp.lib shared list/dropdown helpers - +-- libs/dvxshell.lib shell (app lifecycle, desktop, task manager) + +-- libs/dvxshell.lib shell (app lifecycle, desktop) + +-- libs/taskmgr.lib task manager (Ctrl+Esc, separate DXE) | +-- widgets/*.wgt 26 widget type plugins | @@ -37,9 +38,11 @@ dvx.exe (loader) | `tasks/` | `bin/libs/libtasks.lib` | Cooperative task switching | | `texthelp/` | `bin/libs/texthelp.lib` | Shared text editing helpers | | `listhelp/` | `bin/libs/listhelp.lib` | Shared list/dropdown helpers | -| `shell/` | `bin/libs/dvxshell.lib` | DVX Shell (app lifecycle, task manager) | +| `shell/` | `bin/libs/dvxshell.lib` | DVX Shell (app lifecycle, desktop) | +| `taskmgr/` | `bin/libs/taskmgr.lib` | Task Manager (separate DXE, Ctrl+Esc) | | `widgets/` | `bin/widgets/*.wgt` | 26 individual widget DXE modules | | `apps/` | `bin/apps/*/*.app` | Application DXE modules | +| `tools/` | `bin/dvxres` | Resource tool (host native, not DXE) | | `config/` | `bin/config/`, `bin/libs/`, `bin/widgets/` | INI config, themes, wallpapers, dep files | | `rs232/` | `lib/librs232.a` | ISR-driven UART serial driver | | `packet/` | `lib/libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ | @@ -62,7 +65,8 @@ make clean # remove all build artifacts The top-level Makefile builds in dependency order: ``` -core -> tasks -> loader -> texthelp -> listhelp -> widgets -> shell -> apps +core -> tasks -> loader -> texthelp -> listhelp -> widgets -> shell -> taskmgr -> apps + tools (host native, parallel) ``` Build output goes to `bin/` (executables, DXE modules, config) and @@ -80,6 +84,7 @@ bin/ texthelp.lib text editing helpers listhelp.lib list/dropdown helpers dvxshell.lib DVX shell + taskmgr.lib task manager (Ctrl+Esc) *.dep dependency files for load ordering widgets/ box.wgt VBox/HBox/Frame containers @@ -117,9 +122,17 @@ dvxWidget.h. * **Dynamic type IDs**: `wgtRegisterClass()` assigns IDs at load time * **void *data**: Each widget allocates its own private data struct +* **ABI-stable dispatch**: `WidgetClassT.handlers[]` is an array of function + pointers indexed by `WGT_METHOD_*` constants (0-20, room for 32). Core + dispatches via `w->wclass->handlers[WGT_METHOD_PAINT]` etc., so adding + new methods does not break existing widget DXE binaries +* **Generic drag**: `WGT_METHOD_ON_DRAG_UPDATE` and `WGT_METHOD_ON_DRAG_END` + provide widget-level drag support without per-widget hacks in core * **Per-widget API registry**: `wgtRegisterApi("name", &api)` replaces the monolithic API * **Per-widget headers**: `widgets/widgetButton.h` etc. provide typed macros * **Shared helpers**: texthelp.lib (text editing) and listhelp.lib (dropdown/list) +* **All limits dynamic**: widget child arrays, app slots, and desktop callbacks + are stb_ds dynamic arrays with no fixed maximums ## DXE Module System @@ -148,10 +161,12 @@ emulators, games). ## Crash Recovery -The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If -an app crashes, the handler `longjmp`s back to the shell's main loop, -the crashed app is force-killed, and the shell continues running. -Diagnostic information (registers, faulting EIP) is logged to `dvx.log`. +The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL +before loading any apps, so even crashes during app initialization are +caught. If an app crashes, the handler `longjmp`s back to the shell's +main loop, the crashed app is force-killed, and the shell continues +running. Diagnostic information (registers, faulting EIP) is logged to +`dvx.log`. ## Bundled Applications @@ -190,8 +205,10 @@ All third-party code is vendored as single-header libraries: | stb_image_write.h | `core/thirdparty/` | PNG export for screenshots | | stb_ds.h | `core/thirdparty/` | Dynamic arrays and hash maps | -stb_ds implementation is compiled into dvx.exe (the loader) and -exported via `dlregsym` to all DXE modules. +stb_ds implementation is compiled into dvx.exe (the loader) with +`STBDS_REALLOC`/`STBDS_FREE` overridden to use `dvxRealloc`/`dvxFree`, +so all `arrput`/`arrfree` calls in DXE code are tracked per-app. The +functions are exported via `dlregsym` to all DXE modules. ## Target Hardware diff --git a/core/README.md b/core/README.md index 0e35d8e..5702071 100644 --- a/core/README.md +++ b/core/README.md @@ -10,7 +10,7 @@ modules that register themselves at runtime via `wgtRegisterClass()`. Core knows nothing about individual widget types. There is no WidgetTypeE enum, no widget union, and no per-widget structs in dvxWidget.h. All widget-specific behavior is dispatched through the -WidgetClassT vtable. +WidgetClassT dispatch table. ## 5-Layer Architecture @@ -29,6 +29,8 @@ Additional modules built into libdvx.lib: |--------|--------|-------------| | `dvxDialog.h` | `dvxDialog.c` | Modal message box and file open/save dialogs | | `dvxPrefs.h` | `dvxPrefs.c` | INI-based preferences (read/write with typed accessors) | +| `dvxResource.h` | `dvxResource.c` | Resource system -- icons, text, and binary data appended to DXE files | +| `dvxMem.h` | (header only) | Per-app memory tracking API declarations | | `dvxWidget.h` | `widgetClass.c`, `widgetCore.c`, `widgetEvent.c`, `widgetLayout.c`, `widgetOps.c`, `widgetScrollbar.c` | Widget infrastructure | | `dvxWidgetPlugin.h` | (header only) | Plugin API for widget DXE modules | | -- | `dvxImage.c` | Image loading via stb_image (BMP, PNG, JPEG, GIF) | @@ -46,7 +48,8 @@ Additional modules built into libdvx.lib: | `dvxApp.c` | `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, screenshots | | `dvxDialog.c` | `dvxMessageBox()`, `dvxFileDialog()` -- modal dialogs with own event loops | | `dvxPrefs.c` | `prefsLoad()`, `prefsSave()`, typed get/set for string/int/bool | -| `dvxImage.c` | `dvxLoadImage()` -- stb_image loader, converts to native pixel format | +| `dvxResource.c` | `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` -- resource system | +| `dvxImage.c` | `dvxLoadImage()`, `dvxLoadImageFromMemory()` -- stb_image loader, converts to native pixel format | | `dvxImageWrite.c` | `dvxSaveImage()` -- PNG writer for screenshots | | `widgetClass.c` | `wgtRegisterClass()`, `wgtRegisterApi()`, `wgtGetApi()`, class table | | `widgetCore.c` | Widget allocation, tree ops, focus management, clipboard, hit testing, cursor blink | @@ -68,7 +71,9 @@ Additional modules built into libdvx.lib: | `dvxApp.h` | Application API: `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, image I/O | | `dvxDialog.h` | Modal dialogs: `dvxMessageBox()`, `dvxFileDialog()` | | `dvxPrefs.h` | INI preferences: `prefsLoad()`, `prefsSave()`, typed accessors | -| `dvxWidget.h` | Widget system public API: WidgetT, WidgetClassT, size tags, layout, API registry | +| `dvxResource.h` | Resource system: `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` | +| `dvxMem.h` | Per-app memory tracking: `dvxMalloc()`, `dvxFree()`, `dvxMemGetAppUsage()`, etc. | +| `dvxWidget.h` | Widget system public API: WidgetT, WidgetClassT, size tags, layout, API registry, `wclsFoo()` dispatch helpers | | `dvxWidgetPlugin.h` | Plugin API for widget DXE authors: tree ops, focus, scrollbar helpers, shared state | | `dvxFont.h` | Embedded 8x14 and 8x16 bitmap font data (CP437) | | `dvxCursor.h` | Mouse cursor AND/XOR mask data (arrow, resize H/V/diag, busy) | @@ -79,7 +84,7 @@ Additional modules built into libdvx.lib: | File | Description | |------|-------------| -| `platform/dvxPlatform.h` | Platform abstraction API (video, input, spans, DXE, crash recovery) | +| `platform/dvxPlatform.h` | Platform abstraction API (video, input, spans, DXE, crash recovery, memory tracking) | | `platform/dvxPlatformDos.c` | DJGPP/DPMI implementation (VESA VBE, INT 33h mouse, INT 16h keyboard, asm spans) | The platform layer is compiled into dvx.exe (the loader), not into @@ -96,6 +101,83 @@ libdvx.lib. Platform functions are exported to all DXE modules via | `thirdparty/stb_ds.h` | Dynamic arrays/hash maps (implementation in loader, exported to all DXEs) | +## Dynamic Limits + +All major data structures grow dynamically via realloc. There are no +fixed-size limits for: + +- **Windows** -- `WindowStackT.windows` is a dynamic array +- **Menus** -- `MenuBarT.menus` and `MenuT.items` are dynamic arrays +- **Accelerator entries** -- `AccelTableT.entries` is a dynamic array +- **Dirty rectangles** -- `DirtyListT.rects` is a dynamic array +- **Submenu depth** -- `PopupStateT.parentStack` is a dynamic array + +The only fixed-size buffers remaining are per-element string fields +(`MAX_TITLE_LEN = 128`, `MAX_MENU_LABEL = 32`, `MAX_WIDGET_NAME = 32`) +and the system menu (`SYS_MENU_MAX_ITEMS = 10`). + + +## Resource System + +Resources are appended to DXE3 files (.app, .wgt, .lib) after the +normal DXE content. The DXE loader never reads past the DXE header, +so appended data is invisible to dlopen. + +File layout: + + [DXE3 content] + [resource data entries] -- sequential, variable length + [resource directory] -- fixed-size entries (48 bytes each) + [footer] -- magic + directory offset + count (16 bytes) + +### Resource Types + +| Define | Value | Description | +|--------|-------|-------------| +| `DVX_RES_ICON` | 1 | Image data (BMP icon: 16x16, 32x32, etc.) | +| `DVX_RES_TEXT` | 2 | Null-terminated string (author, copyright, etc.) | +| `DVX_RES_BINARY` | 3 | Arbitrary binary data (app-specific) | + +### Resource API + +| Function | Description | +|----------|-------------| +| `dvxResOpen(path)` | Open a resource handle by reading the footer and directory. Returns NULL if no resources. | +| `dvxResRead(h, name, outSize)` | Find a resource by name and read its data into a malloc'd buffer. Caller frees. | +| `dvxResFind(h, name)` | Find a resource by name and return its directory entry pointer. | +| `dvxResClose(h)` | Close the handle and free associated memory. | + +### Key Types + +| Type | Description | +|------|-------------| +| `DvxResDirEntryT` | Directory entry: name[32], type, offset, size, reserved (48 bytes) | +| `DvxResFooterT` | Footer: magic (`0x52585644` = "DVXR"), dirOffset, entryCount, reserved (16 bytes) | +| `DvxResHandleT` | Runtime handle: path, entries array, entry count | + + +## Memory Tracking (dvxMem.h) + +Per-app memory tracking wraps malloc/free/calloc/realloc/strdup with a +small header per allocation that records the owning app ID and size. +DXE code does not need to include dvxMem.h -- the DXE export table maps +the standard allocator names to these wrappers transparently. + +| Function | Description | +|----------|-------------| +| `dvxMalloc(size)` | Tracked malloc | +| `dvxCalloc(nmemb, size)` | Tracked calloc | +| `dvxRealloc(ptr, size)` | Tracked realloc | +| `dvxFree(ptr)` | Tracked free (falls through to real free on non-tracked pointers) | +| `dvxStrdup(s)` | Tracked strdup | +| `dvxMemSnapshotLoad(appId)` | Record baseline memory for leak detection | +| `dvxMemGetAppUsage(appId)` | Query current memory usage for an app (bytes) | +| `dvxMemResetApp(appId)` | Free all allocations charged to an app | + +The global `dvxMemAppIdPtr` pointer is set by the shell to +`&ctx->currentAppId` so the allocator knows which app to charge. + + ## WidgetT Structure The WidgetT struct is generic -- no widget-specific fields or union: @@ -103,8 +185,8 @@ The WidgetT struct is generic -- no widget-specific fields or union: ```c typedef struct WidgetT { int32_t type; // assigned by wgtRegisterClass() - const struct WidgetClassT *wclass; // vtable pointer - char name[32]; + const struct WidgetClassT *wclass; // dispatch table pointer + char name[MAX_WIDGET_NAME]; // Tree linkage struct WidgetT *parent, *firstChild, *lastChild, *nextSibling; @@ -144,34 +226,84 @@ typedef struct WidgetT { ``` -## WidgetClassT Vtable +## WidgetClassT Dispatch Table -Each widget type defines a static WidgetClassT with flags and function pointers. -The vtable has 26 function slots plus a flags field: +WidgetClassT is an ABI-stable dispatch table. Method IDs are fixed +constants that never change -- adding new methods appends new IDs +without shifting existing ones. Widget DXEs compiled against an older +DVX version continue to work unmodified. -| Slot | Signature | Purpose | -|------|-----------|---------| -| `flags` | `uint32_t` | Static properties (see Flags below) | -| `paint` | `(w, d, ops, font, colors)` | Render the widget | -| `paintOverlay` | `(w, d, ops, font, colors)` | Render overlay (dropdown popup) | -| `calcMinSize` | `(w, font)` | Compute minimum size (bottom-up pass) | -| `layout` | `(w, font)` | Position children (top-down pass) | -| `getLayoutMetrics` | `(w, font, pad, gap, extraTop, borderW)` | Return padding/gap for box layout | -| `onMouse` | `(w, root, vx, vy)` | Handle mouse click | -| `onKey` | `(w, key, mod)` | Handle keyboard input | -| `onAccelActivate` | `(w, root)` | Handle accelerator key match | -| `destroy` | `(w)` | Free widget-private data | -| `onChildChanged` | `(parent, child)` | Notification when a child changes | -| `getText` | `(w)` | Return widget text | -| `setText` | `(w, text)` | Set widget text | -| `clearSelection` | `(w)` | Clear text/item selection | -| `closePopup` | `(w)` | Close dropdown popup | -| `getPopupRect` | `(w, font, contentH, popX, popY, popW, popH)` | Compute popup rectangle | -| `onDragUpdate` | `(w, root, x, y)` | Mouse move during drag | -| `onDragEnd` | `(w, root, x, y)` | Mouse release after drag | -| `getCursorShape` | `(w, vx, vy)` | Return cursor ID for this position | -| `poll` | `(w, win)` | Periodic polling (AnsiTerm comms) | -| `quickRepaint` | `(w, outY, outH)` | Fast incremental repaint | +```c +#define WGT_CLASS_VERSION 1 // bump on breaking ABI change +#define WGT_METHOD_MAX 32 // room for future methods + +typedef struct WidgetClassT { + uint32_t version; + uint32_t flags; + void *handlers[WGT_METHOD_MAX]; +} WidgetClassT; +``` + +### Method ID Table + +21 methods are currently defined (IDs 0--20). WGT_METHOD_MAX is 32, +leaving room for 11 future methods without a version bump. + +| ID | Method ID | Signature | Purpose | +|----|-----------|-----------|---------| +| 0 | `WGT_METHOD_PAINT` | `void (w, d, ops, font, colors)` | Render the widget | +| 1 | `WGT_METHOD_PAINT_OVERLAY` | `void (w, d, ops, font, colors)` | Render overlay (dropdown popup) | +| 2 | `WGT_METHOD_CALC_MIN_SIZE` | `void (w, font)` | Compute minimum size (bottom-up pass) | +| 3 | `WGT_METHOD_LAYOUT` | `void (w, font)` | Position children (top-down pass) | +| 4 | `WGT_METHOD_GET_LAYOUT_METRICS` | `void (w, font, pad, gap, extraTop, borderW)` | Return padding/gap for box layout | +| 5 | `WGT_METHOD_ON_MOUSE` | `void (w, root, vx, vy)` | Handle mouse click | +| 6 | `WGT_METHOD_ON_KEY` | `void (w, key, mod)` | Handle keyboard input | +| 7 | `WGT_METHOD_ON_ACCEL_ACTIVATE` | `void (w, root)` | Handle accelerator key match | +| 8 | `WGT_METHOD_DESTROY` | `void (w)` | Free widget-private data | +| 9 | `WGT_METHOD_ON_CHILD_CHANGED` | `void (parent, child)` | Notification when a child changes | +| 10 | `WGT_METHOD_GET_TEXT` | `const char *(w)` | Return widget text | +| 11 | `WGT_METHOD_SET_TEXT` | `void (w, text)` | Set widget text | +| 12 | `WGT_METHOD_CLEAR_SELECTION` | `bool (w)` | Clear text/item selection | +| 13 | `WGT_METHOD_CLOSE_POPUP` | `void (w)` | Close dropdown popup | +| 14 | `WGT_METHOD_GET_POPUP_RECT` | `void (w, font, contentH, popX, popY, popW, popH)` | Compute popup rectangle | +| 15 | `WGT_METHOD_ON_DRAG_UPDATE` | `void (w, root, x, y)` | Mouse move during drag | +| 16 | `WGT_METHOD_ON_DRAG_END` | `void (w, root, x, y)` | Mouse release after drag | +| 17 | `WGT_METHOD_GET_CURSOR_SHAPE` | `int32_t (w, vx, vy)` | Return cursor ID for this position | +| 18 | `WGT_METHOD_POLL` | `void (w, win)` | Periodic polling (AnsiTerm comms) | +| 19 | `WGT_METHOD_QUICK_REPAINT` | `int32_t (w, outY, outH)` | Fast incremental repaint | +| 20 | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` | `void (parent, child)` | Scroll to make a child visible | + +### Typed Dispatch Helpers + +Each `wclsFoo()` inline function extracts a handler by stable method +ID, casts it to the correct function pointer type, and calls it with +a NULL check. This gives callers type-safe dispatch with the same +codegen as a direct struct field call. + +| Helper | Wraps Method ID | +|--------|-----------------| +| `wclsHas(w, methodId)` | Check if a method is implemented (non-NULL) | +| `wclsPaint(w, d, ops, font, colors)` | `WGT_METHOD_PAINT` | +| `wclsPaintOverlay(w, d, ops, font, colors)` | `WGT_METHOD_PAINT_OVERLAY` | +| `wclsCalcMinSize(w, font)` | `WGT_METHOD_CALC_MIN_SIZE` | +| `wclsLayout(w, font)` | `WGT_METHOD_LAYOUT` | +| `wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW)` | `WGT_METHOD_GET_LAYOUT_METRICS` | +| `wclsOnMouse(w, root, vx, vy)` | `WGT_METHOD_ON_MOUSE` | +| `wclsOnKey(w, key, mod)` | `WGT_METHOD_ON_KEY` | +| `wclsOnAccelActivate(w, root)` | `WGT_METHOD_ON_ACCEL_ACTIVATE` | +| `wclsDestroy(w)` | `WGT_METHOD_DESTROY` | +| `wclsOnChildChanged(parent, child)` | `WGT_METHOD_ON_CHILD_CHANGED` | +| `wclsGetText(w)` | `WGT_METHOD_GET_TEXT` | +| `wclsSetText(w, text)` | `WGT_METHOD_SET_TEXT` | +| `wclsClearSelection(w)` | `WGT_METHOD_CLEAR_SELECTION` | +| `wclsClosePopup(w)` | `WGT_METHOD_CLOSE_POPUP` | +| `wclsGetPopupRect(w, font, contentH, popX, popY, popW, popH)` | `WGT_METHOD_GET_POPUP_RECT` | +| `wclsOnDragUpdate(w, root, x, y)` | `WGT_METHOD_ON_DRAG_UPDATE` | +| `wclsOnDragEnd(w, root, x, y)` | `WGT_METHOD_ON_DRAG_END` | +| `wclsGetCursorShape(w, vx, vy)` | `WGT_METHOD_GET_CURSOR_SHAPE` | +| `wclsPoll(w, win)` | `WGT_METHOD_POLL` | +| `wclsQuickRepaint(w, outY, outH)` | `WGT_METHOD_QUICK_REPAINT` | +| `wclsScrollChildIntoView(parent, child)` | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` | ### WidgetClassT Flags @@ -196,22 +328,25 @@ The vtable has 26 function slots plus a flags field: ## Widget Registration Each widget DXE exports `wgtRegister()`, called by the loader after -`dlopen`. A typical registration: +`dlopen`. The WidgetClassT uses the `handlers[]` array indexed by +method IDs: ```c static int32_t sButtonType; static const WidgetClassT sButtonClass = { - .flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE, - .paint = buttonPaint, - .calcMinSize = buttonCalcMinSize, - .onMouse = buttonOnMouse, - .onKey = buttonOnKey, - .destroy = buttonDestroy, - .getText = buttonGetText, - .setText = buttonSetText, - .setPressed = buttonSetPressed, - .onAccelActivate = buttonAccelActivate, + .version = WGT_CLASS_VERSION, + .flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE, + .handlers = { + [WGT_METHOD_PAINT] = buttonPaint, + [WGT_METHOD_CALC_MIN_SIZE] = buttonCalcMinSize, + [WGT_METHOD_ON_MOUSE] = buttonOnMouse, + [WGT_METHOD_ON_KEY] = buttonOnKey, + [WGT_METHOD_DESTROY] = buttonDestroy, + [WGT_METHOD_GET_TEXT] = buttonGetText, + [WGT_METHOD_SET_TEXT] = buttonSetText, + [WGT_METHOD_ON_ACCEL_ACTIVATE] = buttonAccelActivate, + } }; static const ButtonApiT sApi = { .create = buttonCreate }; @@ -268,6 +403,28 @@ Two-pass flexbox-like layout: Extra space beyond minimum is distributed proportionally to weights. `weight=0` means fixed size, `weight=100` is the default flexible weight. +### Cross-Axis Centering + +When a child widget has a `maxW` (in a VBox) or `maxH` (in an HBox) +that constrains it smaller than the available cross-axis space, the +layout engine automatically centers the child on the cross axis. This +means setting `maxW` or `maxH` on a child inside a container will both +cap its size and center it within the remaining space. + + +## Image Loading + +Two image loading functions are available: + +| Function | Description | +|----------|-------------| +| `dvxLoadImage(ctx, path, outW, outH, outPitch)` | Load from a file path | +| `dvxLoadImageFromMemory(ctx, data, dataLen, outW, outH, outPitch)` | Load from a memory buffer (e.g. resource data) | + +Both convert to the display's native pixel format. Caller frees the +returned buffer with `dvxFreeImage()`. Supported formats: BMP, PNG, +JPEG, GIF (via stb_image). + ## Exported Symbols @@ -275,10 +432,10 @@ libdvx.lib exports symbols matching these prefixes: ``` dvx*, wgt*, wm*, prefs*, rect*, draw*, pack*, text*, setClip*, -resetClip*, stbi*, dirtyList*, widget*, accelParse*, clipboard*, -multiClick*, sCursor*, sDbl*, sDebug*, sClosed*, sFocused*, sKey*, -sOpen*, sPressed*, sDrag*, sDrawing*, sResize*, sListView*, -sSplitter*, sTreeView* +resetClip*, stbi*, stbi_write*, dirtyList*, widget*, +accelParse*, clipboard*, multiClick*, +sCursor*, sDbl*, sDebug*, sClosed*, sFocused*, sKey*, +sOpen*, sDrag*, sClosed*, sKey* ``` diff --git a/core/dvxApp.c b/core/dvxApp.c index 960a241..b310cbf 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -112,40 +112,43 @@ // ============================================================ 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 void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx); static void closeAllPopups(AppContextT *ctx); static void closePopupLevel(AppContextT *ctx); static void closeSysMenu(AppContextT *ctx); -static void interactiveScreenshot(AppContextT *ctx); -static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win); +static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id); static void compositeAndFlush(AppContextT *ctx); static int32_t countVisibleWindows(const AppContextT *ctx); static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y); -static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t col); static bool dispatchAccelKey(AppContextT *ctx, char key); static void dispatchEvents(AppContextT *ctx); +static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t col); static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y); static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo); +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 handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons); -static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData); static void initColorScheme(AppContextT *ctx); +static void interactiveScreenshot(AppContextT *ctx); +static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win); static void invalidateAllWindows(AppContextT *ctx); static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY); static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx); static void openSubMenu(AppContextT *ctx); static void openSysMenu(AppContextT *ctx, WindowT *win); -static void pollWidgets(AppContextT *ctx); -static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win); static void pollKeyboard(AppContextT *ctx); static void pollMouse(AppContextT *ctx); +static void pollWidgets(AppContextT *ctx); +static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win); 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 void updateCursorShape(AppContextT *ctx); static void updateTooltip(AppContextT *ctx); +static void writePixel(uint8_t *row, int32_t x, uint32_t px, int32_t bpp); // Button pressed via keyboard -- shared with widgetEvent.c for Space/Enter. // Non-static so widgetEvent.c can set it when Space/Enter triggers a button. @@ -165,7 +168,6 @@ static const int32_t sBayerMatrix[4][4] = { }; - // ============================================================ // bufferToRgb -- convert native pixel format to 24-bit RGB // ============================================================ @@ -222,6 +224,188 @@ static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, in } +// ============================================================ +// buildWallpaperBuf -- render full-screen wallpaper buffer from RGB source +// ============================================================ + +static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode) { + int32_t screenW = ctx->display.width; + int32_t screenH = ctx->display.height; + int32_t bpp = ctx->display.format.bitsPerPixel; + int32_t bytesPerPx = ctx->display.format.bytesPerPixel; + int32_t pitch = screenW * bytesPerPx; + int32_t srcStride = imgW * RGB_CHANNELS; + + bool dither = (bpp == 15 || bpp == 16); + + uint8_t *buf = (uint8_t *)malloc(pitch * screenH); + + if (!buf) { + return NULL; + } + + // Fill entire buffer with desktop color first (used by center mode + // for the border area, but also ensures no garbage pixels) + uint32_t bgPx = ctx->colors.desktop; + + if (bytesPerPx == 1) { + memset(buf, bgPx & 0xFF, pitch * screenH); + } else if (bytesPerPx == 2) { + uint16_t px16 = (uint16_t)bgPx; + uint32_t fill32 = ((uint32_t)px16 << 16) | px16; + for (int32_t y = 0; y < screenH; y++) { + uint32_t *row = (uint32_t *)(buf + y * pitch); + int32_t pairs = screenW / 2; + for (int32_t i = 0; i < pairs; i++) { + row[i] = fill32; + } + if (screenW & 1) { + *(uint16_t *)(buf + y * pitch + (screenW - 1) * 2) = px16; + } + } + } else { + uint32_t *row32 = (uint32_t *)buf; + for (int32_t i = 0; i < pitch * screenH / 4; i++) { + row32[i] = bgPx; + } + } + + if (mode == WallpaperStretchE) { + // Bilinear scale to screen dimensions with optional dither + for (int32_t y = 0; y < screenH; y++) { + if ((y & (WALLPAPER_YIELD_ROWS - 1)) == 0 && y > 0) { + dvxUpdate(ctx); + } + + int32_t srcYfp = (int32_t)((int64_t)y * imgH * FP_ONE / screenH); + int32_t sy0 = srcYfp >> 16; + int32_t sy1 = (sy0 + 1 < imgH) ? sy0 + 1 : imgH - 1; + int32_t fy = (srcYfp >> FP_FRAC_SHIFT) & FP_FRAC_MASK; + int32_t ify = FP_BLEND_MAX - fy; + uint8_t *dst = buf + y * pitch; + const uint8_t *row0 = rgb + sy0 * srcStride; + const uint8_t *row1 = rgb + sy1 * srcStride; + + for (int32_t x = 0; x < screenW; x++) { + int32_t srcXfp = (int32_t)((int64_t)x * imgW * FP_ONE / screenW); + int32_t sx0 = srcXfp >> 16; + int32_t sx1 = (sx0 + 1 < imgW) ? sx0 + 1 : imgW - 1; + int32_t fx = (srcXfp >> FP_FRAC_SHIFT) & FP_FRAC_MASK; + int32_t ifx = FP_BLEND_MAX - fx; + + const uint8_t *p00 = row0 + sx0 * RGB_CHANNELS; + const uint8_t *p10 = row0 + sx1 * RGB_CHANNELS; + const uint8_t *p01 = row1 + sx0 * RGB_CHANNELS; + const uint8_t *p11 = row1 + sx1 * RGB_CHANNELS; + + int32_t r = (p00[0] * ifx * ify + p10[0] * fx * ify + p01[0] * ifx * fy + p11[0] * fx * fy) >> 16; + int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16; + int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16; + + if (dither) { + ditherRgb(&r, &g, &b, y, x); + } + + uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); + if (bytesPerPx == 2) { + ((uint16_t *)dst)[x] = (uint16_t)px; + } else if (bytesPerPx == 4) { + ((uint32_t *)dst)[x] = px; + } else { + dst[x] = (uint8_t)px; + } + } + } + } else if (mode == WallpaperTileE) { + // Tile: repeat the image at native size across the screen + for (int32_t y = 0; y < screenH; y++) { + if ((y & (WALLPAPER_YIELD_ROWS - 1)) == 0 && y > 0) { + dvxUpdate(ctx); + } + + int32_t srcY = y % imgH; + uint8_t *dst = buf + y * pitch; + const uint8_t *srcRow = rgb + srcY * srcStride; + + for (int32_t x = 0; x < screenW; x++) { + int32_t srcX = x % imgW; + const uint8_t *src = srcRow + srcX * RGB_CHANNELS; + + int32_t r = src[0]; + int32_t g = src[1]; + int32_t b = src[2]; + + if (dither) { + ditherRgb(&r, &g, &b, y, x); + } + + uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); + if (bytesPerPx == 2) { + ((uint16_t *)dst)[x] = (uint16_t)px; + } else if (bytesPerPx == 4) { + ((uint32_t *)dst)[x] = px; + } else { + dst[x] = (uint8_t)px; + } + } + } + } else { + // Center: place at native size in the center of the screen. + // Buffer was already filled with desktop color above. + // Clip the source region to only iterate visible pixels. + int32_t offX = (screenW - imgW) / 2; + int32_t offY = (screenH - imgH) / 2; + + int32_t srcStartX = (offX < 0) ? -offX : 0; + int32_t srcStartY = (offY < 0) ? -offY : 0; + int32_t srcEndX = imgW; + int32_t srcEndY = imgH; + + if (offX + srcEndX > screenW) { + srcEndX = screenW - offX; + } + + if (offY + srcEndY > screenH) { + srcEndY = screenH - offY; + } + + for (int32_t sy = srcStartY; sy < srcEndY; sy++) { + if (((sy - srcStartY) & (WALLPAPER_YIELD_ROWS - 1)) == 0 && sy > srcStartY) { + dvxUpdate(ctx); + } + + int32_t dy = offY + sy; + uint8_t *dst = buf + dy * pitch; + const uint8_t *srcRow = rgb + sy * srcStride; + + for (int32_t sx = srcStartX; sx < srcEndX; sx++) { + int32_t dx = offX + sx; + + const uint8_t *src = srcRow + sx * RGB_CHANNELS; + int32_t r = src[0]; + int32_t g = src[1]; + int32_t b = src[2]; + + if (dither) { + ditherRgb(&r, &g, &b, dy, dx); + } + + uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); + if (bytesPerPx == 2) { + ((uint16_t *)dst)[dx] = (uint16_t)px; + } else if (bytesPerPx == 4) { + ((uint32_t *)dst)[dx] = px; + } else { + dst[dx] = (uint8_t)px; + } + } + } + } + + return buf; +} + + // ============================================================ // calcPopupSize -- compute popup width and height for a menu // ============================================================ @@ -461,6 +645,62 @@ static void closeSysMenu(AppContextT *ctx) { } +// ============================================================ +// Color scheme -- name table and indexed access +// ============================================================ + +// INI/theme key names (must not change -- used for save/load) +static const char *sColorNames[ColorCountE] = { + "desktop", "windowFace", "windowHighlight", + "windowShadow", "activeTitleBg", "activeTitleFg", + "inactiveTitleBg", "inactiveTitleFg", "contentBg", + "contentFg", "menuBg", "menuFg", + "menuHighlightBg", "menuHighlightFg", "buttonFace", + "scrollbarBg", "scrollbarFg", "scrollbarTrough", + "cursorColor", "cursorOutline" +}; + +// Human-readable display names for the UI +static const char *sColorLabels[ColorCountE] = { + "Desktop", "Window Face", "Window Highlight", + "Window Shadow", "Active Title Bar", "Active Title Text", + "Inactive Title Bar", "Inactive Title Text", "Content Background", + "Content Text", "Menu Background", "Menu Text", + "Menu Highlight", "Menu Highlight Text", "Button Face", + "Scrollbar Background", "Scrollbar Foreground", "Scrollbar Trough", + "Cursor Color", "Cursor Outline" +}; + +// Default GEOS Ensemble Motif-style colors (RGB triplets) +static const uint8_t sDefaultColors[ColorCountE][3] = { + { 0, 128, 128}, // desktop -- GEOS teal + {192, 192, 192}, // windowFace + {255, 255, 255}, // windowHighlight + {128, 128, 128}, // windowShadow + { 48, 48, 48}, // activeTitleBg -- dark charcoal + {255, 255, 255}, // activeTitleFg + {160, 160, 160}, // inactiveTitleBg + { 64, 64, 64}, // inactiveTitleFg + {192, 192, 192}, // contentBg + { 0, 0, 0}, // contentFg + {192, 192, 192}, // menuBg + { 0, 0, 0}, // menuFg + { 48, 48, 48}, // menuHighlightBg + {255, 255, 255}, // menuHighlightFg + {192, 192, 192}, // buttonFace + {192, 192, 192}, // scrollbarBg + {128, 128, 128}, // scrollbarFg + {160, 160, 160}, // scrollbarTrough + {255, 255, 255}, // cursorFg -- white + { 0, 0, 0}, // cursorBg -- black +}; + +// Access the packed color value in ColorSchemeT by index. +static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) { + return ((uint32_t *)cs) + (int32_t)id; +} + + // ============================================================ // compositeAndFlush // ============================================================ @@ -669,36 +909,6 @@ static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) { } -// ============================================================ -// ditherRgb -- apply Bayer ordered dithering to an RGB triplet -// ============================================================ - -static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t col) { - int32_t d = sBayerMatrix[row & 3][col & 3]; - *r += d; - *g += d; - *b += d; - - if (*r < 0) { - *r = 0; - } else if (*r > 255) { - *r = 255; - } - - if (*g < 0) { - *g = 0; - } else if (*g > 255) { - *g = 255; - } - - if (*b < 0) { - *b = 0; - } else if (*b > 255) { - *b = 255; - } -} - - // ============================================================ // dispatchAccelKey -- route Alt+key to menu or widget // ============================================================ @@ -1215,6 +1425,36 @@ static void dispatchEvents(AppContextT *ctx) { } +// ============================================================ +// ditherRgb -- apply Bayer ordered dithering to an RGB triplet +// ============================================================ + +static void ditherRgb(int32_t *r, int32_t *g, int32_t *b, int32_t row, int32_t col) { + int32_t d = sBayerMatrix[row & 3][col & 3]; + *r += d; + *g += d; + *b += d; + + if (*r < 0) { + *r = 0; + } else if (*r > 255) { + *r = 255; + } + + if (*g < 0) { + *g = 0; + } else if (*g > 255) { + *g = 255; + } + + if (*b < 0) { + *b = 0; + } else if (*b > 255) { + *b = 255; + } +} + + // ============================================================ // drawCursorAt // ============================================================ @@ -1368,607 +1608,6 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c } -// ============================================================ -// dvxCreateWindow -// ============================================================ - -WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { - // Auto-cascade: if another window already occupies this exact position, - // 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; - - for (;;) { - bool collision = false; - - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *other = ctx->stack.windows[i]; - - if (other->x == x && other->y == y) { - collision = true; - break; - } - } - - if (!collision) { - break; - } - - x += step; - y += step; - - if (x + w > ctx->display.width || y + h > ctx->display.height) { - x = step; - y = step; - break; - } - } - - // Clamp to screen so the window is fully visible - if (x + w > ctx->display.width) { - x = ctx->display.width - w; - } - - if (y + h > ctx->display.height) { - y = ctx->display.height - h; - } - - if (x < 0) { - x = 0; - } - - if (y < 0) { - y = 0; - } - - WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display, title, x, y, w, h, resizable); - - if (win) { - // Stamp window with the current app ID (set by the shell before - // calling app entry points). Enables per-app window tracking for - // cleanup on crash/termination. - win->appId = ctx->currentAppId; - - // Raise and focus - int32_t idx = ctx->stack.count - 1; - wmSetFocus(&ctx->stack, &ctx->dirty, idx); - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - } - - return win; -} - - -// ============================================================ -// dvxCreateWindowCentered -// ============================================================ - -WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) { - int32_t x = (ctx->display.width - w) / 2; - int32_t y = (ctx->display.height - h) / 2; - - return dvxCreateWindow(ctx, title, x, y, w, h, resizable); -} - - -// ============================================================ -// dvxAddAccel -// ============================================================ -// -// Accelerator entries are pre-normalized at registration time: the key -// is uppercased and modifier bits are masked to just Ctrl|Alt. This -// moves the normalization cost from the hot path (every keypress) to -// the cold path (one-time setup), so checkAccelTable can do a simple -// integer compare per entry. - -void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) { - if (!table) { - 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; - - if (normKey >= 'a' && normKey <= 'z') { - normKey = normKey - 32; - } - - AccelEntryT *e = &table->entries[table->count++]; - e->key = key; - e->modifiers = modifiers; - e->cmdId = cmdId; - e->normKey = normKey; - e->normMods = modifiers & (ACCEL_CTRL | ACCEL_ALT); -} - - -// ============================================================ -// dvxClipboardCopy / dvxClipboardGet -// ============================================================ -// -// The clipboard is a simple in-process text buffer managed by the -// platform layer (clipboardCopy/clipboardGet). There is no inter-process -// clipboard because DVX runs as a single-process windowing system -- all -// windows share the same address space. The thin wrappers here exist to -// keep the platform layer out of application code's include path. - -void dvxClipboardCopy(const char *text, int32_t len) { - clipboardCopy(text, len); -} - - -const char *dvxClipboardGet(int32_t *outLen) { - return clipboardGet(outLen); -} - - -// ============================================================ -// Color scheme -- name table and indexed access -// ============================================================ - -// INI/theme key names (must not change -- used for save/load) -static const char *sColorNames[ColorCountE] = { - "desktop", "windowFace", "windowHighlight", - "windowShadow", "activeTitleBg", "activeTitleFg", - "inactiveTitleBg", "inactiveTitleFg", "contentBg", - "contentFg", "menuBg", "menuFg", - "menuHighlightBg", "menuHighlightFg", "buttonFace", - "scrollbarBg", "scrollbarFg", "scrollbarTrough", - "cursorColor", "cursorOutline" -}; - -// Human-readable display names for the UI -static const char *sColorLabels[ColorCountE] = { - "Desktop", "Window Face", "Window Highlight", - "Window Shadow", "Active Title Bar", "Active Title Text", - "Inactive Title Bar", "Inactive Title Text", "Content Background", - "Content Text", "Menu Background", "Menu Text", - "Menu Highlight", "Menu Highlight Text", "Button Face", - "Scrollbar Background", "Scrollbar Foreground", "Scrollbar Trough", - "Cursor Color", "Cursor Outline" -}; - -// Default GEOS Ensemble Motif-style colors (RGB triplets) -static const uint8_t sDefaultColors[ColorCountE][3] = { - { 0, 128, 128}, // desktop -- GEOS teal - {192, 192, 192}, // windowFace - {255, 255, 255}, // windowHighlight - {128, 128, 128}, // windowShadow - { 48, 48, 48}, // activeTitleBg -- dark charcoal - {255, 255, 255}, // activeTitleFg - {160, 160, 160}, // inactiveTitleBg - { 64, 64, 64}, // inactiveTitleFg - {192, 192, 192}, // contentBg - { 0, 0, 0}, // contentFg - {192, 192, 192}, // menuBg - { 0, 0, 0}, // menuFg - { 48, 48, 48}, // menuHighlightBg - {255, 255, 255}, // menuHighlightFg - {192, 192, 192}, // buttonFace - {192, 192, 192}, // scrollbarBg - {128, 128, 128}, // scrollbarFg - {160, 160, 160}, // scrollbarTrough - {255, 255, 255}, // cursorFg -- white - { 0, 0, 0}, // cursorBg -- black -}; - -// Access the packed color value in ColorSchemeT by index. -static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) { - return ((uint32_t *)cs) + (int32_t)id; -} - - -// ============================================================ -// dvxColorName -// ============================================================ - -const char *dvxColorName(ColorIdE id) { - if (id < 0 || id >= ColorCountE) { - return "unknown"; - } - - return sColorNames[id]; -} - - -// ============================================================ -// dvxColorLabel -// ============================================================ - -const char *dvxColorLabel(ColorIdE id) { - if (id < 0 || id >= ColorCountE) { - return "Unknown"; - } - - return sColorLabels[id]; -} - - -// ============================================================ -// dvxApplyColorScheme -// ============================================================ - -void dvxApplyColorScheme(AppContextT *ctx) { - DisplayT *d = &ctx->display; - - for (int32_t i = 0; i < ColorCountE; i++) { - *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); -} - - -// ============================================================ -// dvxResetColorScheme -// ============================================================ - -void dvxResetColorScheme(AppContextT *ctx) { - memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors)); - dvxApplyColorScheme(ctx); -} - - -// ============================================================ -// dvxCascadeWindows -// ============================================================ -// -// Arranges windows in the classic cascade pattern: each window is the same -// size (2/3 of screen), offset diagonally by the title bar height so each -// title bar remains visible. When the cascade would go off-screen, it wraps -// back to (0,0). This matches the Windows 3.x cascade behavior. -// The step size is title_height + border_width so exactly one title bar's -// worth of the previous window peeks out above and to the left. - -void dvxCascadeWindows(AppContextT *ctx) { - int32_t screenW = ctx->display.width; - 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 winW = screenW * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM; - int32_t winH = screenH * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM; - - if (winW < MIN_WINDOW_W) { - winW = MIN_WINDOW_W; - } - - if (winH < MIN_WINDOW_H) { - winH = MIN_WINDOW_H; - } - - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; - - if (win->minimized || !win->visible) { - continue; - } - - repositionWindow(ctx, win, offsetX, offsetY, winW, winH); - - offsetX += step; - offsetY += step; - - // Wrap around if we'd go off screen - if (offsetX + winW > screenW || offsetY + winH > screenH) { - offsetX = 0; - offsetY = 0; - } - } -} - - -// ============================================================ -// dvxChangeVideoMode -// ============================================================ -// -// Live video mode switch. Saves the old display state, attempts to -// set the new mode, and if successful, reinitializes all dependent -// subsystems: blit ops, colors, cursors, mouse range, wallpaper, -// and all window content buffers. Windows larger than the new -// screen are clamped. On failure, the old mode is restored. - -int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { - // Save old state for rollback - DisplayT oldDisplay = ctx->display; - - // Stash old wallpaper (don't free -- we may need it for rollback) - uint8_t *oldWpBuf = ctx->wallpaperBuf; - int32_t oldWpPitch = ctx->wallpaperPitch; - ctx->wallpaperBuf = NULL; - ctx->wallpaperPitch = 0; - - // Free old video buffers (no text mode restore) - platformVideoFreeBuffers(&ctx->display); - - // Try the new mode - if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { - // Restore old mode - ctx->display = oldDisplay; - ctx->display.backBuf = NULL; - ctx->display.palette = NULL; - - if (videoInit(&ctx->display, oldDisplay.width, oldDisplay.height, oldDisplay.format.bitsPerPixel) != 0) { - // Both failed -- catastrophic - return -1; - } - - // Restore wallpaper - ctx->wallpaperBuf = oldWpBuf; - ctx->wallpaperPitch = oldWpPitch; - drawInit(&ctx->blitOps, &ctx->display); - dvxApplyColorScheme(ctx); - return -1; - } - - // New mode succeeded -- free old wallpaper buffer - free(oldWpBuf); - - // Reinit blit ops for new pixel format - drawInit(&ctx->blitOps, &ctx->display); - - // Repack all colors for new pixel format - dvxApplyColorScheme(ctx); - - // Reinit mouse range - platformMouseInit(ctx->display.width, ctx->display.height); - ctx->hasMouseWheel = platformMouseWheelInit(); - - // Clamp mouse position to new screen - if (ctx->mouseX >= ctx->display.width) { - ctx->mouseX = ctx->display.width - 1; - } - - if (ctx->mouseY >= ctx->display.height) { - ctx->mouseY = ctx->display.height - 1; - } - - // Clamp and reallocate all window content buffers - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; - - // Clamp window position to new screen bounds - if (win->x + win->w > ctx->display.width) { - win->x = ctx->display.width - win->w; - } - - if (win->x < 0) { - win->x = 0; - win->w = ctx->display.width; - } - - if (win->y + win->h > ctx->display.height) { - win->y = ctx->display.height - win->h; - } - - if (win->y < 0) { - win->y = 0; - win->h = ctx->display.height; - } - - // 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->contentDirty = true; - } - } - - // Reload wallpaper at the new resolution/bpp - if (ctx->wallpaperPath[0]) { - dvxSetWallpaper(ctx, ctx->wallpaperPath); - } - - // Reset clip and dirty the full screen - resetClipRect(&ctx->display); - dirtyListInit(&ctx->dirty); - dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); - - return 0; -} - - -// ============================================================ -// dvxCreateAccelTable -// ============================================================ - -AccelTableT *dvxCreateAccelTable(void) { - AccelTableT *table = (AccelTableT *)calloc(1, sizeof(AccelTableT)); - return table; -} - - -// ============================================================ -// dvxDestroyWindow -// ============================================================ - -void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - - // If the window is minimized, dirty the icon strip so the icon - // disappears and remaining icons repack correctly. - 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); - } - - wmDestroyWindow(&ctx->stack, win); - - // Dirty icon area again with the updated count (one fewer icon) - 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); - } - - // Focus the new top window - if (ctx->stack.count > 0) { - wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); - } -} - - -// ============================================================ -// dvxFitWindow -// ============================================================ -// -// Resizes a window to exactly fit its widget tree's minimum size, -// accounting for chrome overhead (title bar, borders, optional menu bar). -// Used after building a dialog's widget tree to size the dialog -// automatically rather than requiring the caller to compute sizes manually. - -void dvxFitWindow(AppContextT *ctx, WindowT *win) { - if (!ctx || !win || !win->widgetRoot) { - return; - } - - // Measure the widget tree to get minimum content size - widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); - - int32_t contentW = win->widgetRoot->calcMinW; - int32_t contentH = win->widgetRoot->calcMinH; - - // Compute chrome overhead - int32_t topChrome = CHROME_TOTAL_TOP; - if (win->menuBar) { - topChrome += CHROME_MENU_HEIGHT; - } - - int32_t newW = contentW + CHROME_TOTAL_SIDE * 2; - int32_t newH = contentH + topChrome + CHROME_TOTAL_BOTTOM; - - // Dirty old position - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - - // Resize - win->w = newW; - win->h = newH; - - // Shift position so the window stays fully on screen - if (win->x + newW > ctx->display.width) { - win->x = ctx->display.width - newW; - } - - if (win->y + newH > ctx->display.height) { - win->y = ctx->display.height - newH; - } - - if (win->x < 0) { - win->x = 0; - } - - if (win->y < 0) { - win->y = 0; - } - - wmUpdateContentRect(win); - wmReallocContentBuf(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); -} - - -// ============================================================ -// dvxFreeAccelTable -// ============================================================ - -void dvxFreeAccelTable(AccelTableT *table) { - if (table) { - free(table->entries); - } - - free(table); -} - - -// ============================================================ -// dvxFreeImage -// ============================================================ - -void dvxFreeImage(uint8_t *data) { - free(data); -} - - -// ============================================================ -// dvxGetBlitOps -// ============================================================ - -const BlitOpsT *dvxGetBlitOps(const AppContextT *ctx) { - return &ctx->blitOps; -} - - -// ============================================================ -// dvxGetColors -// ============================================================ - -const ColorSchemeT *dvxGetColors(const AppContextT *ctx) { - return &ctx->colors; -} - - -// ============================================================ -// dvxGetDisplay -// ============================================================ - -DisplayT *dvxGetDisplay(AppContextT *ctx) { - return &ctx->display; -} - - -// ============================================================ -// dvxGetFont -// ============================================================ - -const BitmapFontT *dvxGetFont(const AppContextT *ctx) { - return &ctx->font; -} - - -// ============================================================ -// dvxGetVideoModes -// ============================================================ - -const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count) { - *count = ctx->videoModeCount; - return ctx->videoModes; -} - - // ============================================================ // enumModeCb -- used during dvxInit to capture available modes // ============================================================ @@ -1988,1152 +1627,6 @@ static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) { } -// ============================================================ -// dvxGetColor -// ============================================================ - -void dvxGetColor(const AppContextT *ctx, ColorIdE id, uint8_t *r, uint8_t *g, uint8_t *b) { - if (id < 0 || id >= ColorCountE) { - *r = *g = *b = 0; - return; - } - - *r = ctx->colorRgb[id][0]; - *g = ctx->colorRgb[id][1]; - *b = ctx->colorRgb[id][2]; -} - - -// ============================================================ -// dvxLoadTheme -// ============================================================ - -bool dvxLoadTheme(AppContextT *ctx, const char *filename) { - FILE *fp = fopen(filename, "rb"); - - if (!fp) { - return false; - } - - char line[256]; - - while (fgets(line, sizeof(line), fp)) { - // Strip trailing whitespace - char *end = line + strlen(line) - 1; - - while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ')) { - *end-- = '\0'; - } - - char *p = line; - - while (*p == ' ' || *p == '\t') { - p++; - } - - // Skip comments, blank lines, section headers - if (*p == '\0' || *p == ';' || *p == '#' || *p == '[') { - continue; - } - - // Parse key = r,g,b - char *eq = strchr(p, '='); - - if (!eq) { - continue; - } - - *eq = '\0'; - - // Trim key - char *key = p; - char *keyEnd = eq - 1; - - while (keyEnd >= key && (*keyEnd == ' ' || *keyEnd == '\t')) { - *keyEnd-- = '\0'; - } - - // Parse r,g,b - char *val = eq + 1; - - while (*val == ' ' || *val == '\t') { - val++; - } - - int32_t r; - int32_t g; - int32_t b; - - if (sscanf(val, "%d,%d,%d", &r, &g, &b) != 3) { - continue; - } - - // 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; - break; - } - } - } - - fclose(fp); - dvxApplyColorScheme(ctx); - return true; -} - - -// ============================================================ -// dvxSaveTheme -// ============================================================ - -bool dvxSaveTheme(const AppContextT *ctx, const char *filename) { - FILE *fp = fopen(filename, "wb"); - - if (!fp) { - return false; - } - - fprintf(fp, "; DVX Color Theme\r\n\r\n[colors]\r\n"); - - for (int32_t i = 0; i < ColorCountE; i++) { - fprintf(fp, "%-20s = %d,%d,%d\r\n", sColorNames[i], ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); - } - - fclose(fp); - return true; -} - - -// ============================================================ -// dvxSetColor -// ============================================================ - -void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) { - if (id < 0 || id >= ColorCountE) { - return; - } - - ctx->colorRgb[id][0] = r; - ctx->colorRgb[id][1] = g; - ctx->colorRgb[id][2] = 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); -} - - -// ============================================================ -// dvxSetWallpaper -// ============================================================ - -// writePixel -- write a packed pixel to a buffer at position x -static void writePixel(uint8_t *row, int32_t x, uint32_t px, int32_t bpp) { - if (bpp == 8) { - row[x] = (uint8_t)px; - } else if (bpp == 15 || bpp == 16) { - ((uint16_t *)row)[x] = (uint16_t)px; - } else { - ((uint32_t *)row)[x] = px; - } -} - - -// buildWallpaperBuf -- render the full-screen wallpaper buffer from RGB source -static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode) { - int32_t screenW = ctx->display.width; - int32_t screenH = ctx->display.height; - int32_t bpp = ctx->display.format.bitsPerPixel; - int32_t bytesPerPx = ctx->display.format.bytesPerPixel; - int32_t pitch = screenW * bytesPerPx; - int32_t srcStride = imgW * RGB_CHANNELS; - - bool dither = (bpp == 15 || bpp == 16); - - uint8_t *buf = (uint8_t *)malloc(pitch * screenH); - - if (!buf) { - return NULL; - } - - // Fill entire buffer with desktop color first (used by center mode - // for the border area, but also ensures no garbage pixels) - uint32_t bgPx = ctx->colors.desktop; - - if (bytesPerPx == 1) { - memset(buf, bgPx & 0xFF, pitch * screenH); - } else if (bytesPerPx == 2) { - uint16_t px16 = (uint16_t)bgPx; - uint32_t fill32 = ((uint32_t)px16 << 16) | px16; - for (int32_t y = 0; y < screenH; y++) { - uint32_t *row = (uint32_t *)(buf + y * pitch); - int32_t pairs = screenW / 2; - for (int32_t i = 0; i < pairs; i++) { - row[i] = fill32; - } - if (screenW & 1) { - *(uint16_t *)(buf + y * pitch + (screenW - 1) * 2) = px16; - } - } - } else { - uint32_t *row32 = (uint32_t *)buf; - for (int32_t i = 0; i < pitch * screenH / 4; i++) { - row32[i] = bgPx; - } - } - - if (mode == WallpaperStretchE) { - // Bilinear scale to screen dimensions with optional dither - for (int32_t y = 0; y < screenH; y++) { - if ((y & (WALLPAPER_YIELD_ROWS - 1)) == 0 && y > 0) { - dvxUpdate(ctx); - } - - int32_t srcYfp = (int32_t)((int64_t)y * imgH * FP_ONE / screenH); - int32_t sy0 = srcYfp >> 16; - int32_t sy1 = (sy0 + 1 < imgH) ? sy0 + 1 : imgH - 1; - int32_t fy = (srcYfp >> FP_FRAC_SHIFT) & FP_FRAC_MASK; - int32_t ify = FP_BLEND_MAX - fy; - uint8_t *dst = buf + y * pitch; - const uint8_t *row0 = rgb + sy0 * srcStride; - const uint8_t *row1 = rgb + sy1 * srcStride; - - for (int32_t x = 0; x < screenW; x++) { - int32_t srcXfp = (int32_t)((int64_t)x * imgW * FP_ONE / screenW); - int32_t sx0 = srcXfp >> 16; - int32_t sx1 = (sx0 + 1 < imgW) ? sx0 + 1 : imgW - 1; - int32_t fx = (srcXfp >> FP_FRAC_SHIFT) & FP_FRAC_MASK; - int32_t ifx = FP_BLEND_MAX - fx; - - const uint8_t *p00 = row0 + sx0 * RGB_CHANNELS; - const uint8_t *p10 = row0 + sx1 * RGB_CHANNELS; - const uint8_t *p01 = row1 + sx0 * RGB_CHANNELS; - const uint8_t *p11 = row1 + sx1 * RGB_CHANNELS; - - int32_t r = (p00[0] * ifx * ify + p10[0] * fx * ify + p01[0] * ifx * fy + p11[0] * fx * fy) >> 16; - int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16; - int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16; - - if (dither) { - ditherRgb(&r, &g, &b, y, x); - } - - uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); - if (bytesPerPx == 2) { - ((uint16_t *)dst)[x] = (uint16_t)px; - } else if (bytesPerPx == 4) { - ((uint32_t *)dst)[x] = px; - } else { - dst[x] = (uint8_t)px; - } - } - } - } else if (mode == WallpaperTileE) { - // Tile: repeat the image at native size across the screen - for (int32_t y = 0; y < screenH; y++) { - if ((y & (WALLPAPER_YIELD_ROWS - 1)) == 0 && y > 0) { - dvxUpdate(ctx); - } - - int32_t srcY = y % imgH; - uint8_t *dst = buf + y * pitch; - const uint8_t *srcRow = rgb + srcY * srcStride; - - for (int32_t x = 0; x < screenW; x++) { - int32_t srcX = x % imgW; - const uint8_t *src = srcRow + srcX * RGB_CHANNELS; - - int32_t r = src[0]; - int32_t g = src[1]; - int32_t b = src[2]; - - if (dither) { - ditherRgb(&r, &g, &b, y, x); - } - - uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); - if (bytesPerPx == 2) { - ((uint16_t *)dst)[x] = (uint16_t)px; - } else if (bytesPerPx == 4) { - ((uint32_t *)dst)[x] = px; - } else { - dst[x] = (uint8_t)px; - } - } - } - } else { - // Center: place at native size in the center of the screen. - // Buffer was already filled with desktop color above. - // Clip the source region to only iterate visible pixels. - int32_t offX = (screenW - imgW) / 2; - int32_t offY = (screenH - imgH) / 2; - - int32_t srcStartX = (offX < 0) ? -offX : 0; - int32_t srcStartY = (offY < 0) ? -offY : 0; - int32_t srcEndX = imgW; - int32_t srcEndY = imgH; - - if (offX + srcEndX > screenW) { - srcEndX = screenW - offX; - } - - if (offY + srcEndY > screenH) { - srcEndY = screenH - offY; - } - - for (int32_t sy = srcStartY; sy < srcEndY; sy++) { - if (((sy - srcStartY) & (WALLPAPER_YIELD_ROWS - 1)) == 0 && sy > srcStartY) { - dvxUpdate(ctx); - } - - int32_t dy = offY + sy; - uint8_t *dst = buf + dy * pitch; - const uint8_t *srcRow = rgb + sy * srcStride; - - for (int32_t sx = srcStartX; sx < srcEndX; sx++) { - int32_t dx = offX + sx; - - const uint8_t *src = srcRow + sx * RGB_CHANNELS; - int32_t r = src[0]; - int32_t g = src[1]; - int32_t b = src[2]; - - if (dither) { - ditherRgb(&r, &g, &b, dy, dx); - } - - uint32_t px = packColor(&ctx->display, (uint8_t)r, (uint8_t)g, (uint8_t)b); - if (bytesPerPx == 2) { - ((uint16_t *)dst)[dx] = (uint16_t)px; - } else if (bytesPerPx == 4) { - ((uint32_t *)dst)[dx] = px; - } else { - dst[dx] = (uint8_t)px; - } - } - } - } - - return buf; -} - - -bool dvxSetWallpaper(AppContextT *ctx, const char *path) { - free(ctx->wallpaperBuf); - ctx->wallpaperBuf = NULL; - ctx->wallpaperPitch = 0; - - if (!path) { - ctx->wallpaperPath[0] = '\0'; - dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); - return true; - } - - strncpy(ctx->wallpaperPath, path, sizeof(ctx->wallpaperPath) - 1); - ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0'; - - dvxSetBusy(ctx, true); - - int32_t imgW; - int32_t imgH; - int32_t channels; - uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); - - if (!rgb) { - dvxSetBusy(ctx, false); - return false; - } - - int32_t pitch = ctx->display.width * ctx->display.format.bytesPerPixel; - - ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode); - ctx->wallpaperPitch = pitch; - dvxSetBusy(ctx, false); - - stbi_image_free(rgb); - - if (!ctx->wallpaperBuf) { - return false; - } - - dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); - return true; -} - - -// ============================================================ -// dvxSetWallpaperMode -// ============================================================ - -void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode) { - ctx->wallpaperMode = mode; - - if (ctx->wallpaperPath[0]) { - dvxSetWallpaper(ctx, ctx->wallpaperPath); - } -} - - -// ============================================================ -// dvxInit -// ============================================================ -// -// One-shot initialization of all GUI subsystems. The layered init order -// matters: video must be up before draw ops can be selected (since draw -// ops depend on pixel format), and colors must be packed after the -// display format is known. - -int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { - memset(ctx, 0, sizeof(*ctx)); - - platformInit(); - - // 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; - platformVideoEnumModes(enumModeCb, ctx); - - if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { - return -1; - } - - // Draw ops are pixel-format-dependent function pointers (e.g., 16bpp - // vs 32bpp span fill). Selected once here, then used everywhere. - drawInit(&ctx->blitOps, &ctx->display); - - wmInit(&ctx->stack); - dirtyListInit(&ctx->dirty); - - // 8x16 is the only font size currently supported. Fixed-width bitmap - // fonts are used throughout because variable-width text measurement - // would add complexity and cost on every text draw without much - // benefit at 640x480 resolution. - ctx->font = dvxFont8x16; - - memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors)); - ctx->cursorId = CURSOR_ARROW; - - 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; - ctx->mouseY = ctx->display.height / 2; - ctx->prevMouseX = ctx->mouseX; - ctx->prevMouseY = ctx->mouseY; - - ctx->running = true; - ctx->lastIconClickId = -1; - ctx->lastIconClickTime = 0; - ctx->lastCloseClickId = -1; - ctx->lastCloseClickTime = 0; - ctx->lastTitleClickId = -1; - ctx->lastTitleClickTime = 0; - ctx->wheelDirection = 1; - ctx->dblClickTicks = DBLCLICK_THRESHOLD; - sDblClickTicks = DBLCLICK_THRESHOLD; - - // Pre-compute fixed-point 16.16 reciprocal of character height so - // popup menu item index calculation can use multiply+shift instead - // of division. On a 486, integer divide is 40+ cycles; this - // reciprocal trick reduces it to ~10 cycles (imul + shr). - ctx->charHeightRecip = ((uint32_t)FP_ONE + (uint32_t)ctx->font.charHeight - 1) / (uint32_t)ctx->font.charHeight; - - // Dirty the entire screen so the first compositeAndFlush paints everything - dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); - - return 0; -} - - -// ============================================================ -// dvxLoadImage -// ============================================================ -// -// Public image loading API. Loads any image file supported by stb_image -// (BMP, PNG, JPEG, GIF, etc.) and converts the RGB pixels to the -// display's native pixel format for direct use with rectCopy, wgtImage, -// wgtImageButton, or any other pixel-data consumer. The caller owns the -// returned buffer and must free it with dvxFreeImage(). - -uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) { - if (!ctx || !path) { - return NULL; - } - - const DisplayT *d = &ctx->display; - - int imgW; - int imgH; - int channels; - uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); - - if (!rgb) { - return NULL; - } - - int32_t bpp = d->format.bytesPerPixel; - int32_t pitch = imgW * bpp; - uint8_t *buf = (uint8_t *)malloc(pitch * imgH); - - if (!buf) { - stbi_image_free(rgb); - return NULL; - } - - for (int32_t y = 0; y < imgH; y++) { - for (int32_t x = 0; x < imgW; x++) { - const uint8_t *src = rgb + (y * imgW + x) * RGB_CHANNELS; - uint32_t color = packColor(d, src[0], src[1], src[2]); - uint8_t *dst = buf + y * pitch + x * bpp; - - if (bpp == 1) { - *dst = (uint8_t)color; - } else if (bpp == 2) { - *(uint16_t *)dst = (uint16_t)color; - } else { - *(uint32_t *)dst = color; - } - } - } - - stbi_image_free(rgb); - - if (outW) { - *outW = imgW; - } - - if (outH) { - *outH = imgH; - } - - if (outPitch) { - *outPitch = pitch; - } - - return buf; -} - - -// ============================================================ -// dvxLoadImageFromMemory -// ============================================================ -// -// Same as dvxLoadImage but loads from a memory buffer (e.g. a -// resource read via dvxResRead). Supports BMP, PNG, JPEG, GIF. - -uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) { - if (!ctx || !data || dataLen <= 0) { - return NULL; - } - - const DisplayT *d = &ctx->display; - - int imgW; - int imgH; - int channels; - uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, 3); - - if (!rgb) { - return NULL; - } - - int32_t bpp = d->format.bytesPerPixel; - int32_t pitch = imgW * bpp; - uint8_t *buf = (uint8_t *)malloc(pitch * imgH); - - if (!buf) { - stbi_image_free(rgb); - return NULL; - } - - for (int32_t y = 0; y < imgH; y++) { - for (int32_t x = 0; x < imgW; x++) { - const uint8_t *src = rgb + (y * imgW + x) * 3; - uint32_t color = packColor(d, src[0], src[1], src[2]); - uint8_t *dst = buf + y * pitch + x * bpp; - - if (bpp == 1) { - *dst = (uint8_t)color; - } else if (bpp == 2) { - *(uint16_t *)dst = (uint16_t)color; - } else { - *(uint32_t *)dst = color; - } - } - } - - stbi_image_free(rgb); - - if (outW) { - *outW = imgW; - } - - if (outH) { - *outH = imgH; - } - - if (outPitch) { - *outPitch = pitch; - } - - return buf; -} - - -// ============================================================ -// dvxInvalidateRect -// ============================================================ - -void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) { - // Convert from content-relative to screen coordinates - int32_t screenX = win->x + win->contentX + x; - int32_t screenY = win->y + win->contentY + y; - - dirtyListAdd(&ctx->dirty, screenX, screenY, w, h); -} - - -// ============================================================ -// dvxInvalidateWindow -// ============================================================ - -void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) { - // Call the window's paint callback to update the content buffer - // before marking the screen dirty. This means raw-paint apps only - // need to call dvxInvalidateWindow -- onPaint fires automatically. - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); - } - - win->contentDirty = true; - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); -} - - -// ============================================================ -// dvxMaximizeWindow -// ============================================================ - -void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) { - wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win); -} - - -// ============================================================ -// dvxMinimizeWindow -// ============================================================ - -void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) { - wmMinimize(&ctx->stack, &ctx->dirty, win); -} - - -// ============================================================ -// dvxQuit -// ============================================================ - -void dvxQuit(AppContextT *ctx) { - ctx->running = false; -} - - -// ============================================================ -// dvxSetBusy -// ============================================================ - -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); - - // Flush immediately so the cursor change is visible before - // the blocking operation starts (or after it ends). - // Skip if display isn't initialized yet (startup wallpaper load). - if (ctx->display.backBuf) { - compositeAndFlush(ctx); - } -} - - -// ============================================================ -// dvxRun -// ============================================================ - -void dvxRun(AppContextT *ctx) { - while (dvxUpdate(ctx)) { - // dvxUpdate returns false when the GUI wants to exit - } -} - - -// ============================================================ -// dvxUpdate -// ============================================================ -// -// Single iteration of the main event loop. This is the fundamental -// heartbeat of the GUI. The sequence is: -// 1. Poll hardware (mouse position/buttons, keyboard buffer) -// 2. Dispatch events (route input to windows, menus, widgets) -// 3. Update tooltip visibility -// 4. Poll ANSI terminal widgets (check for new data from PTYs) -// 5. Periodic tasks (minimized icon thumbnail refresh) -// 6. Composite dirty regions and flush to LFB -// 7. If nothing was dirty: run idle callback or yield CPU -// -// The idle callback mechanism exists so applications can do background -// work (e.g., polling serial ports, processing network data) when the -// GUI has nothing to paint. Without it, the loop would busy-wait or -// yield the CPU slice. With it, the application gets a callback to do -// useful work. platformYield is the fallback -- it calls INT 28h (DOS -// idle) or SDL_Delay (Linux) to avoid burning CPU when truly idle. - -bool dvxUpdate(AppContextT *ctx) { - if (!ctx->running) { - return false; - } - - pollMouse(ctx); - pollKeyboard(ctx); - dispatchEvents(ctx); - updateTooltip(ctx); - pollWidgets(ctx); - if (sCursorBlinkFn) { - sCursorBlinkFn(); - } - - wgtUpdateTimers(); - - ctx->frameCount++; - - if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) { - refreshMinimizedIcons(ctx); - } - - if (ctx->dirty.count > 0) { - compositeAndFlush(ctx); - } else if (ctx->idleCallback) { - ctx->idleCallback(ctx->idleCtx); - } else { - platformYield(); - } - - // Release key-pressed button after one frame. The button was set to - // "pressed" state in dispatchAccelKey; here we clear it and fire - // onClick. The one-frame delay ensures the pressed visual state - // renders before the callback runs (which may open a dialog, etc.). - if (sKeyPressedBtn) { - if (wclsHas(sKeyPressedBtn, WGT_METHOD_ON_DRAG_END)) { - // Pass button center as coordinates so bounds check succeeds and onClick fires - WidgetT *root = sKeyPressedBtn->window ? sKeyPressedBtn->window->widgetRoot : sKeyPressedBtn; - wclsOnDragEnd(sKeyPressedBtn, root, - sKeyPressedBtn->x + sKeyPressedBtn->w / 2, - sKeyPressedBtn->y + sKeyPressedBtn->h / 2); - } - - wgtInvalidate(sKeyPressedBtn); - sKeyPressedBtn = NULL; - } - - ctx->prevMouseX = ctx->mouseX; - ctx->prevMouseY = ctx->mouseY; - ctx->prevMouseButtons = ctx->mouseButtons; - - return ctx->running; -} - - -// ============================================================ -// dvxSaveImage -// ============================================================ -// -// Save native-format pixel data to a PNG file. Converts from the -// display's native pixel format to RGB, then encodes as PNG via -// stb_image_write. This is the general-purpose image save function; -// dvxScreenshot and dvxWindowScreenshot are convenience wrappers -// around it for common use cases. - -int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) { - if (!ctx || !data || !path || w <= 0 || h <= 0) { - return -1; - } - - uint8_t *rgb = bufferToRgb(&ctx->display, data, w, h, pitch); - - if (!rgb) { - return -1; - } - - int32_t result = stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS) ? 0 : -1; - - free(rgb); - - return result; -} - - -// ============================================================ -// dvxScreenshot -// ============================================================ -// -// Save the entire screen (backbuffer) to a PNG file. Uses the backbuffer -// rather than the LFB because reading from video memory through PCI/ISA -// is extremely slow on period hardware (uncacheable MMIO reads). The -// backbuffer is in system RAM and is always coherent with the LFB since -// we only write to the LFB, never read. - -int32_t dvxScreenshot(AppContextT *ctx, const char *path) { - DisplayT *d = &ctx->display; - uint8_t *rgb = bufferToRgb(d, d->backBuf, d->width, d->height, d->pitch); - - if (!rgb) { - return -1; - } - - int32_t result = stbi_write_png(path, d->width, d->height, RGB_CHANNELS, rgb, d->width * RGB_CHANNELS) ? 0 : -1; - - free(rgb); - - return result; -} - - -// ============================================================ -// dvxSetMouseConfig -// ============================================================ - -void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold) { - ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; - ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000; - sDblClickTicks = ctx->dblClickTicks; - - if (accelThreshold > 0) { - platformMouseSetAccel(accelThreshold); - } -} - - -// ============================================================ -// dvxShutdown -// ============================================================ - -void dvxShutdown(AppContextT *ctx) { - // Destroy all remaining windows - while (ctx->stack.count > 0) { - 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); - ctx->wallpaperBuf = NULL; - arrfree(ctx->videoModes); - ctx->videoModes = NULL; - ctx->videoModeCount = 0; - videoShutdown(&ctx->display); -} - - -// ============================================================ -// dvxSetTitle -// ============================================================ - -void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) { - wmSetTitle(win, &ctx->dirty, title); - - if (ctx->onTitleChange) { - ctx->onTitleChange(ctx->titleChangeCtx); - } -} - - -// ============================================================ -// dvxSetWindowIcon -// ============================================================ - -int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) { - return wmSetIcon(win, path, &ctx->display); -} - - -// ============================================================ -// dvxWindowScreenshot -// ============================================================ -// -// Save a window's content buffer to a PNG file. Because each window has -// its own persistent content buffer (not a shared backbuffer), this -// captures the full content even if the window is partially or fully -// occluded by other windows. This is a unique advantage of the per-window -// content buffer architecture. - -int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) { - if (!win || !win->contentBuf) { - return -1; - } - - uint8_t *rgb = bufferToRgb(&ctx->display, win->contentBuf, win->contentW, win->contentH, win->contentPitch); - - if (!rgb) { - return -1; - } - - int32_t result = stbi_write_png(path, win->contentW, win->contentH, RGB_CHANNELS, rgb, win->contentW * RGB_CHANNELS) ? 0 : -1; - - free(rgb); - - return result; -} - - -// ============================================================ -// dvxTileWindows -// ============================================================ -// -// Tile windows in a grid. The grid dimensions are chosen so columns = -// ceil(sqrt(n)), which produces a roughly square grid. This is better than -// always using rows or columns because it maximizes the minimum dimension -// of each tile (a 1xN or Nx1 layout makes windows very narrow or short). -// The last row may have fewer windows; those get wider tiles to fill the -// remaining screen width, avoiding dead space. -// -// The integer sqrt is computed by a simple loop rather than calling sqrt() -// to avoid pulling in floating-point math on DJGPP targets. - -void dvxTileWindows(AppContextT *ctx) { - int32_t screenW = ctx->display.width; - int32_t screenH = ctx->display.height; - int32_t count = countVisibleWindows(ctx); - - if (count == 0) { - return; - } - - // Integer ceil(sqrt(count)) for column count - int32_t cols = 1; - - while (cols * cols < count) { - cols++; - } - - int32_t rows = (count + cols - 1) / cols; - int32_t tileW = screenW / cols; - int32_t tileH = screenH / rows; - - if (tileW < MIN_WINDOW_W) { - tileW = MIN_WINDOW_W; - } - - if (tileH < MIN_WINDOW_H) { - tileH = MIN_WINDOW_H; - } - - int32_t slot = 0; - - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; - - if (win->minimized || !win->visible) { - continue; - } - - int32_t row = slot / cols; - int32_t col = slot % cols; - - // Last row: fewer windows get wider tiles - int32_t remaining = count - row * cols; - int32_t rowCols = (remaining < cols) ? remaining : cols; - int32_t cellW = screenW / rowCols; - - repositionWindow(ctx, win, col * cellW, row * tileH, cellW, tileH); - - slot++; - } -} - - -// ============================================================ -// dvxTileWindowsH -// ============================================================ -// -// Horizontal tiling: windows side by side left to right, each the full -// screen height. Good for comparing two documents or viewing output -// alongside source. With many windows the tiles become very narrow, but -// MIN_WINDOW_W prevents them from becoming unusably small. - -void dvxTileWindowsH(AppContextT *ctx) { - int32_t screenW = ctx->display.width; - int32_t screenH = ctx->display.height; - int32_t count = countVisibleWindows(ctx); - - if (count == 0) { - return; - } - - int32_t tileW = screenW / count; - - if (tileW < MIN_WINDOW_W) { - tileW = MIN_WINDOW_W; - } - - int32_t slot = 0; - - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; - - if (win->minimized || !win->visible) { - continue; - } - - repositionWindow(ctx, win, slot * tileW, 0, tileW, screenH); - - slot++; - } -} - - -// ============================================================ -// dvxTileWindowsV -// ============================================================ -// -// Vertical tiling: windows stacked top to bottom, each the full screen -// width. The complement of dvxTileWindowsH. - -void dvxTileWindowsV(AppContextT *ctx) { - int32_t screenW = ctx->display.width; - int32_t screenH = ctx->display.height; - int32_t count = countVisibleWindows(ctx); - - if (count == 0) { - return; - } - - int32_t tileH = screenH / count; - - if (tileH < MIN_WINDOW_H) { - tileH = MIN_WINDOW_H; - } - - int32_t slot = 0; - - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; - - if (win->minimized || !win->visible) { - continue; - } - - repositionWindow(ctx, win, 0, slot * tileH, screenW, tileH); - - slot++; - } -} - - -// ============================================================ -// interactiveScreenshot -- snapshot screen, prompt for save path -// ============================================================ - -static void interactiveScreenshot(AppContextT *ctx) { - FileFilterT filters[] = { - { "PNG Images (*.png)", "*.png" }, - { "BMP Images (*.bmp)", "*.bmp" } - }; - char path[260]; - - 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 (scrBuf) { - memcpy(scrBuf, ctx->display.backBuf, scrSize); - - if (dvxFileDialog(ctx, "Save Screenshot", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { - dvxSaveImage(ctx, scrBuf, scrW, scrH, scrPitch, path); - } - - free(scrBuf); - } -} - - -// ============================================================ -// interactiveWindowScreenshot -- snapshot window content, prompt for save path -// ============================================================ - -static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) { - if (!win || !win->contentBuf) { - return; - } - - FileFilterT filters[] = { - { "PNG Images (*.png)", "*.png" }, - { "BMP Images (*.bmp)", "*.bmp" } - }; - char path[260]; - - 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))) { - dvxSaveImage(ctx, capBuf, capW, capH, capPitch, path); - } - - free(capBuf); - } -} - - // ============================================================ // executeSysMenuCmd // ============================================================ @@ -3440,6 +1933,68 @@ static void initColorScheme(AppContextT *ctx) { } +// ============================================================ +// interactiveScreenshot -- snapshot screen, prompt for save path +// ============================================================ + +static void interactiveScreenshot(AppContextT *ctx) { + FileFilterT filters[] = { + { "PNG Images (*.png)", "*.png" }, + { "BMP Images (*.bmp)", "*.bmp" } + }; + char path[260]; + + 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 (scrBuf) { + memcpy(scrBuf, ctx->display.backBuf, scrSize); + + if (dvxFileDialog(ctx, "Save Screenshot", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { + dvxSaveImage(ctx, scrBuf, scrW, scrH, scrPitch, path); + } + + free(scrBuf); + } +} + + +// ============================================================ +// interactiveWindowScreenshot -- snapshot window content, prompt for save path +// ============================================================ + +static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) { + if (!win || !win->contentBuf) { + return; + } + + FileFilterT filters[] = { + { "PNG Images (*.png)", "*.png" }, + { "BMP Images (*.bmp)", "*.bmp" } + }; + char path[260]; + + 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))) { + dvxSaveImage(ctx, capBuf, capW, capH, capPitch, path); + } + + free(capBuf); + } +} + + // ============================================================ // invalidateAllWindows -- repaint all windows and desktop // ============================================================ @@ -3726,59 +2281,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) { } -// ============================================================ -// pollWidgets -- poll widgets that need periodic updates -// ============================================================ -// -// Widgets with WCLASS_NEEDS_POLL (e.g. ANSI terminals with asynchronous -// data sources) need periodic polling between frames. This function walks -// every window's widget tree and calls the poll vtable method on matching -// widgets. The poll method handles data ingestion and targeted dirty -// rect generation for efficient repainting. - -static void pollWidgets(AppContextT *ctx) { - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; - - if (win->widgetRoot) { - pollWidgetsWalk(ctx, win->widgetRoot, win); - } - } -} - - -// ============================================================ -// pollWidgetsWalk -- recursive helper -// ============================================================ - -static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) { - if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL) && wclsHas(w, WGT_METHOD_POLL)) { - wclsPoll(w, win); - - // If the poll dirtied internal state and the widget supports - // quickRepaint, render the dirty rows directly into the window's - // content buffer and add the affected area to the global dirty list. - if (wclsHas(w, WGT_METHOD_QUICK_REPAINT)) { - int32_t dirtyY = 0; - int32_t dirtyH = 0; - - if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) { - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t rectX = win->x + win->contentX; - int32_t rectY = win->y + win->contentY + dirtyY - scrollY; - int32_t rectW = win->contentW; - win->contentDirty = true; - dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); - } - } - } - - for (WidgetT *child = w->firstChild; child; child = child->nextSibling) { - pollWidgetsWalk(ctx, child, win); - } -} - - // ============================================================ // pollKeyboard // ============================================================ @@ -4527,6 +3029,59 @@ static void pollMouse(AppContextT *ctx) { } +// ============================================================ +// pollWidgets -- poll widgets that need periodic updates +// ============================================================ +// +// Widgets with WCLASS_NEEDS_POLL (e.g. ANSI terminals with asynchronous +// data sources) need periodic polling between frames. This function walks +// every window's widget tree and calls the poll vtable method on matching +// widgets. The poll method handles data ingestion and targeted dirty +// rect generation for efficient repainting. + +static void pollWidgets(AppContextT *ctx) { + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->widgetRoot) { + pollWidgetsWalk(ctx, win->widgetRoot, win); + } + } +} + + +// ============================================================ +// pollWidgetsWalk -- recursive helper +// ============================================================ + +static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) { + if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL) && wclsHas(w, WGT_METHOD_POLL)) { + wclsPoll(w, win); + + // If the poll dirtied internal state and the widget supports + // quickRepaint, render the dirty rows directly into the window's + // content buffer and add the affected area to the global dirty list. + if (wclsHas(w, WGT_METHOD_QUICK_REPAINT)) { + int32_t dirtyY = 0; + int32_t dirtyH = 0; + + if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) { + int32_t scrollY = win->vScroll ? win->vScroll->value : 0; + int32_t rectX = win->x + win->contentX; + int32_t rectY = win->y + win->contentY + dirtyY - scrollY; + int32_t rectW = win->contentW; + win->contentDirty = true; + dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); + } + } + } + + for (WidgetT *child = w->firstChild; child; child = child->nextSibling) { + pollWidgetsWalk(ctx, child, win); + } +} + + // ============================================================ // refreshMinimizedIcons // ============================================================ @@ -4879,3 +3434,1456 @@ static void updateTooltip(AppContextT *ctx) { // Dirty the tooltip area dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, tw, th); } + + +// ============================================================ +// writePixel -- write a packed pixel to a buffer at position x +// ============================================================ + +static void writePixel(uint8_t *row, int32_t x, uint32_t px, int32_t bpp) { + if (bpp == 8) { + row[x] = (uint8_t)px; + } else if (bpp == 15 || bpp == 16) { + ((uint16_t *)row)[x] = (uint16_t)px; + } else { + ((uint32_t *)row)[x] = px; + } +} + + +// ============================================================ +// dvxAddAccel +// ============================================================ +// +// Accelerator entries are pre-normalized at registration time: the key +// is uppercased and modifier bits are masked to just Ctrl|Alt. This +// moves the normalization cost from the hot path (every keypress) to +// the cold path (one-time setup), so checkAccelTable can do a simple +// integer compare per entry. + +void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) { + if (!table) { + 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; + + if (normKey >= 'a' && normKey <= 'z') { + normKey = normKey - 32; + } + + AccelEntryT *e = &table->entries[table->count++]; + e->key = key; + e->modifiers = modifiers; + e->cmdId = cmdId; + e->normKey = normKey; + e->normMods = modifiers & (ACCEL_CTRL | ACCEL_ALT); +} + + +// ============================================================ +// dvxApplyColorScheme +// ============================================================ + +void dvxApplyColorScheme(AppContextT *ctx) { + DisplayT *d = &ctx->display; + + for (int32_t i = 0; i < ColorCountE; i++) { + *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); +} + + +// ============================================================ +// dvxCascadeWindows +// ============================================================ +// +// Arranges windows in the classic cascade pattern: each window is the same +// size (2/3 of screen), offset diagonally by the title bar height so each +// title bar remains visible. When the cascade would go off-screen, it wraps +// back to (0,0). This matches the Windows 3.x cascade behavior. +// The step size is title_height + border_width so exactly one title bar's +// worth of the previous window peeks out above and to the left. + +void dvxCascadeWindows(AppContextT *ctx) { + int32_t screenW = ctx->display.width; + 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 winW = screenW * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM; + int32_t winH = screenH * CASCADE_SIZE_NUMER / CASCADE_SIZE_DENOM; + + if (winW < MIN_WINDOW_W) { + winW = MIN_WINDOW_W; + } + + if (winH < MIN_WINDOW_H) { + winH = MIN_WINDOW_H; + } + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->minimized || !win->visible) { + continue; + } + + repositionWindow(ctx, win, offsetX, offsetY, winW, winH); + + offsetX += step; + offsetY += step; + + // Wrap around if we'd go off screen + if (offsetX + winW > screenW || offsetY + winH > screenH) { + offsetX = 0; + offsetY = 0; + } + } +} + + +// ============================================================ +// dvxChangeVideoMode +// ============================================================ +// +// Live video mode switch. Saves the old display state, attempts to +// set the new mode, and if successful, reinitializes all dependent +// subsystems: blit ops, colors, cursors, mouse range, wallpaper, +// and all window content buffers. Windows larger than the new +// screen are clamped. On failure, the old mode is restored. + +int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { + // Save old state for rollback + DisplayT oldDisplay = ctx->display; + + // Stash old wallpaper (don't free -- we may need it for rollback) + uint8_t *oldWpBuf = ctx->wallpaperBuf; + int32_t oldWpPitch = ctx->wallpaperPitch; + ctx->wallpaperBuf = NULL; + ctx->wallpaperPitch = 0; + + // Free old video buffers (no text mode restore) + platformVideoFreeBuffers(&ctx->display); + + // Try the new mode + if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { + // Restore old mode + ctx->display = oldDisplay; + ctx->display.backBuf = NULL; + ctx->display.palette = NULL; + + if (videoInit(&ctx->display, oldDisplay.width, oldDisplay.height, oldDisplay.format.bitsPerPixel) != 0) { + // Both failed -- catastrophic + return -1; + } + + // Restore wallpaper + ctx->wallpaperBuf = oldWpBuf; + ctx->wallpaperPitch = oldWpPitch; + drawInit(&ctx->blitOps, &ctx->display); + dvxApplyColorScheme(ctx); + return -1; + } + + // New mode succeeded -- free old wallpaper buffer + free(oldWpBuf); + + // Reinit blit ops for new pixel format + drawInit(&ctx->blitOps, &ctx->display); + + // Repack all colors for new pixel format + dvxApplyColorScheme(ctx); + + // Reinit mouse range + platformMouseInit(ctx->display.width, ctx->display.height); + ctx->hasMouseWheel = platformMouseWheelInit(); + + // Clamp mouse position to new screen + if (ctx->mouseX >= ctx->display.width) { + ctx->mouseX = ctx->display.width - 1; + } + + if (ctx->mouseY >= ctx->display.height) { + ctx->mouseY = ctx->display.height - 1; + } + + // Clamp and reallocate all window content buffers + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + // Clamp window position to new screen bounds + if (win->x + win->w > ctx->display.width) { + win->x = ctx->display.width - win->w; + } + + if (win->x < 0) { + win->x = 0; + win->w = ctx->display.width; + } + + if (win->y + win->h > ctx->display.height) { + win->y = ctx->display.height - win->h; + } + + if (win->y < 0) { + win->y = 0; + win->h = ctx->display.height; + } + + // 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->contentDirty = true; + } + } + + // Reload wallpaper at the new resolution/bpp + if (ctx->wallpaperPath[0]) { + dvxSetWallpaper(ctx, ctx->wallpaperPath); + } + + // Reset clip and dirty the full screen + resetClipRect(&ctx->display); + dirtyListInit(&ctx->dirty); + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); + + return 0; +} + + +// ============================================================ +// dvxClipboardCopy / dvxClipboardGet +// ============================================================ +// +// The clipboard is a simple in-process text buffer managed by the +// platform layer (clipboardCopy/clipboardGet). There is no inter-process +// clipboard because DVX runs as a single-process windowing system -- all +// windows share the same address space. The thin wrappers here exist to +// keep the platform layer out of application code's include path. + +void dvxClipboardCopy(const char *text, int32_t len) { + clipboardCopy(text, len); +} + + +const char *dvxClipboardGet(int32_t *outLen) { + return clipboardGet(outLen); +} + + +// ============================================================ +// dvxColorLabel +// ============================================================ + +const char *dvxColorLabel(ColorIdE id) { + if (id < 0 || id >= ColorCountE) { + return "Unknown"; + } + + return sColorLabels[id]; +} + + +// ============================================================ +// dvxColorName +// ============================================================ + +const char *dvxColorName(ColorIdE id) { + if (id < 0 || id >= ColorCountE) { + return "unknown"; + } + + return sColorNames[id]; +} + + +// ============================================================ +// dvxCreateAccelTable +// ============================================================ + +AccelTableT *dvxCreateAccelTable(void) { + AccelTableT *table = (AccelTableT *)calloc(1, sizeof(AccelTableT)); + return table; +} + + +// ============================================================ +// dvxCreateWindow +// ============================================================ + +WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { + // Auto-cascade: if another window already occupies this exact position, + // 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; + + for (;;) { + bool collision = false; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *other = ctx->stack.windows[i]; + + if (other->x == x && other->y == y) { + collision = true; + break; + } + } + + if (!collision) { + break; + } + + x += step; + y += step; + + if (x + w > ctx->display.width || y + h > ctx->display.height) { + x = step; + y = step; + break; + } + } + + // Clamp to screen so the window is fully visible + if (x + w > ctx->display.width) { + x = ctx->display.width - w; + } + + if (y + h > ctx->display.height) { + y = ctx->display.height - h; + } + + if (x < 0) { + x = 0; + } + + if (y < 0) { + y = 0; + } + + WindowT *win = wmCreateWindow(&ctx->stack, &ctx->display, title, x, y, w, h, resizable); + + if (win) { + // Stamp window with the current app ID (set by the shell before + // calling app entry points). Enables per-app window tracking for + // cleanup on crash/termination. + win->appId = ctx->currentAppId; + + // Raise and focus + int32_t idx = ctx->stack.count - 1; + wmSetFocus(&ctx->stack, &ctx->dirty, idx); + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + } + + return win; +} + + +// ============================================================ +// dvxCreateWindowCentered +// ============================================================ + +WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable) { + int32_t x = (ctx->display.width - w) / 2; + int32_t y = (ctx->display.height - h) / 2; + + return dvxCreateWindow(ctx, title, x, y, w, h, resizable); +} + + +// ============================================================ +// dvxDestroyWindow +// ============================================================ + +void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + + // If the window is minimized, dirty the icon strip so the icon + // disappears and remaining icons repack correctly. + 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); + } + + wmDestroyWindow(&ctx->stack, win); + + // Dirty icon area again with the updated count (one fewer icon) + 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); + } + + // Focus the new top window + if (ctx->stack.count > 0) { + wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); + } +} + + +// ============================================================ +// dvxFitWindow +// ============================================================ +// +// Resizes a window to exactly fit its widget tree's minimum size, +// accounting for chrome overhead (title bar, borders, optional menu bar). +// Used after building a dialog's widget tree to size the dialog +// automatically rather than requiring the caller to compute sizes manually. + +void dvxFitWindow(AppContextT *ctx, WindowT *win) { + if (!ctx || !win || !win->widgetRoot) { + return; + } + + // Measure the widget tree to get minimum content size + widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); + + int32_t contentW = win->widgetRoot->calcMinW; + int32_t contentH = win->widgetRoot->calcMinH; + + // Compute chrome overhead + int32_t topChrome = CHROME_TOTAL_TOP; + if (win->menuBar) { + topChrome += CHROME_MENU_HEIGHT; + } + + int32_t newW = contentW + CHROME_TOTAL_SIDE * 2; + int32_t newH = contentH + topChrome + CHROME_TOTAL_BOTTOM; + + // Dirty old position + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + + // Resize + win->w = newW; + win->h = newH; + + // Shift position so the window stays fully on screen + if (win->x + newW > ctx->display.width) { + win->x = ctx->display.width - newW; + } + + if (win->y + newH > ctx->display.height) { + win->y = ctx->display.height - newH; + } + + if (win->x < 0) { + win->x = 0; + } + + if (win->y < 0) { + win->y = 0; + } + + wmUpdateContentRect(win); + wmReallocContentBuf(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); +} + + +// ============================================================ +// dvxFreeAccelTable +// ============================================================ + +void dvxFreeAccelTable(AccelTableT *table) { + if (table) { + free(table->entries); + } + + free(table); +} + + +// ============================================================ +// dvxFreeImage +// ============================================================ + +void dvxFreeImage(uint8_t *data) { + free(data); +} + + +// ============================================================ +// dvxGetBlitOps +// ============================================================ + +const BlitOpsT *dvxGetBlitOps(const AppContextT *ctx) { + return &ctx->blitOps; +} + + +// ============================================================ +// dvxGetColor +// ============================================================ + +void dvxGetColor(const AppContextT *ctx, ColorIdE id, uint8_t *r, uint8_t *g, uint8_t *b) { + if (id < 0 || id >= ColorCountE) { + *r = *g = *b = 0; + return; + } + + *r = ctx->colorRgb[id][0]; + *g = ctx->colorRgb[id][1]; + *b = ctx->colorRgb[id][2]; +} + + +// ============================================================ +// dvxGetColors +// ============================================================ + +const ColorSchemeT *dvxGetColors(const AppContextT *ctx) { + return &ctx->colors; +} + + +// ============================================================ +// dvxGetDisplay +// ============================================================ + +DisplayT *dvxGetDisplay(AppContextT *ctx) { + return &ctx->display; +} + + +// ============================================================ +// dvxGetFont +// ============================================================ + +const BitmapFontT *dvxGetFont(const AppContextT *ctx) { + return &ctx->font; +} + + +// ============================================================ +// dvxGetVideoModes +// ============================================================ + +const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, int32_t *count) { + *count = ctx->videoModeCount; + return ctx->videoModes; +} + + +// ============================================================ +// dvxInit +// ============================================================ +// +// One-shot initialization of all GUI subsystems. The layered init order +// matters: video must be up before draw ops can be selected (since draw +// ops depend on pixel format), and colors must be packed after the +// display format is known. + +int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { + memset(ctx, 0, sizeof(*ctx)); + + platformInit(); + + // 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; + platformVideoEnumModes(enumModeCb, ctx); + + if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { + return -1; + } + + // Draw ops are pixel-format-dependent function pointers (e.g., 16bpp + // vs 32bpp span fill). Selected once here, then used everywhere. + drawInit(&ctx->blitOps, &ctx->display); + + wmInit(&ctx->stack); + dirtyListInit(&ctx->dirty); + + // 8x16 is the only font size currently supported. Fixed-width bitmap + // fonts are used throughout because variable-width text measurement + // would add complexity and cost on every text draw without much + // benefit at 640x480 resolution. + ctx->font = dvxFont8x16; + + memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors)); + ctx->cursorId = CURSOR_ARROW; + + 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; + ctx->mouseY = ctx->display.height / 2; + ctx->prevMouseX = ctx->mouseX; + ctx->prevMouseY = ctx->mouseY; + + ctx->running = true; + ctx->lastIconClickId = -1; + ctx->lastIconClickTime = 0; + ctx->lastCloseClickId = -1; + ctx->lastCloseClickTime = 0; + ctx->lastTitleClickId = -1; + ctx->lastTitleClickTime = 0; + ctx->wheelDirection = 1; + ctx->dblClickTicks = DBLCLICK_THRESHOLD; + sDblClickTicks = DBLCLICK_THRESHOLD; + + // Pre-compute fixed-point 16.16 reciprocal of character height so + // popup menu item index calculation can use multiply+shift instead + // of division. On a 486, integer divide is 40+ cycles; this + // reciprocal trick reduces it to ~10 cycles (imul + shr). + ctx->charHeightRecip = ((uint32_t)FP_ONE + (uint32_t)ctx->font.charHeight - 1) / (uint32_t)ctx->font.charHeight; + + // Dirty the entire screen so the first compositeAndFlush paints everything + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); + + return 0; +} + + +// ============================================================ +// dvxInvalidateRect +// ============================================================ + +void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) { + // Convert from content-relative to screen coordinates + int32_t screenX = win->x + win->contentX + x; + int32_t screenY = win->y + win->contentY + y; + + dirtyListAdd(&ctx->dirty, screenX, screenY, w, h); +} + + +// ============================================================ +// dvxInvalidateWindow +// ============================================================ + +void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) { + // Call the window's paint callback to update the content buffer + // before marking the screen dirty. This means raw-paint apps only + // need to call dvxInvalidateWindow -- onPaint fires automatically. + if (win->onPaint) { + RectT fullRect = {0, 0, win->contentW, win->contentH}; + WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); + } + + win->contentDirty = true; + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); +} + + +// ============================================================ +// dvxLoadImage +// ============================================================ +// +// Public image loading API. Loads any image file supported by stb_image +// (BMP, PNG, JPEG, GIF, etc.) and converts the RGB pixels to the +// display's native pixel format for direct use with rectCopy, wgtImage, +// wgtImageButton, or any other pixel-data consumer. The caller owns the +// returned buffer and must free it with dvxFreeImage(). + +uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) { + if (!ctx || !path) { + return NULL; + } + + const DisplayT *d = &ctx->display; + + int imgW; + int imgH; + int channels; + uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); + + if (!rgb) { + return NULL; + } + + int32_t bpp = d->format.bytesPerPixel; + int32_t pitch = imgW * bpp; + uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + + if (!buf) { + stbi_image_free(rgb); + return NULL; + } + + for (int32_t y = 0; y < imgH; y++) { + for (int32_t x = 0; x < imgW; x++) { + const uint8_t *src = rgb + (y * imgW + x) * RGB_CHANNELS; + uint32_t color = packColor(d, src[0], src[1], src[2]); + uint8_t *dst = buf + y * pitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + + stbi_image_free(rgb); + + if (outW) { + *outW = imgW; + } + + if (outH) { + *outH = imgH; + } + + if (outPitch) { + *outPitch = pitch; + } + + return buf; +} + + +// ============================================================ +// dvxLoadImageFromMemory +// ============================================================ +// +// Same as dvxLoadImage but loads from a memory buffer (e.g. a +// resource read via dvxResRead). Supports BMP, PNG, JPEG, GIF. + +uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) { + if (!ctx || !data || dataLen <= 0) { + return NULL; + } + + const DisplayT *d = &ctx->display; + + int imgW; + int imgH; + int channels; + uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, 3); + + if (!rgb) { + return NULL; + } + + int32_t bpp = d->format.bytesPerPixel; + int32_t pitch = imgW * bpp; + uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + + if (!buf) { + stbi_image_free(rgb); + return NULL; + } + + for (int32_t y = 0; y < imgH; y++) { + for (int32_t x = 0; x < imgW; x++) { + const uint8_t *src = rgb + (y * imgW + x) * 3; + uint32_t color = packColor(d, src[0], src[1], src[2]); + uint8_t *dst = buf + y * pitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + + stbi_image_free(rgb); + + if (outW) { + *outW = imgW; + } + + if (outH) { + *outH = imgH; + } + + if (outPitch) { + *outPitch = pitch; + } + + return buf; +} + + +// ============================================================ +// dvxLoadTheme +// ============================================================ + +bool dvxLoadTheme(AppContextT *ctx, const char *filename) { + FILE *fp = fopen(filename, "rb"); + + if (!fp) { + return false; + } + + char line[256]; + + while (fgets(line, sizeof(line), fp)) { + // Strip trailing whitespace + char *end = line + strlen(line) - 1; + + while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ')) { + *end-- = '\0'; + } + + char *p = line; + + while (*p == ' ' || *p == '\t') { + p++; + } + + // Skip comments, blank lines, section headers + if (*p == '\0' || *p == ';' || *p == '#' || *p == '[') { + continue; + } + + // Parse key = r,g,b + char *eq = strchr(p, '='); + + if (!eq) { + continue; + } + + *eq = '\0'; + + // Trim key + char *key = p; + char *keyEnd = eq - 1; + + while (keyEnd >= key && (*keyEnd == ' ' || *keyEnd == '\t')) { + *keyEnd-- = '\0'; + } + + // Parse r,g,b + char *val = eq + 1; + + while (*val == ' ' || *val == '\t') { + val++; + } + + int32_t r; + int32_t g; + int32_t b; + + if (sscanf(val, "%d,%d,%d", &r, &g, &b) != 3) { + continue; + } + + // 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; + break; + } + } + } + + fclose(fp); + dvxApplyColorScheme(ctx); + return true; +} + + +// ============================================================ +// dvxMaximizeWindow +// ============================================================ + +void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) { + wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win); +} + + +// ============================================================ +// dvxMinimizeWindow +// ============================================================ + +void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) { + wmMinimize(&ctx->stack, &ctx->dirty, win); +} + + +// ============================================================ +// dvxQuit +// ============================================================ + +void dvxQuit(AppContextT *ctx) { + ctx->running = false; +} + + +// ============================================================ +// dvxResetColorScheme +// ============================================================ + +void dvxResetColorScheme(AppContextT *ctx) { + memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors)); + dvxApplyColorScheme(ctx); +} + + +// ============================================================ +// dvxRun +// ============================================================ + +void dvxRun(AppContextT *ctx) { + while (dvxUpdate(ctx)) { + // dvxUpdate returns false when the GUI wants to exit + } +} + + +// ============================================================ +// dvxSaveImage +// ============================================================ +// +// Save native-format pixel data to a PNG file. Converts from the +// display's native pixel format to RGB, then encodes as PNG via +// stb_image_write. This is the general-purpose image save function; +// dvxScreenshot and dvxWindowScreenshot are convenience wrappers +// around it for common use cases. + +int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) { + if (!ctx || !data || !path || w <= 0 || h <= 0) { + return -1; + } + + uint8_t *rgb = bufferToRgb(&ctx->display, data, w, h, pitch); + + if (!rgb) { + return -1; + } + + int32_t result = stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS) ? 0 : -1; + + free(rgb); + + return result; +} + + +// ============================================================ +// dvxSaveTheme +// ============================================================ + +bool dvxSaveTheme(const AppContextT *ctx, const char *filename) { + FILE *fp = fopen(filename, "wb"); + + if (!fp) { + return false; + } + + fprintf(fp, "; DVX Color Theme\r\n\r\n[colors]\r\n"); + + for (int32_t i = 0; i < ColorCountE; i++) { + fprintf(fp, "%-20s = %d,%d,%d\r\n", sColorNames[i], ctx->colorRgb[i][0], ctx->colorRgb[i][1], ctx->colorRgb[i][2]); + } + + fclose(fp); + return true; +} + + +// ============================================================ +// dvxScreenshot +// ============================================================ +// +// Save the entire screen (backbuffer) to a PNG file. Uses the backbuffer +// rather than the LFB because reading from video memory through PCI/ISA +// is extremely slow on period hardware (uncacheable MMIO reads). The +// backbuffer is in system RAM and is always coherent with the LFB since +// we only write to the LFB, never read. + +int32_t dvxScreenshot(AppContextT *ctx, const char *path) { + DisplayT *d = &ctx->display; + uint8_t *rgb = bufferToRgb(d, d->backBuf, d->width, d->height, d->pitch); + + if (!rgb) { + return -1; + } + + int32_t result = stbi_write_png(path, d->width, d->height, RGB_CHANNELS, rgb, d->width * RGB_CHANNELS) ? 0 : -1; + + free(rgb); + + return result; +} + + +// ============================================================ +// dvxSetBusy +// ============================================================ + +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); + + // Flush immediately so the cursor change is visible before + // the blocking operation starts (or after it ends). + // Skip if display isn't initialized yet (startup wallpaper load). + if (ctx->display.backBuf) { + compositeAndFlush(ctx); + } +} + + +// ============================================================ +// dvxSetColor +// ============================================================ + +void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b) { + if (id < 0 || id >= ColorCountE) { + return; + } + + ctx->colorRgb[id][0] = r; + ctx->colorRgb[id][1] = g; + ctx->colorRgb[id][2] = 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); +} + + +// ============================================================ +// dvxSetMouseConfig +// ============================================================ + +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold) { + ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; + ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000; + sDblClickTicks = ctx->dblClickTicks; + + if (accelThreshold > 0) { + platformMouseSetAccel(accelThreshold); + } +} + + +// ============================================================ +// dvxSetTitle +// ============================================================ + +void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) { + wmSetTitle(win, &ctx->dirty, title); + + if (ctx->onTitleChange) { + ctx->onTitleChange(ctx->titleChangeCtx); + } +} + + +// ============================================================ +// dvxSetWallpaper +// ============================================================ + +bool dvxSetWallpaper(AppContextT *ctx, const char *path) { + free(ctx->wallpaperBuf); + ctx->wallpaperBuf = NULL; + ctx->wallpaperPitch = 0; + + if (!path) { + ctx->wallpaperPath[0] = '\0'; + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); + return true; + } + + strncpy(ctx->wallpaperPath, path, sizeof(ctx->wallpaperPath) - 1); + ctx->wallpaperPath[sizeof(ctx->wallpaperPath) - 1] = '\0'; + + dvxSetBusy(ctx, true); + + int32_t imgW; + int32_t imgH; + int32_t channels; + uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); + + if (!rgb) { + dvxSetBusy(ctx, false); + return false; + } + + int32_t pitch = ctx->display.width * ctx->display.format.bytesPerPixel; + + ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode); + ctx->wallpaperPitch = pitch; + dvxSetBusy(ctx, false); + + stbi_image_free(rgb); + + if (!ctx->wallpaperBuf) { + return false; + } + + dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); + return true; +} + + +// ============================================================ +// dvxSetWallpaperMode +// ============================================================ + +void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode) { + ctx->wallpaperMode = mode; + + if (ctx->wallpaperPath[0]) { + dvxSetWallpaper(ctx, ctx->wallpaperPath); + } +} + + +// ============================================================ +// dvxSetWindowIcon +// ============================================================ + +int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) { + return wmSetIcon(win, path, &ctx->display); +} + + +// ============================================================ +// dvxShutdown +// ============================================================ + +void dvxShutdown(AppContextT *ctx) { + // Destroy all remaining windows + while (ctx->stack.count > 0) { + 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); + ctx->wallpaperBuf = NULL; + arrfree(ctx->videoModes); + ctx->videoModes = NULL; + ctx->videoModeCount = 0; + videoShutdown(&ctx->display); +} + + +// ============================================================ +// dvxTileWindows +// ============================================================ +// +// Tile windows in a grid. The grid dimensions are chosen so columns = +// ceil(sqrt(n)), which produces a roughly square grid. This is better than +// always using rows or columns because it maximizes the minimum dimension +// of each tile (a 1xN or Nx1 layout makes windows very narrow or short). +// The last row may have fewer windows; those get wider tiles to fill the +// remaining screen width, avoiding dead space. +// +// The integer sqrt is computed by a simple loop rather than calling sqrt() +// to avoid pulling in floating-point math on DJGPP targets. + +void dvxTileWindows(AppContextT *ctx) { + int32_t screenW = ctx->display.width; + int32_t screenH = ctx->display.height; + int32_t count = countVisibleWindows(ctx); + + if (count == 0) { + return; + } + + // Integer ceil(sqrt(count)) for column count + int32_t cols = 1; + + while (cols * cols < count) { + cols++; + } + + int32_t rows = (count + cols - 1) / cols; + int32_t tileW = screenW / cols; + int32_t tileH = screenH / rows; + + if (tileW < MIN_WINDOW_W) { + tileW = MIN_WINDOW_W; + } + + if (tileH < MIN_WINDOW_H) { + tileH = MIN_WINDOW_H; + } + + int32_t slot = 0; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->minimized || !win->visible) { + continue; + } + + int32_t row = slot / cols; + int32_t col = slot % cols; + + // Last row: fewer windows get wider tiles + int32_t remaining = count - row * cols; + int32_t rowCols = (remaining < cols) ? remaining : cols; + int32_t cellW = screenW / rowCols; + + repositionWindow(ctx, win, col * cellW, row * tileH, cellW, tileH); + + slot++; + } +} + + +// ============================================================ +// dvxTileWindowsH +// ============================================================ +// +// Horizontal tiling: windows side by side left to right, each the full +// screen height. Good for comparing two documents or viewing output +// alongside source. With many windows the tiles become very narrow, but +// MIN_WINDOW_W prevents them from becoming unusably small. + +void dvxTileWindowsH(AppContextT *ctx) { + int32_t screenW = ctx->display.width; + int32_t screenH = ctx->display.height; + int32_t count = countVisibleWindows(ctx); + + if (count == 0) { + return; + } + + int32_t tileW = screenW / count; + + if (tileW < MIN_WINDOW_W) { + tileW = MIN_WINDOW_W; + } + + int32_t slot = 0; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->minimized || !win->visible) { + continue; + } + + repositionWindow(ctx, win, slot * tileW, 0, tileW, screenH); + + slot++; + } +} + + +// ============================================================ +// dvxTileWindowsV +// ============================================================ +// +// Vertical tiling: windows stacked top to bottom, each the full screen +// width. The complement of dvxTileWindowsH. + +void dvxTileWindowsV(AppContextT *ctx) { + int32_t screenW = ctx->display.width; + int32_t screenH = ctx->display.height; + int32_t count = countVisibleWindows(ctx); + + if (count == 0) { + return; + } + + int32_t tileH = screenH / count; + + if (tileH < MIN_WINDOW_H) { + tileH = MIN_WINDOW_H; + } + + int32_t slot = 0; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->minimized || !win->visible) { + continue; + } + + repositionWindow(ctx, win, 0, slot * tileH, screenW, tileH); + + slot++; + } +} + + +// ============================================================ +// dvxUpdate +// ============================================================ +// +// Single iteration of the main event loop. This is the fundamental +// heartbeat of the GUI. The sequence is: +// 1. Poll hardware (mouse position/buttons, keyboard buffer) +// 2. Dispatch events (route input to windows, menus, widgets) +// 3. Update tooltip visibility +// 4. Poll ANSI terminal widgets (check for new data from PTYs) +// 5. Periodic tasks (minimized icon thumbnail refresh) +// 6. Composite dirty regions and flush to LFB +// 7. If nothing was dirty: run idle callback or yield CPU +// +// The idle callback mechanism exists so applications can do background +// work (e.g., polling serial ports, processing network data) when the +// GUI has nothing to paint. Without it, the loop would busy-wait or +// yield the CPU slice. With it, the application gets a callback to do +// useful work. platformYield is the fallback -- it calls INT 28h (DOS +// idle) or SDL_Delay (Linux) to avoid burning CPU when truly idle. + +bool dvxUpdate(AppContextT *ctx) { + if (!ctx->running) { + return false; + } + + pollMouse(ctx); + pollKeyboard(ctx); + dispatchEvents(ctx); + updateTooltip(ctx); + pollWidgets(ctx); + if (sCursorBlinkFn) { + sCursorBlinkFn(); + } + + wgtUpdateTimers(); + + ctx->frameCount++; + + if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) { + refreshMinimizedIcons(ctx); + } + + if (ctx->dirty.count > 0) { + compositeAndFlush(ctx); + } else if (ctx->idleCallback) { + ctx->idleCallback(ctx->idleCtx); + } else { + platformYield(); + } + + // Release key-pressed button after one frame. The button was set to + // "pressed" state in dispatchAccelKey; here we clear it and fire + // onClick. The one-frame delay ensures the pressed visual state + // renders before the callback runs (which may open a dialog, etc.). + if (sKeyPressedBtn) { + if (wclsHas(sKeyPressedBtn, WGT_METHOD_ON_DRAG_END)) { + // Pass button center as coordinates so bounds check succeeds and onClick fires + WidgetT *root = sKeyPressedBtn->window ? sKeyPressedBtn->window->widgetRoot : sKeyPressedBtn; + wclsOnDragEnd(sKeyPressedBtn, root, + sKeyPressedBtn->x + sKeyPressedBtn->w / 2, + sKeyPressedBtn->y + sKeyPressedBtn->h / 2); + } + + wgtInvalidate(sKeyPressedBtn); + sKeyPressedBtn = NULL; + } + + ctx->prevMouseX = ctx->mouseX; + ctx->prevMouseY = ctx->mouseY; + ctx->prevMouseButtons = ctx->mouseButtons; + + return ctx->running; +} + + +// ============================================================ +// dvxWindowScreenshot +// ============================================================ +// +// Save a window's content buffer to a PNG file. Because each window has +// its own persistent content buffer (not a shared backbuffer), this +// captures the full content even if the window is partially or fully +// occluded by other windows. This is a unique advantage of the per-window +// content buffer architecture. + +int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) { + if (!win || !win->contentBuf) { + return -1; + } + + uint8_t *rgb = bufferToRgb(&ctx->display, win->contentBuf, win->contentW, win->contentH, win->contentPitch); + + if (!rgb) { + return -1; + } + + int32_t result = stbi_write_png(path, win->contentW, win->contentH, RGB_CHANNELS, rgb, win->contentW * RGB_CHANNELS) ? 0 : -1; + + free(rgb); + + return result; +} diff --git a/loader/README.md b/loader/README.md index d947862..9eb5747 100644 --- a/loader/README.md +++ b/loader/README.md @@ -34,6 +34,7 @@ libdvx.lib (deps: libtasks) texthelp.lib (deps: libtasks, libdvx) listhelp.lib (deps: libtasks, libdvx) dvxshell.lib (deps: libtasks, libdvx, texthelp, listhelp) +taskmgr.lib (deps: dvxshell, libtasks, libdvx, texthelp, listhelp) ``` ### Phase 2: Widgets (widgets/*.wgt) @@ -73,7 +74,11 @@ void dvxLog(const char *fmt, ...); ### stb_ds -The `STB_DS_IMPLEMENTATION` is compiled into the loader. stb_ds +The `STB_DS_IMPLEMENTATION` is compiled into the loader with +`STBDS_REALLOC` and `STBDS_FREE` overridden to use `dvxRealloc` and +`dvxFree`. This means all `arrput`/`hmput`/`arrfree` calls in DXE +code route through the per-app memory tracker, so stb_ds memory is +attributed correctly in the Task Manager's memory column. stb_ds functions are exported to all DXE modules via the platform export table. @@ -89,6 +94,24 @@ All platform functions are exported to DXE modules. This includes: * Crash: signal handler installation, register dump logging * System: memory info, directory creation, path utilities +### Per-App Memory Tracking + +The DXE export table maps standard C allocation symbols to tracked +wrappers: + +| C Symbol | Mapped To | Effect | +|----------|-----------|--------| +| `malloc` | `dvxMalloc` | Allocations attributed to `currentAppId` | +| `calloc` | `dvxCalloc` | Same | +| `realloc` | `dvxRealloc` | Transfers attribution on resize | +| `free` | `dvxFree` | Decrements app's tracked usage | +| `strdup` | `dvxStrdup` | Tracks the duplicated string | + +This is transparent to DXE code -- apps call `malloc` normally and the +tracked wrapper runs instead. The Task Manager reads per-app usage via +`dvxMemGetAppUsage()`. When an app is reaped, `dvxMemResetApp()` zeroes +its counter. + ## Files diff --git a/shell/README.md b/shell/README.md index c3ed6c1..f8684f4 100644 --- a/shell/README.md +++ b/shell/README.md @@ -14,14 +14,17 @@ are loaded. `shellMain()`: 1. Loads preferences from `CONFIG/DVX.INI` 2. Initializes the GUI via `dvxInit()` with configured video mode 3. Applies saved mouse, color, and wallpaper settings from INI -4. Initializes the cooperative task system (`tsInit()`) -5. Sets shell task (task 0) to `TS_PRIORITY_HIGH` -6. Gathers system information via the platform layer -7. Initializes the app slot table -8. Registers idle callback, Ctrl+Esc handler, and title change handler -9. Loads the desktop app (default: `apps/progman/progman.app`) -10. Installs the crash handler (after init, so init crashes are fatal) -11. Enters the main loop +4. Shows a splash screen ("DVX - DOS Visual eXecutive / Loading...") +5. Initializes the cooperative task system (`tsInit()`) +6. Sets shell task (task 0) to `TS_PRIORITY_HIGH` +7. Gathers system information via the platform layer +8. Initializes the app slot table +9. Points the memory tracker at `currentAppId` for per-app attribution +10. Registers idle callback, Ctrl+Esc handler, and title change handler +11. Installs the crash handler (before loading apps, so init crashes are caught) +12. Loads the desktop app (default: `apps/progman/progman.app`) +13. Dismisses the splash screen +14. Enters the main loop ## Main Loop @@ -60,8 +63,9 @@ during graceful shutdown. `appMain()` is called directly in the shell's task 0. It creates windows, registers callbacks, and returns immediately. The app lives -entirely through GUI callbacks. Lifecycle ends when the last window -closes. +entirely through GUI callbacks. The shell reaps callback-only apps +automatically when their last window closes -- `shellReapApps()` checks +each frame for running callback apps with zero remaining windows. ### Main-Loop Apps (hasMainLoop = true) @@ -87,7 +91,12 @@ Free -> Loaded -> Running -> Terminating -> Free App slots are managed as a stb_ds dynamic array (no fixed max). Each slot tracks: app ID, name, path, DXE handle, state, task ID, entry/ -shutdown function pointers, and the `DxeAppContextT` passed to the app. +shutdown function pointers, and a pointer to the `DxeAppContextT` +passed to the app. + +`DxeAppContextT` is heap-allocated (via `calloc`) so its address is +stable across `sApps` array reallocs -- apps save this pointer in their +static globals and it must not move. The shell frees it during reap. The `DxeAppContextT` gives each app: - `shellCtx` -- pointer to the shell's `AppContextT` @@ -102,6 +111,15 @@ executing. The shell sets this before calling app code. `dvxCreateWindow()` stamps `win->appId` directly so the shell can associate windows with apps for cleanup. +For main-loop apps, `appTaskWrapper` receives the app ID (as an int +cast to `void *`), not a direct pointer to `ShellAppT`. This is because +the `sApps` dynamic array may reallocate between `tsCreate` and the +first time the task runs, which would invalidate a direct pointer. + +The shell calls `dvxSetBusy()` before `dlopen` to show the hourglass +cursor during app loading, and clears it after `appMain` returns (for +callback apps) or after task creation (for main-loop apps). + ## Crash Recovery @@ -121,13 +139,15 @@ This gives Windows 3.1-style fault tolerance -- one bad app does not take down the whole system. -## Task Manager +## Task Manager Integration -The built-in Task Manager is always available via Ctrl+Esc. It shows -all running apps with their names and provides an "End Task" button -for force-killing hung apps. The Task Manager is a shell-level -component, not tied to any app -- it persists even if the desktop app -is terminated. +The Task Manager is a separate DXE (`taskmgr.lib` in `taskmgr/`), not +built into the shell. It registers itself at load time via a DXE +constructor that sets the `shellCtrlEscFn` function pointer. The shell +calls this pointer on Ctrl+Esc. If `taskmgr.lib` is not loaded, +`shellCtrlEscFn` is NULL and Ctrl+Esc does nothing. + +See `taskmgr/README.md` for full Task Manager documentation. ## Desktop Update Notifications @@ -141,13 +161,11 @@ Apps (especially the desktop app) register callbacks via | File | Description | |------|-------------| -| `shellMain.c` | Entry point, main loop, crash recovery, idle callback | -| `shellApp.h` | App lifecycle types: `AppDescriptorT`, `DxeAppContextT`, `ShellAppT`, `AppStateE` | -| `shellApp.c` | App loading, reaping, task creation, DXE management | +| `shellMain.c` | Entry point, main loop, crash recovery, splash screen, idle callback | +| `shellApp.h` | App lifecycle types: `AppDescriptorT`, `DxeAppContextT`, `ShellAppT`, `AppStateE`; `shellCtrlEscFn` extern | +| `shellApp.c` | App loading, reaping, task creation, DXE management, per-app memory tracking | | `shellInfo.h` | System information wrapper | | `shellInfo.c` | Gathers and caches hardware info via platform layer | -| `shellTaskMgr.h` | Task Manager API | -| `shellTaskMgr.c` | Task Manager window (list of running apps, End Task) | | `Makefile` | Builds `bin/libs/dvxshell.lib` + config/themes/wallpapers | diff --git a/taskmgr/README.md b/taskmgr/README.md new file mode 100644 index 0000000..3559b76 --- /dev/null +++ b/taskmgr/README.md @@ -0,0 +1,75 @@ +# DVX Task Manager (taskmgr.lib) + +A separate DXE3 module that provides the system Task Manager window. +Built as `taskmgr.lib` and loaded alongside the other libs at startup. +The Task Manager is a shell-level component, not tied to any app -- it +persists even if the desktop app (Program Manager) is terminated. + + +## Registration + +The module uses a GCC `__attribute__((constructor))` function to +register itself with the shell at load time. The constructor sets the +`shellCtrlEscFn` function pointer (declared in `shellApp.h`) to +`shellTaskMgrOpen`. The shell calls this pointer when Ctrl+Esc is +pressed. If `taskmgr.lib` is not loaded, the pointer remains NULL and +Ctrl+Esc does nothing. + + +## Features + +- **App list**: 6-column ListView showing Name, Title, File, Type, + Memory, and Status for all running apps +- **Memory column**: Per-app memory usage from `dvxMemGetAppUsage()`, + displayed in KB or MB +- **Switch To**: Raises and focuses the selected app's topmost window; + restores it if minimized. Also triggered by double-clicking a row. +- **End Task**: Force-kills the selected app via `shellForceKillApp()` +- **Run...**: Opens a file dialog to browse for and launch a `.app` file +- **Accelerator keys**: `&Switch To` (Alt+S), `&End Task` (Alt+E), + `&Run...` (Alt+R) +- **Status bar**: Shows running app count and system memory usage +- **Live refresh**: Registers a desktop update callback so the list + refreshes automatically when apps are loaded, reaped, or crash + + +## Window Behavior + +- Owned by the shell (appId = 0), not by any app +- Single-instance: if already open, Ctrl+Esc raises and focuses it +- Closing the window unregisters the desktop update callback and + cleans up dynamic arrays + + +## API + +```c +// Open or raise the Task Manager window. +void shellTaskMgrOpen(AppContextT *ctx); + +// Refresh the task list (called by desktop update notification). +void shellTaskMgrRefresh(void); +``` + + +## Dependencies + +Loaded after: `dvxshell`, `libtasks`, `libdvx`, `texthelp`, `listhelp` +(via `taskmgr.dep`). + + +## Files + +| File | Description | +|------|-------------| +| `shellTaskMgr.h` | Public API (open, refresh) | +| `shellTaskMgr.c` | Task Manager window, list, buttons, DXE constructor | +| `Makefile` | Builds `bin/libs/taskmgr.lib` + dep file | + + +## Build + +``` +make # builds bin/libs/taskmgr.lib + taskmgr.dep +make clean # removes objects, library, and dep file +``` diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..7cb24c4 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,117 @@ +# DVX Tools + +Host-native utilities that run on the development machine (Linux or +DOS). These are not DXE modules and do not cross-compile with DJGPP -- +they build with the system GCC. + + +## dvxres -- Resource Tool + +Command-line tool for managing resource blocks appended to DXE3 files +(`.app`, `.wgt`, `.lib`). Resources are invisible to `dlopen` because +they are appended after the DXE3 content. + +### Commands + +``` +dvxres add +dvxres build +dvxres list +dvxres get [outfile] +dvxres strip +``` + +| Command | Description | +|---------|-------------| +| `add` | Add or replace a single resource in a DXE file | +| `build` | Add all resources listed in a manifest file (replaces any existing resources) | +| `list` | List all resources in a DXE file | +| `get` | Extract a resource to a file or stdout | +| `strip` | Remove all appended resources, leaving only the DXE content | + +### Resource Types + +| Type | Keyword | Description | +|------|---------|-------------| +| `DVX_RES_ICON` | `icon` or `image` | Image data (BMP icons, etc.) | +| `DVX_RES_TEXT` | `text` | Null-terminated string (author, copyright) | +| `DVX_RES_BINARY` | `binary` | Arbitrary binary data (app-specific) | + +For `add` with type `text`, the data argument is the string value +directly. For `icon` or `binary`, the data argument is a file path. + +### Resource File Format + +Resources are appended after the normal DXE3 content: + +``` +[DXE3 content] -- untouched, loaded by dlopen +[resource data entries] -- sequential, variable length +[resource directory] -- fixed-size 48-byte entries +[footer] -- 16 bytes: magic + dir offset + count +``` + +The footer is at the very end of the file. Reading starts from +`EOF - 16` bytes. The magic value is `0x52585644` ("DVXR" in +little-endian). The directory offset points to the start of the +directory entries, and the entry count gives the number of resources. + +Each directory entry (48 bytes) contains: +- `name[32]` -- resource name (null-terminated) +- `type` (uint32) -- DVX_RES_ICON, DVX_RES_TEXT, or DVX_RES_BINARY +- `offset` (uint32) -- absolute file offset of data +- `size` (uint32) -- data size in bytes +- `reserved` (uint32) -- padding + +### Manifest File Format (.res) + +Plain text, one resource per line: + +``` +# Comment lines start with # +name type data + +# Examples: +icon32 icon icons/myapp32.bmp +icon16 icon icons/myapp16.bmp +author text "John Doe" +appdata binary data/config.bin +``` + +Each line has three fields: name, type, and data. Text data can be +quoted. Empty lines and lines starting with `#` are ignored. + + +## mkicon -- Icon Generator + +Generates simple 32x32 24-bit BMP pixel-art icons for DVX apps. + +``` +mkicon +``` + +Available icon types: `clock`, `notepad`, `cpanel`, `dvxdemo`, `imgview`. + +Note: mkicon is defined in `mkicon.c` but is not currently built by the +Makefile (it was used during initial development). + + +## Files + +| File | Description | +|------|-------------| +| `dvxres.c` | Resource tool implementation | +| `mkicon.c` | Icon generator (not built by default) | +| `Makefile` | Builds `bin/dvxres` (host native) | + + +## Build + +``` +make # builds bin/dvxres +make clean # removes bin/dvxres +``` + +Uses the system GCC, not the DJGPP cross-compiler. Links against +`core/dvxResource.c` for the runtime resource API (`dvxResOpen`, +`dvxResRead`, `dvxResClose`). diff --git a/widgets/README.md b/widgets/README.md index 44a5adb..6b8c621 100644 --- a/widgets/README.md +++ b/widgets/README.md @@ -7,11 +7,50 @@ which registers its widget class(es) and API struct with the core. Core knows nothing about individual widgets. All per-widget state lives in `w->data` (allocated by the widget DXE). All per-widget behavior is -dispatched through the WidgetClassT vtable. Each widget provides a +dispatched through the `WidgetClassT` vtable. Each widget provides a public header with its API struct, typed accessor, and convenience macros. +## ABI-Stable Handler Dispatch + +`WidgetClassT` contains a `handlers[WGT_METHOD_MAX]` array of function +pointers. Core dispatches via integer index: +`w->wclass->handlers[WGT_METHOD_PAINT]`, etc. Currently 21 methods are +defined (`WGT_METHOD_COUNT`), with room for 32 (`WGT_METHOD_MAX`). +Adding new methods does not break existing widget DXE binaries -- old +widgets simply have NULL in the new slots. + +Key method constants: + +| Constant | Index | Signature | +|----------|-------|-----------| +| `WGT_METHOD_PAINT` | 0 | `(w, d, ops, font, colors)` | +| `WGT_METHOD_PAINT_OVERLAY` | 1 | `(w, d, ops, font, colors)` | +| `WGT_METHOD_CALC_MIN_SIZE` | 2 | `(w, font)` | +| `WGT_METHOD_LAYOUT` | 3 | `(w, font)` | +| `WGT_METHOD_ON_MOUSE` | 5 | `(w, root, vx, vy)` | +| `WGT_METHOD_ON_KEY` | 6 | `(w, key, mod)` | +| `WGT_METHOD_DESTROY` | 8 | `(w)` | +| `WGT_METHOD_ON_DRAG_UPDATE` | 15 | `(w, root, x, y)` | +| `WGT_METHOD_ON_DRAG_END` | 16 | `(w, root, x, y)` | +| `WGT_METHOD_QUICK_REPAINT` | 19 | `(w, outY, outH)` | + +## Generic Drag Support + +Widgets that need drag behavior implement `WGT_METHOD_ON_DRAG_UPDATE` +and `WGT_METHOD_ON_DRAG_END`. Core tracks the drag widget and calls +these methods on mouse move/release during a drag. This replaces +per-widget drag hacks. Used by Slider, Splitter, ListBox (reorder), +ListView (reorder, column resize), and TreeView (reorder). + +## Dynamic Limits + +All widget child arrays, app slot tables, and callback lists use stb_ds +dynamic arrays. There are no fixed maximums for child count, app count, +or registered callbacks. + + ## Widget Summary 26 source files produce 26 `.wgt` modules containing 32 widget types: