From 786353fa08625e120e8a1bd55634a5a6262a9e4f Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 17 Mar 2026 23:51:01 -0500 Subject: [PATCH] Much new documentation added as comments to code. --- apps/clock/clock.c | 52 ++- apps/dvxdemo/dvxdemo.c | 73 +++- apps/notepad/notepad.c | 35 +- apps/progman/progman.c | 98 ++++- dvx/dvxApp.c | 469 ++++++++++++++++++++---- dvx/dvxApp.h | 107 ++++-- dvx/dvxComp.c | 98 ++++- dvx/dvxComp.h | 42 ++- dvx/dvxCursor.h | 18 +- dvx/dvxDialog.c | 194 ++++++++-- dvx/dvxDialog.h | 35 +- dvx/dvxDraw.c | 325 ++++++++++++++++- dvx/dvxDraw.h | 78 ++-- dvx/dvxFont.h | 31 ++ dvx/dvxIcon.c | 18 + dvx/dvxImageWrite.c | 10 + dvx/dvxPalette.h | 28 +- dvx/dvxTypes.h | 210 ++++++++++- dvx/dvxVideo.c | 82 +++++ dvx/dvxVideo.h | 39 +- dvx/dvxWidget.h | 204 +++++++++-- dvx/dvxWm.c | 620 ++++++++++++++++++++++++++++---- dvx/dvxWm.h | 140 ++++++-- dvx/platform/dvxPlatform.h | 108 ++++-- dvx/platform/dvxPlatformDos.c | 316 ++++++++++++++-- dvx/widgets/widgetAnsiTerm.c | 237 +++++++++++- dvx/widgets/widgetBox.c | 32 ++ dvx/widgets/widgetButton.c | 28 ++ dvx/widgets/widgetCanvas.c | 82 ++++- dvx/widgets/widgetCheckbox.c | 21 +- dvx/widgets/widgetClass.c | 74 ++++ dvx/widgets/widgetComboBox.c | 49 ++- dvx/widgets/widgetCore.c | 175 +++++++-- dvx/widgets/widgetDropdown.c | 38 +- dvx/widgets/widgetEvent.c | 131 ++++++- dvx/widgets/widgetImage.c | 28 ++ dvx/widgets/widgetImageButton.c | 16 + dvx/widgets/widgetInternal.h | 185 ++++++++-- dvx/widgets/widgetLabel.c | 15 + dvx/widgets/widgetLayout.c | 117 +++++- dvx/widgets/widgetListBox.c | 59 ++- dvx/widgets/widgetListView.c | 160 ++++++++- dvx/widgets/widgetOps.c | 99 ++++- dvx/widgets/widgetProgressBar.c | 29 +- dvx/widgets/widgetRadio.c | 46 +++ dvx/widgets/widgetScrollPane.c | 73 ++++ dvx/widgets/widgetScrollbar.c | 26 ++ dvx/widgets/widgetSeparator.c | 17 + dvx/widgets/widgetSlider.c | 41 +++ dvx/widgets/widgetSpacer.c | 12 + dvx/widgets/widgetSpinner.c | 64 ++++ dvx/widgets/widgetSplitter.c | 53 +++ dvx/widgets/widgetStatusBar.c | 21 ++ dvx/widgets/widgetTabControl.c | 58 +++ dvx/widgets/widgetTextInput.c | 183 ++++++++++ dvx/widgets/widgetToolbar.c | 18 + dvx/widgets/widgetTreeView.c | 129 +++++++ dvxshell/shellApp.c | 55 ++- dvxshell/shellApp.h | 65 +++- dvxshell/shellExport.c | 58 ++- dvxshell/shellMain.c | 92 ++++- packet/packet.c | 91 ++++- packet/packet.h | 32 +- proxy/proxy.c | 58 ++- proxy/sockShim.c | 19 + proxy/sockShim.h | 13 +- rs232/rs232.c | 77 +++- rs232/rs232.h | 28 +- seclink/secLink.c | 32 +- seclink/secLink.h | 20 +- security/security.c | 108 +++++- security/security.h | 29 +- tasks/demo.c | 13 +- tasks/taskswitch.c | 155 +++++++- tasks/taskswitch.h | 54 ++- termdemo/termdemo.c | 44 ++- 76 files changed, 6217 insertions(+), 572 deletions(-) diff --git a/apps/clock/clock.c b/apps/clock/clock.c index b8c5880..c4a3b0c 100644 --- a/apps/clock/clock.c +++ b/apps/clock/clock.c @@ -1,7 +1,22 @@ // clock.c — Clock DXE application (main-loop with tsYield) // -// Demonstrates a main-loop app: runs its own loop calling tsYield(), -// updates a clock display every second, and invalidates its window. +// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback- +// only apps like notepad or progman. The difference is fundamental: +// +// Callback-only: appMain creates UI, returns. Shell calls callbacks. +// Main-loop: appMain runs forever in its own task, calling tsYield() +// to cooperatively yield to the shell and other tasks. +// +// A main-loop app is needed when the app has ongoing work that can't be +// expressed purely as event callbacks — here, polling the system clock +// every second and repainting. The shell allocates a dedicated task stack +// and schedules this task alongside the main event loop. +// +// The onPaint/onClose callbacks still execute in task 0 during dvxUpdate(), +// not in this task. The main loop only handles the clock-tick polling; the +// actual rendering and window management happen through the normal callback +// path. tsYield() is what makes this cooperative — without it, this task +// would starve the shell and all other apps. #include "dvxApp.h" #include "dvxWidget.h" @@ -45,6 +60,9 @@ static void updateTime(void); // App descriptor // ============================================================ +// hasMainLoop = true tells the shell to create a dedicated task for appMain. +// TS_PRIORITY_LOW because clock updates are cosmetic and should never +// preempt interactive apps or the shell's event processing. AppDescriptorT appDescriptor = { .name = "Clock", .hasMainLoop = true, @@ -56,12 +74,23 @@ AppDescriptorT appDescriptor = { // Callbacks (fire in task 0 during dvxUpdate) // ============================================================ +// Setting quit = true tells the main loop (running in a separate task) to +// exit. The main loop then destroys the window and returns from appMain, +// which causes the shell to reap this task and unload the DXE. static void onClose(WindowT *win) { (void)win; sState.quit = true; } +// onPaint demonstrates the raw paint callback approach (no widget tree). +// Instead of using wgtInitWindow + widgets, this app renders directly into +// the window's content buffer. This is the lower-level alternative to the +// widget system — useful for custom rendering like this centered clock display. +// +// We create a temporary DisplayT struct pointing at the window's content buffer +// so the drawing primitives (rectFill, drawText) can operate on the window +// content directly rather than the backbuffer. static void onPaint(WindowT *win, RectT *dirty) { (void)dirty; @@ -70,7 +99,9 @@ static void onPaint(WindowT *win, RectT *dirty) { const BitmapFontT *font = dvxGetFont(ac); const ColorSchemeT *colors = dvxGetColors(ac); - // Local display copy pointing at content buffer + // Shallow copy of the display, redirected to the window's content buffer. + // This lets us reuse the standard drawing API without modifying the + // global display state. DisplayT cd = *dvxGetDisplay(ac); cd.backBuf = win->contentBuf; cd.width = win->contentW; @@ -121,6 +152,10 @@ static void updateTime(void) { // Shutdown hook (optional DXE export) // ============================================================ +// The shell calls appShutdown (if exported) when force-killing an app via +// Task Manager or during shell shutdown. For main-loop apps, this is the +// signal to break out of the main loop. Without this, shellForceKillApp +// would have to terminate the task forcibly, potentially leaking resources. void appShutdown(void) { sState.quit = true; } @@ -129,6 +164,8 @@ void appShutdown(void) { // Entry point (runs in its own task) // ============================================================ +// This runs in its own task. The shell creates the task before calling +// appMain, and reaps it when appMain returns. int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; AppContextT *ac = ctx->shellCtx; @@ -136,11 +173,13 @@ int32_t appMain(DxeAppContextT *ctx) { memset(&sState, 0, sizeof(sState)); updateTime(); + // Position in the upper-right corner, out of the way of other windows int32_t winW = 200; int32_t winH = 100; int32_t winX = ac->display.width - winW - 40; int32_t winY = 40; + // resizable=false: clock has a fixed size, no resize handle sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false); if (!sWin) { @@ -155,7 +194,12 @@ int32_t appMain(DxeAppContextT *ctx) { onPaint(sWin, &full); dvxInvalidateWindow(ac, sWin); - // Main loop: update time, invalidate, yield + // Main loop: check if the second has changed, repaint if so, then yield. + // tsYield() transfers control back to the shell's task scheduler. + // On a 486, time() resolution is 1 second, so we yield many times per + // second between actual updates — this keeps CPU usage near zero. + // dvxInvalidateWindow marks the window dirty so the compositor will + // flush it to the LFB on the next composite pass. while (!sState.quit) { time_t now = time(NULL); diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index 38e380e..9bd293b 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -1,7 +1,18 @@ // dvxdemo.c — DVX GUI demonstration app (DXE version) // -// Callback-only app that opens several windows showcasing the DVX -// widget system, paint callbacks, menus, accelerators, etc. +// Callback-only DXE app (hasMainLoop = false) that opens several windows +// showcasing the DVX widget system, paint callbacks, menus, accelerators, etc. +// +// This serves as a comprehensive widget catalog and integration test: +// - setupMainWindow: raw onPaint callbacks (text, gradient, pattern) +// - setupWidgetDemo: form widgets (input, checkbox, radio, listbox) +// - setupControlsWindow: advanced widgets in a TabControl (dropdown, +// progress, slider, spinner, tree, listview, +// scrollpane, toolbar, canvas, splitter, image) +// - setupTerminalWindow: ANSI terminal emulator widget +// +// Each window is independent; closing one doesn't affect the others. +// The app has no persistent state — it's purely a showcase. #include "dvxApp.h" #include "dvxDialog.h" @@ -79,6 +90,9 @@ static DxeAppContextT *sDxeCtx = NULL; // App descriptor // ============================================================ +// Callback-only: all windows use either widget trees (which handle their own +// input) or onPaint callbacks (which are invoked by the compositor). No need +// for a dedicated task since there's no ongoing computation. AppDescriptorT appDescriptor = { .name = "DVX Demo", .hasMainLoop = false, @@ -218,6 +232,9 @@ static void onMenuCb(WindowT *win, int32_t menuId) { } +// Walk up the widget tree to find the root, then use wgtFind() to locate +// the named status label. This pattern avoids static coupling between the +// button and the status label — they're connected only by the name string. static void onOkClick(WidgetT *w) { WidgetT *root = w; @@ -233,6 +250,11 @@ static void onOkClick(WidgetT *w) { } +// Renders a vertical gradient directly into the window's content buffer. +// This demonstrates the raw onPaint approach: the app has full pixel control +// and uses the BlitOps span primitives for performance. The gradient is +// computed per-scanline using the span fill operation (rep stosl on x86), +// which is much faster than per-pixel writes. static void onPaintColor(WindowT *win, RectT *dirtyArea) { (void)dirtyArea; @@ -256,6 +278,9 @@ static void onPaintColor(WindowT *win, RectT *dirtyArea) { } +// Renders a checkerboard pattern pixel-by-pixel. This is the slowest +// approach (per-pixel BPP branching) and exists to test that the content +// buffer renders correctly at all supported bit depths (8/16/32 bpp). static void onPaintPattern(WindowT *win, RectT *dirtyArea) { (void)dirtyArea; @@ -287,6 +312,10 @@ static void onPaintPattern(WindowT *win, RectT *dirtyArea) { } +// Renders text manually using the bitmap font glyph data, bypassing the +// normal drawText helper. This demonstrates direct glyph rendering and +// also serves as a regression test for the font subsystem — if the glyph +// data format ever changes, this paint callback would visibly break. static void onPaintText(WindowT *win, RectT *dirtyArea) { (void)dirtyArea; @@ -398,9 +427,14 @@ static void onToolbarClick(WidgetT *w) { // setupControlsWindow — advanced widgets with tabs // ============================================================ +// Item arrays are static because dropdown/combobox widgets store pointers, +// not copies. They must outlive the widgets that reference them. static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"}; static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"}; +// Demonstrates every advanced widget type, organized into tabs. The TabControl +// widget manages tab pages; each page is a container that shows/hides based +// on the selected tab. This is the densest widget test in the project. static void setupControlsWindow(void) { WindowT *win = dvxCreateWindow(sAc, "Advanced Widgets", 380, 50, 360, 440, true); @@ -597,9 +631,11 @@ static void setupControlsWindow(void) { wgtCanvasSetMouseCallback(cv, onCanvasDraw); // --- Tab 8: Splitter --- + // Demonstrates nested splitters: a horizontal split (top/bottom) where + // the top pane contains a vertical split (left/right). This creates a + // three-pane file-explorer layout similar to Windows Explorer. WidgetT *page8s = wgtTabPage(tabs, "S&plit"); - // Outer horizontal splitter: explorer on top, detail on bottom WidgetT *hSplit = wgtSplitter(page8s, false); hSplit->weight = 100; wgtSplitterSetPos(hSplit, 120); @@ -637,9 +673,9 @@ static void setupControlsWindow(void) { wgtLabel(detailFrame, "Select a file above to preview."); // --- Tab 9: Disabled --- + // Side-by-side comparison of every widget in enabled vs disabled state. + // Tests that wgtSetEnabled(w, false) correctly greys out each widget type. WidgetT *page9d = wgtTabPage(tabs, "&Disabled"); - - // Left column: enabled widgets Right column: disabled widgets WidgetT *disRow = wgtHBox(page9d); disRow->weight = 100; @@ -736,8 +772,11 @@ static void setupControlsWindow(void) { // setupMainWindow — info window + paint demos // ============================================================ +// Creates three windows that demonstrate raw onPaint rendering (no widgets): +// 1. Text info window with full menu bar, accelerators, and context menu +// 2. Color gradient window +// 3. Checkerboard pattern window with scrollbars static void setupMainWindow(void) { - // Window 1: Text information window with menu bar WindowT *win1 = dvxCreateWindow(sAc, "DVX Information", 50, 40, 340, 350, true); if (win1) { @@ -809,7 +848,9 @@ static void setupMainWindow(void) { } } - // Accelerator table (global hotkeys) + // Accelerator table: keyboard shortcuts that work even without the + // menu being open. The WM dispatches these via the window's onMenu + // callback, making them indistinguishable from menu clicks. AccelTableT *accel = dvxCreateAccelTable(); dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW); dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN); @@ -818,7 +859,8 @@ static void setupMainWindow(void) { dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT); win1->accelTable = accel; - // Window-level context menu + // Right-click context menu, attached at the window level. If a widget + // has its own contextMenu it takes priority; otherwise this one fires. MenuT *winCtx = wmCreateMenu(); wmAddMenuItem(winCtx, "Cu&t", CMD_CTX_CUT); wmAddMenuItem(winCtx, "&Copy", CMD_CTX_COPY); @@ -827,6 +869,10 @@ static void setupMainWindow(void) { wmAddMenuItem(winCtx, "&Properties...", CMD_CTX_PROPS); win1->contextMenu = winCtx; + // For raw-paint windows (no widget tree), we must manually update + // the content rect and allocate the content buffer after adding + // menu bars or scrollbars, since those shrink the content area. + // Then we do an initial paint to fill the buffer before it's composited. wmUpdateContentRect(win1); wmReallocContentBuf(win1, &sAc->display); @@ -867,6 +913,10 @@ static void setupMainWindow(void) { // setupTerminalWindow — ANSI terminal widget demo // ============================================================ +// Creates an ANSI terminal widget with sample output demonstrating all +// supported attributes (bold, reverse, blink), 8+8 colors, background +// colors, and CP437 box-drawing/graphic characters. The terminal is +// minimized on creation to avoid cluttering the initial demo view. static void setupTerminalWindow(void) { WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", 60, 60, 660, 420, true); @@ -946,6 +996,10 @@ static void setupTerminalWindow(void) { // setupWidgetDemo — form with accelerators // ============================================================ +// Demonstrates the standard form pattern: labeled inputs in frames, checkbox +// and radio groups, list boxes (single and multi-select with context menus), +// and a button row. The '&' in label text marks the Alt+key mnemonic that +// moves focus to the associated widget — this is the accelerator mechanism. static void setupWidgetDemo(void) { WindowT *win = dvxCreateWindow(sAc, "Widget Demo", 80, 200, 280, 360, true); @@ -1037,6 +1091,9 @@ static void setupWidgetDemo(void) { // Entry point // ============================================================ +// Create all demo windows at once. Because this is a callback-only app, +// appMain returns immediately and the shell's event loop takes over. +// Each window is self-contained with its own callbacks. int32_t appMain(DxeAppContextT *ctx) { sDxeCtx = ctx; sAc = ctx->shellCtx; diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index a4570a7..30b0533 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -1,6 +1,14 @@ // notepad.c — Simple text editor DXE application (callback-only) // -// Demonstrates a callback-only app with menus, text editing, and file I/O. +// A callback-only DXE app (hasMainLoop = false) that provides basic text +// editing with file I/O. Demonstrates the standard DXE app pattern: +// 1. appMain creates window + widget tree, registers callbacks, returns 0 +// 2. All editing happens through the TextArea widget's built-in behavior +// 3. File operations and dirty tracking are handled by menu callbacks +// +// The TextArea widget handles keyboard input, cursor movement, selection, +// scrolling, word wrap, and undo internally. Notepad only needs to wire +// up menus and file I/O around it. #include "dvxApp.h" #include "dvxDialog.h" @@ -18,6 +26,9 @@ // Constants // ============================================================ +// 32KB text buffer limit. Keeps memory usage bounded on DOS; larger files +// are silently truncated on load. The TextArea widget allocates this buffer +// internally when constructed. #define TEXT_BUF_SIZE 32768 #define CMD_NEW 100 @@ -38,6 +49,7 @@ static DxeAppContextT *sCtx = NULL; static WindowT *sWin = NULL; static WidgetT *sTextArea = NULL; static char sFilePath[260] = ""; +// Hash of text content at last save/open, used for cheap dirty detection static uint32_t sCleanHash = 0; // ============================================================ @@ -60,6 +72,8 @@ static void onMenu(WindowT *win, int32_t menuId); // App descriptor // ============================================================ +// Callback-only: the TextArea widget handles all interactive editing +// entirely within the shell's event dispatch, so no dedicated task needed. AppDescriptorT appDescriptor = { .name = "Notepad", .hasMainLoop = false, @@ -71,6 +85,10 @@ AppDescriptorT appDescriptor = { // Dirty tracking // ============================================================ +// djb2-xor hash for dirty detection. Not cryptographic — just a fast way +// to detect changes without storing a full copy of the last-saved text. +// False negatives are theoretically possible but vanishingly unlikely for +// text edits. This avoids the memory cost of keeping a shadow buffer. static uint32_t hashText(const char *text) { if (!text) { return 0; @@ -151,6 +169,8 @@ static void doOpen(void) { return; } + // Open in binary mode to avoid DJGPP's CR/LF translation; the TextArea + // widget handles line endings internally. FILE *f = fopen(path, "rb"); if (!f) { @@ -158,6 +178,9 @@ static void doOpen(void) { return; } + // Read entire file into a temporary buffer. Files larger than the + // TextArea's buffer are silently truncated — this matches the behavior + // of Windows Notepad on large files. fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); @@ -234,6 +257,9 @@ static void doSaveAs(void) { // Callbacks // ============================================================ +// onClose is the only cleanup path for a callback-only app. We null our +// pointers after destroying the window; the shell will reap the DXE on the +// next frame when it detects no windows remain for this appId. static void onClose(WindowT *win) { if (!askSaveChanges()) { return; @@ -287,6 +313,8 @@ static void onMenu(WindowT *win, int32_t menuId) { // Entry point // ============================================================ +// Entry point. Offset +20 from center so multiple notepad instances cascade +// naturally rather than stacking exactly on top of each other. int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; AppContextT *ac = ctx->shellCtx; @@ -326,7 +354,10 @@ int32_t appMain(DxeAppContextT *ctx) { wmAddMenuSeparator(editMenu); wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELALL); - // Widget tree + // The widget tree is minimal: just a TextArea filling the entire content + // area (weight=100). The TextArea widget provides editing, scrolling, + // selection, copy/paste, and undo — all driven by keyboard events that + // the shell dispatches to the focused widget. WidgetT *root = wgtInitWindow(ac, sWin); sTextArea = wgtTextArea(root, TEXT_BUF_SIZE); diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 925ef36..1d0d612 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -2,8 +2,21 @@ // // Displays a grid of available apps from the apps/ directory. // Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc). -// This is a callback-only DXE app: creates windows, registers callbacks, -// and returns. +// +// DXE App Contract: +// This is a callback-only DXE app (hasMainLoop = false). It exports two +// symbols: appDescriptor (metadata) and appMain (entry point). The shell +// calls appMain once; we create windows, register callbacks, and return 0. +// From that point on, the shell's event loop drives everything through +// our window callbacks (onClose, onMenu, widget onClick, etc.). +// +// Because we have no main loop, we don't need a dedicated task stack. +// The shell runs our callbacks in task 0 during dvxUpdate(). +// +// Progman is special: it's the desktop app. It calls +// shellRegisterDesktopUpdate() so the shell notifies us whenever an app +// is loaded, reaped, or crashes, keeping our status bar and Task Manager +// list current without polling. #include "dvxApp.h" #include "dvxDialog.h" @@ -23,8 +36,11 @@ // Constants // ============================================================ +// 64 entries is generous; limited by screen real estate before this cap #define MAX_APP_FILES 64 +// DOS 8.3 paths are short, but long names under DJGPP can reach ~260 #define MAX_PATH_LEN 260 +// Grid layout for app buttons: 4 columns, rows created dynamically #define PM_GRID_COLS 4 #define PM_BTN_W 100 #define PM_BTN_H 24 @@ -44,11 +60,15 @@ // Module state // ============================================================ +// Each discovered .app file in the apps/ directory tree typedef struct { char name[SHELL_APP_NAME_MAX]; // display name (filename without .app) char path[MAX_PATH_LEN]; // full path } AppEntryT; +// Module-level statics (s prefix). DXE apps use file-scoped statics because +// each DXE is a separate shared object with its own data segment. No risk +// of collision between apps even though the names look global. static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; static int32_t sMyAppId = 0; @@ -86,6 +106,10 @@ static void refreshTaskList(void); // App descriptor // ============================================================ +// The shell reads this exported symbol to determine how to manage the app. +// hasMainLoop = false means the shell won't create a dedicated task; our +// appMain runs to completion and all subsequent work happens via callbacks. +// stackSize = 0 means "use the shell default" (irrelevant for callback apps). AppDescriptorT appDescriptor = { .name = "Program Manager", .hasMainLoop = false, @@ -97,6 +121,9 @@ AppDescriptorT appDescriptor = { // Static functions (alphabetical) // ============================================================ +// Build the main Program Manager window with app buttons, menus, and status bar. +// Window is centered horizontally and placed in the upper quarter vertically +// so spawned app windows don't hide behind it. static void buildPmWindow(void) { int32_t screenW = sAc->display.width; int32_t screenH = sAc->display.height; @@ -135,10 +162,12 @@ static void buildPmWindow(void) { wmAddMenuSeparator(helpMenu); wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR); - // Widget tree + // wgtInitWindow creates the root VBox widget that the window's content + // area maps to. All child widgets are added to this root. WidgetT *root = wgtInitWindow(sAc, sPmWindow); - // App button grid in a frame + // App button grid in a labeled frame. weight=100 tells the layout engine + // this frame should consume all available vertical space (flex weight). WidgetT *appFrame = wgtFrame(root, "Applications"); appFrame->weight = 100; @@ -146,7 +175,9 @@ static void buildPmWindow(void) { WidgetT *lbl = wgtLabel(appFrame, "(No applications found in apps/ directory)"); (void)lbl; } else { - // Build rows of buttons + // Build rows of buttons. Each row is an HBox holding PM_GRID_COLS + // buttons. userData points back to the AppEntryT so the click + // callback knows which app to launch. int32_t row = 0; WidgetT *hbox = NULL; @@ -168,19 +199,26 @@ static void buildPmWindow(void) { (void)row; } - // Status bar + // Status bar at bottom; weight=100 on the label makes it fill the bar + // width so text can be left-aligned naturally. WidgetT *statusBar = wgtStatusBar(root); sStatusLabel = wgtLabel(statusBar, ""); sStatusLabel->weight = 100; updateStatusText(); + // dvxFitWindow sizes the window to tightly fit the widget tree, + // honoring preferred sizes. Without this, the window would use the + // initial dimensions from dvxCreateWindow even if widgets don't fit. dvxFitWindow(sAc, sPmWindow); } +// Build or raise the Task Manager window. Singleton pattern: if sTmWindow is +// already live, we just raise it to the top of the Z-order instead of +// creating a duplicate, mimicking Windows 3.x Task Manager behavior. static void buildTaskManager(void) { if (sTmWindow) { - // Already open — just raise it + // Already open — find it in the window stack and bring to front for (int32_t i = 0; i < sAc->stack.count; i++) { if (sAc->stack.windows[i] == sTmWindow) { wmRaiseWindow(&sAc->stack, &sAc->dirty, i); @@ -208,7 +246,8 @@ static void buildTaskManager(void) { WidgetT *root = wgtInitWindow(sAc, sTmWindow); - // List view of running apps + // ListView widget shows running apps with Name/Type/Status columns. + // wgtPercent() returns a size encoding recognized by the layout engine. ListViewColT tmCols[3]; tmCols[0].title = "Name"; tmCols[0].width = wgtPercent(50); @@ -225,7 +264,7 @@ static void buildTaskManager(void) { sTmListView->prefH = wgtPixels(160); wgtListViewSetColumns(sTmListView, tmCols, 3); - // Button row + // Button row right-aligned (AlignEndE) to follow Windows UI convention WidgetT *btnRow = wgtHBox(root); btnRow->align = AlignEndE; btnRow->spacing = wgtPixels(8); @@ -243,6 +282,8 @@ static void buildTaskManager(void) { } +// Shell calls this via shellRegisterDesktopUpdate whenever an app is loaded, +// reaped, or crashes. We refresh the running count and Task Manager list. static void desktopUpdate(void) { updateStatusText(); @@ -252,6 +293,8 @@ static void desktopUpdate(void) { } +// Widget click handler for app grid buttons. userData was set to the +// AppEntryT pointer during window construction, giving us the .app path. static void onAppButtonClick(WidgetT *w) { AppEntryT *entry = (AppEntryT *)w->userData; @@ -268,9 +311,11 @@ static void onAppButtonClick(WidgetT *w) { } +// Closing the Program Manager is equivalent to shutting down the entire shell. +// dvxQuit() signals the main event loop to exit, which triggers +// shellTerminateAllApps() to gracefully tear down all loaded DXEs. static void onPmClose(WindowT *win) { (void)win; - // Confirm exit int32_t result = dvxMessageBox(sAc, "Exit Shell", "Are you sure you want to exit DVX Shell?", MB_YESNO | MB_ICONQUESTION); if (result == ID_YES) { @@ -337,6 +382,8 @@ static void onPmMenu(WindowT *win, int32_t menuId) { } +// Null the static pointers before destroying so buildTaskManager() knows +// the window is gone and will create a fresh one next time. static void onTmClose(WindowT *win) { sTmListView = NULL; sTmWindow = NULL; @@ -357,7 +404,10 @@ static void onTmEndTask(WidgetT *w) { return; } - // Map list index to app ID (skip our own appId) + // The list view rows don't carry app IDs directly, so we re-walk the + // app slot table in the same order as refreshTaskList() to map the + // selected row index back to the correct ShellAppT. We skip our own + // appId so progman can't kill itself. int32_t idx = 0; for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { @@ -394,7 +444,9 @@ static void onTmSwitchTo(WidgetT *w) { return; } - // Map list index to app ID (skip our own appId), find topmost window + // Same index-to-appId mapping as onTmEndTask. We scan the window + // stack top-down (highest Z first) to find the app's topmost window, + // restore it if minimized, then raise and focus it. int32_t idx = 0; for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { @@ -430,12 +482,15 @@ static void onTmSwitchTo(WidgetT *w) { } +// Rebuild the Task Manager list view from the shell's app slot table. +// Uses static arrays because the list view data pointers must remain valid +// until the next call to wgtListViewSetData (the widget doesn't copy strings). static void refreshTaskList(void) { if (!sTmListView) { return; } - // 3 columns per row: Name, Type, Status + // Flat array of cell strings: [row0_col0, row0_col1, row0_col2, row1_col0, ...] static const char *cells[SHELL_MAX_APPS * 3]; static char typeStrs[SHELL_MAX_APPS][12]; int32_t rowCount = 0; @@ -461,6 +516,9 @@ static void refreshTaskList(void) { } +// Top-level scan entry point. Recursively walks apps/ looking for .app files. +// The apps/ path is relative to the working directory, which the shell sets +// to the root of the DVX install before loading any apps. static void scanAppsDir(void) { sAppCount = 0; scanAppsDirRecurse("apps"); @@ -468,6 +526,10 @@ static void scanAppsDir(void) { } +// Recursive directory walker. Subdirectories under apps/ allow organizing +// apps (e.g., apps/games/, apps/tools/). Each .app file is a DXE3 shared +// object that the shell can dlopen(). We skip progman.app to avoid listing +// ourselves in the launcher grid. static void scanAppsDirRecurse(const char *dirPath) { DIR *dir = opendir(dirPath); @@ -555,7 +617,8 @@ static void updateStatusText(void) { static char buf[64]; - // Subtract 1 to exclude ourselves from the count + // shellRunningAppCount() includes us. Subtract 1 so the user sees + // only the apps they launched, not the Program Manager itself. int32_t count = shellRunningAppCount() - 1; if (count < 0) { @@ -577,6 +640,10 @@ static void updateStatusText(void) { // Entry point // ============================================================ +// The shell calls appMain exactly once after dlopen() resolves our symbols. +// We scan for apps, build the UI, register our desktop update callback, then +// return 0 (success). From here on the shell drives us through callbacks. +// Returning non-zero would signal a load failure and the shell would unload us. int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; @@ -585,7 +652,8 @@ int32_t appMain(DxeAppContextT *ctx) { scanAppsDir(); buildPmWindow(); - // Register for state change notifications from the shell + // Register for state change notifications from the shell so our status + // bar and Task Manager stay current without polling shellRegisterDesktopUpdate(desktopUpdate); return 0; diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 73b2641..3e96822 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1,4 +1,32 @@ // dvx_app.c — Layer 5: Application API for DVX GUI +// +// Top-level layer of the DVX windowing system. This is the only layer +// that application code interacts with directly. It owns the main event +// loop, input polling, popup/context menu system, tooltip management, +// accelerator dispatch, clipboard, and window tiling/cascading. +// +// Architecture: poll-based event dispatch +// ---------------------------------------- +// The design uses polling (dvxUpdate) rather than an event queue because: +// 1. The target platform (DOS/DPMI) has no OS-provided event queue. +// BIOS keyboard and mouse services are inherently polled. +// 2. Polling avoids the need for dynamic memory allocation (no malloc +// per event, no queue growth) which matters on a 486 with limited RAM. +// 3. It maps directly to the DOS main-loop model: poll hardware, process, +// composite, repeat. No need for event serialization or priority. +// 4. Modal dialogs (message boxes, file dialogs) simply run a nested +// dvxUpdate loop, which is trivial with polling but would require +// re-entrant queue draining with an event queue. +// +// The tradeoff is that all input sources are polled every frame, but on a +// 486 this is actually faster than maintaining queue data structures. +// +// Compositing model: only dirty rectangles are redrawn and flushed to the +// LFB (linear framebuffer). The compositor walks bottom-to-top for each +// dirty rect, so overdraw happens in the backbuffer (system RAM), not +// video memory. The final flush is the only VRAM write per dirty rect, +// which is critical because VRAM writes through the PCI/ISA bus are an +// order of magnitude slower than system RAM writes on period hardware. #include "dvxApp.h" #include "dvxWidget.h" @@ -14,9 +42,19 @@ #include "thirdparty/stb_image_write.h" +// Double-click timing uses CLOCKS_PER_SEC so it's portable between DJGPP +// (where CLOCKS_PER_SEC is typically 91, from the PIT) and Linux/SDL. #define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2) + +// Minimized icon thumbnails are refreshed in a round-robin fashion rather +// than all at once, spreading the repaint cost across multiple frames. +// Every 8 frames, one dirty icon gets refreshed. #define ICON_REFRESH_INTERVAL 8 + +// Keyboard move/resize uses a fixed pixel step per arrow key press. +// 8 pixels keeps it responsive without being too coarse on a 640x480 screen. #define KB_MOVE_STEP 8 + #define MENU_CHECK_WIDTH 14 #define SUBMENU_ARROW_WIDTH 12 #define SUBMENU_ARROW_HALF 3 // half-size of submenu arrow glyph @@ -60,14 +98,24 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t static void updateCursorShape(AppContextT *ctx); static void updateTooltip(AppContextT *ctx); -// Button pressed via keyboard — shared with widgetEvent.c for Space/Enter +// 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. +// The button stays visually pressed for one frame (see dvxUpdate), then the +// click callback fires. This gives the user visual feedback that the +// keyboard activation was registered, matching Win3.x/Motif behavior. WidgetT *sKeyPressedBtn = NULL; // ============================================================ -// calcPopupSize — compute popup width and height for a menu +// bufferToRgb — convert native pixel format to 24-bit RGB // ============================================================ +// +// Screenshots must produce standard RGB data for stb_image_write, but the +// backbuffer uses whatever native pixel format VESA gave us (8/16/32bpp). +// This function handles the conversion using the display's format metadata +// (shift counts, bit widths) rather than assuming a specific layout. +// The 8bpp path uses the VGA palette for lookup. static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch) { uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * 3); @@ -115,6 +163,18 @@ static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, in } +// ============================================================ +// calcPopupSize — compute popup width and height for a menu +// ============================================================ +// +// Popup width is determined by the widest item, plus conditional margins +// for check/radio indicators and submenu arrows. Menu labels use a tab +// character to separate the item text from the keyboard shortcut string +// (e.g., "&Save\tCtrl+S"), which are measured and laid out independently +// so shortcuts right-align within the popup. The '&' prefix in labels +// marks the accelerator underline character and is excluded from width +// measurement by textWidthAccel. + static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) { int32_t maxW = 0; bool hasSub = false; @@ -155,6 +215,9 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw } } + // Width includes: padding, text, check margin (if any items are check/radio), + // submenu arrow space (if any items have submenus). All items in the popup + // share the same width for visual consistency, even if only some have checks. *pw = maxW + CHROME_TITLE_PAD * 2 + POPUP_ITEM_PAD_H + (hasSub ? SUBMENU_ARROW_WIDTH : 0) + (hasCheck ? MENU_CHECK_WIDTH : 0); *ph = menu->itemCount * ctx->font.charHeight + POPUP_BEVEL_WIDTH * 2; } @@ -163,6 +226,16 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw // ============================================================ // checkAccelTable — test key against window's accelerator table // ============================================================ +// +// Accelerator tables map key+modifier combos (e.g., Ctrl+S) to menu +// command IDs. Keys are pre-normalized to uppercase at registration time +// (in dvxAddAccel) so matching here is a simple linear scan with no +// allocation. Linear scan is fine because accelerator tables are small +// (typically <20 entries) and this runs at most once per keypress. +// +// BIOS keyboard quirk: Ctrl+A through Ctrl+Z come through as ASCII +// 0x01..0x1A rather than 'A'..'Z'. We reverse that mapping here so the +// user-facing accelerator definition can use plain letter keys. static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers) { if (!win->accelTable || !win->onMenu) { @@ -185,7 +258,6 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT); AccelTableT *table = win->accelTable; - // Match against pre-normalized keys (Item 6) for (int32_t i = 0; i < table->count; i++) { AccelEntryT *e = &table->entries[i]; @@ -203,6 +275,13 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t // ============================================================ // clickMenuCheckRadio — toggle check or select radio on click // ============================================================ +// +// Check items simply toggle. Radio items use an implicit grouping +// strategy: consecutive radio-type items in the menu array form a group. +// This avoids needing an explicit group ID field. When a radio item is +// clicked, we scan backward and forward from it to find the group +// boundaries, then uncheck everything in the group except the clicked +// item. This is the same approach Windows uses for menu radio groups. static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) { MenuItemT *item = &menu->items[itemIdx]; @@ -210,22 +289,18 @@ static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) { if (item->type == MenuItemCheckE) { item->checked = !item->checked; } else if (item->type == MenuItemRadioE) { - // Uncheck all radio items in the same group (consecutive radio items) - // Search backward to find group start int32_t groupStart = itemIdx; while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) { groupStart--; } - // Search forward to find group end int32_t groupEnd = itemIdx; while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) { groupEnd++; } - // Uncheck all in group, check the clicked one for (int32_t i = groupStart; i <= groupEnd; i++) { menu->items[i].checked = (i == itemIdx); } @@ -236,6 +311,13 @@ static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) { // ============================================================ // closeAllPopups — dirty all popup levels and deactivate // ============================================================ +// +// Popup menus can be nested (submenus). The popup system uses a stack +// (parentStack) where the current level is always in popup.menu/popupX/etc. +// and parent levels are saved in parentStack[0..depth-1]. Closing all +// popups means dirtying every level's screen area so the compositor +// repaints those regions, then resetting state. We must dirty every +// level individually because submenus may not overlap their parents. static void closeAllPopups(AppContextT *ctx) { if (!ctx->popup.active) { @@ -260,6 +342,12 @@ static void closeAllPopups(AppContextT *ctx) { // ============================================================ // closePopupLevel — close one submenu level (or deactivate if top) // ============================================================ +// +// Pops one level off the popup stack. If we're already at the top +// level (depth==0), the entire popup system is deactivated. Otherwise, +// the parent level's state is restored as the new "current" level. +// This enables Left Arrow to close a submenu and return to the parent, +// matching standard Windows/Motif keyboard navigation. static void closePopupLevel(AppContextT *ctx) { if (!ctx->popup.active) { @@ -303,6 +391,25 @@ static void closeSysMenu(AppContextT *ctx) { // ============================================================ // compositeAndFlush // ============================================================ +// +// The compositor is the heart of the rendering pipeline. For each dirty +// rectangle, it redraws the entire Z-ordered scene into the backbuffer +// (system RAM) then flushes that rectangle to the LFB (video memory). +// +// Rendering order per dirty rect (painter's algorithm, back-to-front): +// 1. Desktop background fill +// 2. Minimized window icons (always below all windows) +// 3. Non-minimized windows, bottom-to-top (chrome + content + scrollbars) +// 4. Popup menus (all levels, parent first, then current/deepest) +// 4b. System menu (window close-gadget menu) +// 5. Tooltip +// 6. Hardware cursor (software-rendered, always on top) +// 7. Flush dirty rect from backbuffer to LFB +// +// Pre-filtering the visible window list avoids redundant minimized/hidden +// checks in the inner loop. The clip rect is set to each dirty rect so +// draw calls outside the rect are automatically clipped by the draw layer, +// avoiding unnecessary pixel writes. static void compositeAndFlush(AppContextT *ctx) { DisplayT *d = &ctx->display; @@ -310,9 +417,11 @@ static void compositeAndFlush(AppContextT *ctx) { DirtyListT *dl = &ctx->dirty; WindowStackT *ws = &ctx->stack; + // Merge overlapping dirty rects to reduce flush count dirtyListMerge(dl); - // Pre-filter visible, non-minimized windows once (Item 7) + // Pre-filter visible, non-minimized windows once to avoid + // re-checking visibility in the inner dirty-rect loop int32_t visibleIdx[MAX_WINDOWS]; int32_t visibleCount = 0; @@ -444,11 +553,16 @@ static void compositeAndFlush(AppContextT *ctx) { // ============================================================ // dirtyCursorArea // ============================================================ +// +// Dirties a 23x23 pixel area centered on the worst-case cursor bounds. +// We use a fixed size that covers ALL cursor shapes rather than the +// current shape's exact bounds. This handles the case where the cursor +// shape changes between frames (e.g., arrow to resize) — we need to +// erase the old shape AND draw the new one, and both might have +// different hotspot offsets. The 23x23 area is the union of all possible +// cursor footprints (16x16 with hotspot at 0,0 or 7,7). static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) { - // Dirty the union of all cursor shapes at this position to handle shape changes - // All cursors are 16x16 with hotspot at either (0,0) or (7,7), so worst case - // covers from (x-7, y-7) to (x+15, y+15) = 23x23 area dirtyListAdd(&ctx->dirty, x - 7, y - 7, 23, 23); } @@ -456,6 +570,15 @@ static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y) { // ============================================================ // dispatchAccelKey — route Alt+key to menu or widget // ============================================================ +// +// Handles Alt+letter keypresses. Menu bar accelerators are checked first +// (e.g., Alt+F for File menu), then widget tree accelerators (e.g., +// Alt+O for an "&OK" button). Labels use the '&' convention to mark the +// accelerator character, matching Windows/Motif conventions. +// +// For non-focusable widgets like labels and frames, the accelerator +// transfers focus to the next focusable sibling — this lets labels act +// as access keys for adjacent input fields, following standard GUI idiom. static bool dispatchAccelKey(AppContextT *ctx, char key) { if (ctx->stack.focusedIdx < 0) { @@ -464,7 +587,7 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; - // Check menu bar first + // Menu bar accelerators take priority over widget accelerators if (win->menuBar) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) { if (win->menuBar->menus[i].accelKey == key) { @@ -613,6 +736,20 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { // ============================================================ // dispatchEvents // ============================================================ +// +// Central event dispatcher, called once per frame after polling. Handles +// mouse and input state changes in a priority chain: +// 1. Cursor position changes (always dirty old+new positions) +// 2. Active drag/resize/scroll operations (exclusive capture) +// 3. System menu interaction +// 4. Popup menu interaction (with cascading submenu support) +// 5. Left-button press (window chrome hit testing and content delivery) +// 6. Right-button press (context menu support) +// 7. Button release and mouse-move events to focused window +// +// The priority chain means that once a drag is active, all mouse events +// go to the drag handler until the button is released. This is simpler +// and more robust than a general-purpose event capture mechanism. static void dispatchEvents(AppContextT *ctx) { int32_t mx = ctx->mouseX; @@ -697,14 +834,20 @@ static void dispatchEvents(AppContextT *ctx) { } } - // Handle popup menu interaction (with cascading submenu support) + // Handle popup menu interaction (with cascading submenu support). + // Popup menus form a stack (parentStack) with the deepest submenu as + // "current". Hit testing checks the current level first. If the mouse + // is in a parent level instead, all deeper levels are closed (popped) + // so the user can navigate back up the submenu tree by moving the + // mouse to a parent menu. This matches Win3.x cascading menu behavior. if (ctx->popup.active) { - // Check if mouse is inside current (deepest) popup level bool inCurrent = (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW && my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH); if (inCurrent) { - // Hover tracking in current level + // Hover tracking: convert mouse Y to item index using fixed-point + // reciprocal multiply instead of integer divide. This avoids a + // costly division on 486 hardware where div can take 40+ cycles. int32_t relY = my - ctx->popup.popupY - POPUP_BEVEL_WIDTH; int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0; @@ -755,7 +898,10 @@ static void dispatchEvents(AppContextT *ctx) { clickMenuCheckRadio(ctx->popup.menu, itemIdx); } - // Close popup BEFORE calling onMenu (onMenu may run a nested event loop) + // Close popup BEFORE calling onMenu because the menu + // handler may open a modal dialog, which runs a nested + // dvxUpdate loop. If the popup were still active, the + // nested loop would try to draw/interact with a stale popup. int32_t menuId = item->id; WindowT *win = findWindowById(ctx, ctx->popup.windowId); closeAllPopups(ctx); @@ -862,7 +1008,12 @@ static void dispatchEvents(AppContextT *ctx) { handleMouseButton(ctx, mx, my, buttons); } - // Handle right button press — context menus + // Handle right button press — context menus. + // Context menu resolution walks UP the widget tree from the hit widget + // to find the nearest ancestor with a contextMenu set, then falls back + // to the window-level context menu. This lets containers provide menus + // that apply to all their children without requiring each child to have + // its own menu, while still allowing per-widget overrides. if ((buttons & 2) && !(prevBtn & 2)) { int32_t hitPart; int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); @@ -870,7 +1021,6 @@ static void dispatchEvents(AppContextT *ctx) { if (hitIdx >= 0 && hitPart == 0) { WindowT *win = ctx->stack.windows[hitIdx]; - // Raise and focus if not already if (hitIdx != ctx->stack.focusedIdx) { wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx); hitIdx = ctx->stack.count - 1; @@ -878,7 +1028,6 @@ static void dispatchEvents(AppContextT *ctx) { win = ctx->stack.windows[hitIdx]; } - // Check widget context menu first MenuT *ctxMenu = NULL; if (win->widgetRoot) { @@ -886,7 +1035,6 @@ static void dispatchEvents(AppContextT *ctx) { int32_t relY = my - win->y - win->contentY; WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY); - // Walk up the tree to find a context menu while (hit && !hit->contextMenu) { hit = hit->parent; } @@ -896,7 +1044,6 @@ static void dispatchEvents(AppContextT *ctx) { } } - // Fall back to window context menu if (!ctxMenu) { ctxMenu = win->contextMenu; } @@ -937,6 +1084,12 @@ static void dispatchEvents(AppContextT *ctx) { // ============================================================ // drawCursorAt // ============================================================ +// +// Software cursor rendering using AND/XOR mask pairs, the same format +// Windows uses for cursor resources. The AND mask determines transparency +// (0=opaque, 1=transparent) and the XOR mask determines color. This +// lets cursors have transparent pixels without needing alpha blending, +// which would be expensive on a 486. static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) { const CursorT *cur = &ctx->cursors[ctx->cursorId]; @@ -948,6 +1101,17 @@ static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) { // ============================================================ // drawPopupLevel — draw one popup menu (bevel + items) // ============================================================ +// +// Draws a single popup menu level (the compositor calls this for each +// level in the popup stack, parent-first). Each item gets: +// - Full-width highlight bar when hovered +// - Optional check/radio glyph in a left margin column +// - Tab-split label: left part with underlined accelerator, right-aligned shortcut +// - Submenu arrow (right-pointing triangle) if item has a subMenu +// - Separators drawn as horizontal lines +// +// The check margin is conditional: only present if any item in the menu +// has check or radio type, keeping non-checkable menus compact. 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) { RectT popRect = { px, py, pw, ph }; @@ -1103,13 +1267,18 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, // ============================================================ // 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 || table->count >= MAX_ACCEL_ENTRIES) { return; } - // Pre-normalize key for fast matching (Item 6) int32_t normKey = key; if (normKey >= 'a' && normKey <= 'z') { @@ -1126,18 +1295,20 @@ void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmd // ============================================================ -// dvxClipboardCopy +// 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); } -// ============================================================ -// dvxClipboardGet -// ============================================================ - const char *dvxClipboardGet(int32_t *outLen) { return clipboardGet(outLen); } @@ -1147,8 +1318,12 @@ const char *dvxClipboardGet(int32_t *outLen) { // dvxCascadeWindows // ============================================================ // -// Arrange all visible, non-minimized windows in a staggered -// diagonal pattern from the top-left corner. +// 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 DESQview/X and 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; @@ -1157,7 +1332,6 @@ void dvxCascadeWindows(AppContextT *ctx) { int32_t offsetY = 0; int32_t step = CHROME_TITLE_HEIGHT + CHROME_BORDER_WIDTH; - // Default cascade size: 2/3 of screen int32_t winW = screenW * 2 / 3; int32_t winH = screenH * 2 / 3; @@ -1218,6 +1392,11 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { // ============================================================ // 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) { @@ -1304,42 +1483,44 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) { // ============================================================ // 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)); - // Platform-specific initialization (signal handling, etc.) platformInit(); - // Initialize video if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { return -1; } - // Initialize blit ops + // 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); - // Initialize window stack wmInit(&ctx->stack); - - // Initialize dirty list dirtyListInit(&ctx->dirty); - // Set up font (use 8x16) + // 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; - // Set up cursors memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors)); ctx->cursorId = CURSOR_ARROW; - // Initialize colors initColorScheme(ctx); - // Cache cursor colors + // Pre-pack cursor colors once. packColor converts RGB to the native + // pixel format, which is too expensive to do per-frame. ctx->cursorFg = packColor(&ctx->display, 255, 255, 255); ctx->cursorBg = packColor(&ctx->display, 0, 0, 0); - // Initialize mouse platformMouseInit(ctx->display.width, ctx->display.height); ctx->mouseX = ctx->display.width / 2; ctx->mouseY = ctx->display.height / 2; @@ -1351,9 +1532,14 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ ctx->lastIconClickTime = 0; ctx->lastCloseClickId = -1; ctx->lastCloseClickTime = 0; + + // 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)0x10000 + (uint32_t)ctx->font.charHeight - 1) / (uint32_t)ctx->font.charHeight; - // Dirty the entire screen for first paint + // Dirty the entire screen so the first compositeAndFlush paints everything dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); return 0; @@ -1424,6 +1610,23 @@ void dvxRun(AppContextT *ctx) { // ============================================================ // 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) { @@ -1436,7 +1639,6 @@ bool dvxUpdate(AppContextT *ctx) { updateTooltip(ctx); pollAnsiTermWidgets(ctx); - // Periodically refresh one minimized window thumbnail (staggered) ctx->frameCount++; if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) { @@ -1451,7 +1653,10 @@ bool dvxUpdate(AppContextT *ctx) { platformYield(); } - // After compositing, release key-pressed button (one frame of animation) + // 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 (sKeyPressedBtn->type == WidgetImageButtonE) { sKeyPressedBtn->as.imageButton.pressed = false; @@ -1479,7 +1684,11 @@ bool dvxUpdate(AppContextT *ctx) { // dvxScreenshot // ============================================================ // -// Save the entire screen (backbuffer) to a PNG file. +// 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; @@ -1533,8 +1742,11 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) { // dvxWindowScreenshot // ============================================================ // -// Save a window's content buffer to a PNG file. This captures the -// full content area regardless of whether the window is occluded. +// 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) { @@ -1559,9 +1771,15 @@ int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) { // dvxTileWindows // ============================================================ // -// Tile all visible, non-minimized windows in a grid pattern. -// Columns = ceil(sqrt(count)), rows = ceil(count / cols). -// Last row may have fewer windows, which are widened to fill. +// 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; @@ -1629,8 +1847,10 @@ void dvxTileWindows(AppContextT *ctx) { // dvxTileWindowsH // ============================================================ // -// Tile all visible, non-minimized windows horizontally: -// side by side left to right, each window gets full screen height. +// 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; @@ -1677,8 +1897,8 @@ void dvxTileWindowsH(AppContextT *ctx) { // dvxTileWindowsV // ============================================================ // -// Tile all visible, non-minimized windows vertically: -// stacked top to bottom, each window gets full screen width. +// 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; @@ -1724,6 +1944,12 @@ void dvxTileWindowsV(AppContextT *ctx) { // ============================================================ // executeSysMenuCmd // ============================================================ +// +// Executes a system menu (window control menu) command. The system menu +// is the DESQview/X equivalent of the Win3.x control-menu box — it +// provides Restore, Move, Size, Minimize, Maximize, and Close. Keyboard +// move/resize mode is entered by setting kbMoveResize state, which causes +// pollKeyboard to intercept arrow keys until Enter/Esc. static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) { WindowT *win = findWindowById(ctx, ctx->sysMenu.windowId); @@ -1799,6 +2025,22 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) { // ============================================================ // handleMouseButton // ============================================================ +// +// Handles a left-button press that is not consumed by a drag, popup, or +// system menu. Uses wmHitTest to determine what part of what window was +// clicked: +// hitPart 0: content area (forwarded to window's onMouse callback) +// hitPart 1: title bar (begins mouse drag) +// hitPart 2: close/sys-menu gadget (single-click opens sys menu, +// double-click closes — DESQview/X convention) +// hitPart 3: resize border (begins edge/corner resize) +// hitPart 4: menu bar (opens popup for clicked menu) +// hitPart 5/6: vertical/horizontal scrollbar +// hitPart 7: minimize button +// hitPart 8: maximize/restore button +// +// Windows are raised-and-focused on click regardless of which part was +// hit, ensuring the clicked window always comes to the front. static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons) { // Modal window gating: only the modal window receives clicks @@ -1946,6 +2188,13 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t // ============================================================ // initColorScheme // ============================================================ +// +// Colors are pre-packed to native pixel format at init time so no +// per-pixel conversion is needed during drawing. The scheme is inspired +// by GEOS Ensemble with Motif-style 3D bevels: teal desktop, grey window +// chrome with white highlights and dark shadows to create the raised/sunken +// illusion. The dark charcoal active title bar distinguishes it from +// GEOS's blue, giving DV/X its own identity. static void initColorScheme(AppContextT *ctx) { DisplayT *d = &ctx->display; @@ -1977,6 +2226,13 @@ static void initColorScheme(AppContextT *ctx) { // ============================================================ // openContextMenu — open a context menu at a screen position // ============================================================ +// +// Context menus reuse the same popup system as menu bar popups but with +// isContextMenu=true. The difference affects dismiss behavior: context +// menus close on any click outside (since there's no menu bar to switch +// to), while menu bar popups allow horizontal mouse movement to switch +// between top-level menus. Position is clamped to screen edges so the +// popup doesn't go off-screen. static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) { if (!menu || menu->itemCount <= 0) { @@ -2016,6 +2272,12 @@ static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t // ============================================================ // openPopupAtMenu — open top-level popup for a menu bar menu // ============================================================ +// +// Opens the dropdown for a menu bar item (e.g., "File", "Edit"). Any +// existing popup chain is closed first, then a new top-level popup is +// positioned directly below the menu bar item, aligned with its barX +// coordinate. This is called both from mouse clicks on the menu bar +// and from keyboard navigation (Alt+key, Left/Right arrows). static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) { if (!win->menuBar || menuIdx < 0 || menuIdx >= win->menuBar->menuCount) { @@ -2047,6 +2309,11 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) { // ============================================================ // openSubMenu — open submenu for the currently hovered item // ============================================================ +// +// Pushes the current popup state onto parentStack and opens the submenu +// as the new current level. The submenu is positioned at the right edge +// of the current popup, vertically aligned with the hovered item. +// MAX_SUBMENU_DEPTH prevents runaway nesting from overflowing the stack. static void openSubMenu(AppContextT *ctx) { if (!ctx->popup.active || !ctx->popup.menu) { @@ -2096,9 +2363,17 @@ static void openSubMenu(AppContextT *ctx) { // ============================================================ // openSysMenu // ============================================================ +// +// The system menu is a separate popup from the regular menu system +// because it has different semantics: it's tied to the window's close +// gadget (top-left icon), uses its own SysMenuItemT type with +// enabled/disabled state, and dispatches to executeSysMenuCmd rather +// than the window's onMenu callback. Items are dynamically enabled +// based on window state (e.g., Restore is only enabled when maximized, +// Size is disabled when maximized or non-resizable). Triggered by +// single-click on the close gadget or Alt+Space. static void openSysMenu(AppContextT *ctx, WindowT *win) { - // Close any existing popup menus first closeAllPopups(ctx); if (ctx->sysMenu.active) { @@ -2108,8 +2383,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) { ctx->sysMenu.itemCount = 0; ctx->sysMenu.windowId = win->id; - - // Restore — enabled only when maximized SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++]; strncpy(item->label, "&Restore", MAX_MENU_LABEL - 1); item->cmd = SysMenuRestoreE; @@ -2194,6 +2467,15 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) { // ============================================================ // pollAnsiTermWidgets — poll and repaint all ANSI term widgets // ============================================================ +// +// ANSI terminal widgets have asynchronous data sources (PTYs, serial +// ports) that produce output between frames. This function walks every +// window's widget tree looking for AnsiTerm widgets, polls them for new +// data, and if data arrived, triggers a targeted repaint of just the +// affected rows. The fine-grained dirty rect (just the changed rows +// rather than the whole window) is critical for terminal performance — +// a single character echo should only flush one row to the LFB, not +// the entire terminal viewport. static void pollAnsiTermWidgets(AppContextT *ctx) { for (int32_t i = 0; i < ctx->stack.count; i++) { @@ -2240,30 +2522,51 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) // ============================================================ // pollKeyboard // ============================================================ +// +// Drains the keyboard buffer and dispatches each key through a priority +// chain. The priority order is important — higher priority handlers +// consume the key and skip lower ones via 'continue': +// +// 1. Alt+Tab / Shift+Alt+Tab — window cycling (always works) +// 2. Alt+F4 — close focused window +// 3. F10 — activate/toggle menu bar +// 4. Keyboard move/resize mode (arrow keys captured exclusively) +// 5. Alt+Space — system menu toggle +// 6. System menu keyboard navigation (arrows, enter, esc, accel) +// 7. Alt+key — menu bar / widget accelerator dispatch +// 8. Popup menu keyboard navigation (arrows, enter, esc, accel) +// 9. Accelerator table on focused window (Ctrl+S, etc.) +// 10. Tab/Shift+Tab — widget focus cycling +// 11. Fall-through to focused window's onKey callback +// +// Key encoding: ASCII keys use their ASCII value; extended keys (arrows, +// function keys) use scancode | 0x100 to distinguish from ASCII 0. +// This avoids needing a separate "is_extended" flag. static void pollKeyboard(AppContextT *ctx) { - // Read modifier state once per poll int32_t shiftFlags = platformKeyboardGetModifiers(); ctx->keyModifiers = shiftFlags; bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift - // Process buffered keys PlatformKeyEventT evt; while (platformKeyboardRead(&evt)) { int32_t scancode = evt.scancode; int32_t ascii = evt.ascii; - // Alt+Tab / Shift+Alt+Tab — cycle windows - // Alt+Tab: scancode=0xA5, ascii=0x00 + // Alt+Tab / Shift+Alt+Tab — cycle windows. + // Unlike Windows, there's no task-switcher overlay here — each press + // immediately rotates the window stack and focuses the new top. + // Alt+Tab rotates the top window to the bottom of the stack (so the + // second window becomes visible). Shift+Alt+Tab does the reverse, + // pulling the bottom window to the top. if (ascii == 0 && scancode == 0xA5) { if (ctx->stack.count > 1) { if (shiftHeld) { - // Shift+Alt+Tab — focus the bottom window, raise it wmRaiseWindow(&ctx->stack, &ctx->dirty, 0); wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); } else { - // Alt+Tab — send top window to bottom, focus new top + // Rotate: move top to bottom, shift everything else up WindowT *top = ctx->stack.windows[ctx->stack.count - 1]; dirtyListAdd(&ctx->dirty, top->x, top->y, top->w, top->h); @@ -2903,9 +3206,14 @@ static void pollMouse(AppContextT *ctx) { // refreshMinimizedIcons // ============================================================ // -// Dirty the next minimized window icon whose content has changed -// since the last refresh. Only considers windows without custom -// iconData. Called every ICON_REFRESH_INTERVAL frames to stagger. +// Minimized windows show a thumbnail of their content. When the content +// changes (e.g., a terminal receives output while minimized), the icon +// thumbnail needs updating. Rather than refreshing all dirty icons every +// frame (which could cause a burst of repaints), this function refreshes +// at most ONE icon per call, using a round-robin index (iconRefreshIdx) +// so each dirty icon gets its turn. Called every ICON_REFRESH_INTERVAL +// frames, this spreads the cost across time. Windows with custom iconData +// (loaded from .bmp/.png) are skipped since their thumbnails don't change. static void refreshMinimizedIcons(AppContextT *ctx) { WindowStackT *ws = &ctx->stack; @@ -2944,6 +3252,12 @@ static void refreshMinimizedIcons(AppContextT *ctx) { // ============================================================ // repositionWindow — move/resize a window, dirty old & new, fire callbacks // ============================================================ +// +// Shared helper for tiling/cascading. Dirties both the old and new +// positions (the old area needs repainting because the window moved away, +// the new area needs repainting because the window appeared there). +// Also reallocates the content buffer and fires onResize/onPaint so the +// window's content scales to the new dimensions. static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) { // Dirty old position @@ -2980,6 +3294,19 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t // ============================================================ // updateCursorShape // ============================================================ +// +// Updates the software cursor shape based on what the mouse is hovering +// over. The cursor is software-rendered (drawn in the compositor pass) +// rather than using a hardware cursor because VESA VBE doesn't provide +// hardware cursor support, and hardware cursors on VGA are limited to +// text mode. The shape priority is: +// 1. Active resize drag — keep the edge-specific resize cursor +// 2. ListView column resize drag +// 3. Splitter drag +// 4. Hover over resize edge — show directional resize cursor +// 5. Hover over ListView column border — horizontal resize cursor +// 6. Hover over splitter bar — orientation-specific resize cursor +// 7. Default arrow cursor static void updateCursorShape(AppContextT *ctx) { int32_t newCursor = CURSOR_ARROW; @@ -3099,6 +3426,16 @@ static void updateCursorShape(AppContextT *ctx) { // ============================================================ // updateTooltip — show/hide tooltip based on hover state // ============================================================ +// +// Tooltip lifecycle: when the mouse stops moving over a widget that has +// a tooltip string set, a timer starts. After TOOLTIP_DELAY_MS (500ms), +// the tooltip appears. Any mouse movement or button press hides it and +// resets the timer. This avoids tooltip flicker during normal mouse use +// while still being responsive when the user hovers intentionally. +// +// The widget lookup walks into NO_HIT_RECURSE containers (like toolbars) +// to find the deepest child with a tooltip, so toolbar buttons can have +// individual tooltips even though the toolbar itself handles hit testing. static void updateTooltip(AppContextT *ctx) { clock_t now = clock(); diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index b36f2dd..9c8d402 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -1,4 +1,15 @@ // dvx_app.h — Layer 5: Application API for DVX GUI +// +// The topmost layer and the public-facing API for applications. Aggregates +// all lower layers into a single AppContextT and provides a clean interface +// for window creation, event dispatch, and utilities. Applications interact +// exclusively through dvx*() functions and window callbacks — they never +// need to call the lower WM, compositor, or draw layers directly. +// +// The event loop (dvxRun/dvxUpdate) follows a cooperative model: poll mouse +// and keyboard, dispatch events to the focused window, run the compositor +// for dirty regions, then yield. There's no preemptive scheduling — the +// application must return from callbacks promptly. #ifndef DVX_APP_H #define DVX_APP_H @@ -13,6 +24,13 @@ // ============================================================ // Application context // ============================================================ +// +// Single monolithic context that owns all GUI state. Allocated on the +// caller's stack (or statically) — no internal heap allocation for the +// context itself. This "god struct" approach keeps the API simple (one +// pointer to pass everywhere) and avoids the overhead of a handle-based +// system with opaque lookups. The tradeoff is a large struct, but on DOS +// memory layout is flat and cache pressure isn't a concern at this level. typedef struct AppContextT { DisplayT display; @@ -33,57 +51,88 @@ typedef struct AppContextT { int32_t mouseY; int32_t mouseButtons; int32_t keyModifiers; // current BIOS shift state (KEY_MOD_xxx) + // Previous-frame mouse state for edge detection (button press/release + // transitions and delta-based cursor movement). int32_t prevMouseX; int32_t prevMouseY; int32_t prevMouseButtons; + // Double-click detection for minimized window icons: timestamps and + // window IDs track whether two clicks land on the same icon within + // the system double-click interval. clock_t lastIconClickTime; int32_t lastIconClickId; // window ID of last-clicked minimized icon (-1 = none) clock_t lastCloseClickTime; int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none) + // Minimized icon thumbnails are refreshed one per frame (round-robin) + // rather than all at once, to amortize the cost of scaling the content + // buffer down to icon size. int32_t iconRefreshIdx; // next minimized icon to refresh (staggered) int32_t frameCount; // frame counter for periodic tasks + // The idle callback allows the host application (e.g. the DV/X shell) + // to do background work (poll serial ports, service DXE apps) during + // frames where no GUI events occurred, instead of just yielding the CPU. void (*idleCallback)(void *ctx); // called instead of yield when non-NULL void *idleCtx; WindowT *modalWindow; // if non-NULL, only this window receives input - // Tooltip state + // Tooltip state — tooltip appears after the mouse hovers over a widget + // with a tooltip string for a brief delay. Pre-computing W/H avoids + // re-measuring on every paint frame. clock_t tooltipHoverStart; // when mouse stopped moving const char *tooltipText; // text to show (NULL = hidden) int32_t tooltipX; // screen position int32_t tooltipY; int32_t tooltipW; // size (pre-computed) int32_t tooltipH; + // Fixed-point reciprocal (16.16) of font.charHeight, pre-computed so + // that dividing by charHeight (needed for pixel-to-row conversion in + // terminal/text widgets) becomes a multiply+shift instead of an + // integer divide, which is very slow on 486 (40+ cycles per DIV). uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight } AppContextT; -// Initialize the application (VESA mode, input, etc.) +// Initialize the entire GUI stack: video mode, input devices, font, +// color scheme, cursor shapes, and internal state. This is the single +// entry point for starting a DVX application. Returns 0 on success. int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); -// Shut down and restore text mode +// Tear down the GUI stack in reverse order: destroy all windows, restore +// text mode, release input devices. Safe to call after a failed dvxInit(). void dvxShutdown(AppContextT *ctx); -// Run the main event loop (returns when ctx->running is set to false) +// Enter the main event loop. Polls input, dispatches events, composites +// dirty regions, and yields on each iteration. Returns when ctx->running +// becomes false (typically from dvxQuit() or a close callback). void dvxRun(AppContextT *ctx); -// Process one iteration of the event loop. -// Returns true if the GUI is still running, false if it wants to exit. +// Process exactly one frame of the event loop. Provided for applications +// that need to integrate the GUI into their own main loop (e.g. the DV/X +// shell polling serial ports between frames). Returns false when the GUI +// wants to exit. bool dvxUpdate(AppContextT *ctx); -// Create a window +// Create a window at an explicit screen position. The window is raised +// to the top, focused, and its entire region is dirtied for the next +// composite pass. WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable); -// Create a centered window (position computed from screen size) +// Convenience wrapper that centers the window on screen. WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, int32_t h, bool resizable); -// Destroy a window +// Destroy a window, free all its resources, and dirty its former region. void dvxDestroyWindow(AppContextT *ctx, WindowT *win); -// Resize a window to fit its widget tree's minimum size +// Resize a window to exactly fit its widget tree's computed minimum size +// (plus chrome). Used for dialog boxes and other fixed-layout windows +// where the window should shrink-wrap its content. void dvxFitWindow(AppContextT *ctx, WindowT *win); -// Invalidate a region of a window's content area (triggers repaint) +// Mark a sub-region of a window's content area as needing repaint. The +// coordinates are relative to the content area, not the screen. The +// onPaint callback will be called during the next composite pass with +// the dirty area. void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); -// Invalidate entire window content +// Mark the entire window content area as dirty. void dvxInvalidateWindow(AppContextT *ctx, WindowT *win); // Minimize a window (show as icon at bottom of screen) @@ -95,7 +144,9 @@ void dvxMaximizeWindow(AppContextT *ctx, WindowT *win); // Request exit from main loop void dvxQuit(AppContextT *ctx); -// Save the entire screen to a PNG file (returns 0 on success, -1 on failure) +// Save the entire screen (backbuffer contents) to a PNG file. Converts +// from native pixel format to RGB for the PNG encoder. Returns 0 on +// success, -1 on failure. int32_t dvxScreenshot(AppContextT *ctx, const char *path); // Set window title @@ -119,31 +170,43 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path); // Save a window's content to a PNG file (returns 0 on success, -1 on failure) int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path); -// Create an accelerator table (caller must free with dvxFreeAccelTable) +// Allocate a new accelerator table. Attach it to a window via +// win->accelTable. The event loop checks the focused window's accel +// table on every keystroke and fires onMenu(cmdId) on match. AccelTableT *dvxCreateAccelTable(void); -// Free an accelerator table +// Free an accelerator table and its entries. void dvxFreeAccelTable(AccelTableT *table); -// Add an entry to an accelerator table +// Register a keyboard shortcut. key is an ASCII char or KEY_Fxx constant. +// modifiers is a bitmask of ACCEL_CTRL/ACCEL_SHIFT/ACCEL_ALT. cmdId is +// passed to the window's onMenu callback, matching the convention used +// for menu item IDs so the same handler works for both menus and hotkeys. void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId); -// Arrange windows in a staggered diagonal cascade pattern +// Window arrangement functions — operate on all visible, non-minimized +// windows. These mirror the classic Windows 3.x "Window" menu commands. + +// Cascade: offset each window diagonally by the title bar height. void dvxCascadeWindows(AppContextT *ctx); -// Tile windows in a grid pattern +// Grid tile: arrange windows in an NxM grid filling the screen. void dvxTileWindows(AppContextT *ctx); -// Tile windows horizontally (side by side left to right, full height) +// Horizontal tile: side by side, equal width, full height. void dvxTileWindowsH(AppContextT *ctx); -// Tile windows vertically (stacked top to bottom, full width) +// Vertical tile: stacked, full width, equal height. void dvxTileWindowsV(AppContextT *ctx); -// Copy text to the shared clipboard +// Copy text to the process-wide clipboard buffer. The clipboard is a +// simple static buffer (not inter-process) — adequate for copy/paste +// within the DVX environment on single-tasking DOS. void dvxClipboardCopy(const char *text, int32_t len); -// Get clipboard contents (returns pointer to internal buffer, NULL if empty) +// Retrieve the current clipboard contents. Returns a pointer to the +// internal static buffer (valid until the next dvxClipboardCopy), or +// NULL if the clipboard is empty. const char *dvxClipboardGet(int32_t *outLen); #endif // DVX_APP_H diff --git a/dvx/dvxComp.c b/dvx/dvxComp.c index 0ec9279..63ae600 100644 --- a/dvx/dvxComp.c +++ b/dvx/dvxComp.c @@ -1,10 +1,33 @@ // dvx_comp.c — Layer 3: Dirty rectangle compositor for DVX GUI (optimized) +// +// This layer implements dirty rectangle tracking and merging. The compositor +// avoids full-screen redraws, which would be prohibitively expensive on the +// target 486/Pentium hardware over ISA bus VESA LFB. A full 640x480x16bpp +// framebuffer is ~600KB — at ISA's ~8MB/s theoretical peak, a blind full +// flush costs ~75ms (>1 frame at 60Hz). By tracking which rectangles have +// actually changed and flushing only those regions from the system RAM +// backbuffer to the LFB, the bandwidth consumed per frame scales with the +// amount of visual change rather than the screen resolution. +// +// The compositing loop lives in dvxApp.c (compositeAndFlush). For each dirty +// rect, it repaints the desktop, then walks the window stack bottom-to-top +// painting chrome, content, scrollbars, popup menus, and the cursor — all +// clipped to the dirty rect. Only then is the dirty rect flushed to the LFB. +// This means each pixel in a dirty region is written to system RAM potentially +// multiple times (painter's algorithm), but the expensive LFB write happens +// exactly once per pixel per frame. #include "dvxComp.h" #include "platform/dvxPlatform.h" #include +// Rects within this many pixels of each other get merged even if they don't +// overlap. A small gap tolerance absorbs jitter from mouse movement and +// closely-spaced UI invalidations (e.g. title bar + content during a drag) +// without bloating merged rects excessively. The value 4 was chosen to match +// the chrome border width — adjacent chrome/content invalidations merge +// naturally. #define DIRTY_MERGE_GAP 4 // ============================================================ @@ -18,16 +41,33 @@ static inline void rectUnion(const RectT *a, const RectT *b, RectT *result); // ============================================================ // dirtyListAdd // ============================================================ +// +// Appends a dirty rect to the list. Uses a fixed-size array (MAX_DIRTY_RECTS +// = 128) rather than a dynamic allocation — this is called on every UI +// mutation (drag, repaint, focus change) so allocation overhead must be zero. +// +// When the list fills up, an eager merge pass tries to consolidate rects. +// If the list is STILL full after merging (pathological scatter), the +// nuclear option collapses everything into one bounding box. This guarantees +// the list never overflows, at the cost of potentially over-painting a large +// rect. In practice the merge pass almost always frees enough slots because +// GUI mutations tend to cluster spatially. void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) { + // Branch hint: degenerate rects are rare — callers usually validate first if (__builtin_expect(w <= 0 || h <= 0, 0)) { return; } + // Overflow path: try merging, then fall back to a single bounding rect if (__builtin_expect(dl->count >= MAX_DIRTY_RECTS, 0)) { dirtyListMerge(dl); if (dl->count >= MAX_DIRTY_RECTS) { + // Still full — collapse the entire list plus the new rect into one + // bounding box. This is a last resort; it means the next flush will + // repaint a potentially large region, but at least we won't lose + // dirty information or crash. RectT merged = dl->rects[0]; for (int32_t i = 1; i < dl->count; i++) { @@ -72,16 +112,31 @@ void dirtyListInit(DirtyListT *dl) { // ============================================================ // dirtyListMerge // ============================================================ +// +// Coalesces overlapping or nearby dirty rects to reduce the number of +// composite+flush passes. The trade-off: merging two rects into their +// bounding box may add "clean" pixels that get needlessly repainted, but +// this is far cheaper than the per-rect overhead of an extra composite +// pass (clip setup, window-stack walk, LFB flush). On 486/Pentium ISA, +// the LFB write latency per-rect dominates, so fewer larger rects win. +// +// Algorithm: O(N^2) pairwise sweep with bounded restarts. For each rect i, +// scan all rects j>i and merge any that overlap or are within DIRTY_MERGE_GAP +// pixels. When a merge happens, rect i grows and may now overlap rects that +// it previously missed, so the inner scan restarts — but restarts are capped +// at 3 per slot to prevent O(N^3) cascading in pathological layouts (e.g. +// a diagonal scatter of tiny rects). The cap of 3 was chosen empirically: +// typical GUI operations produce clustered invalidations that converge in +// 1-2 passes; 3 gives a safety margin without measurable overhead. +// +// Merged-away rects are removed by swap-with-last (O(1) removal from an +// unordered list), which is why the rects array is not kept sorted. void dirtyListMerge(DirtyListT *dl) { if (dl->count <= 1) { return; } - // O(N²) with bounded restarts: for each rect, try to merge it - // into an earlier rect. When a merge succeeds the merged rect - // may now overlap others, so restart the inner scan — but cap - // restarts per slot to avoid O(N³) pathological cascades. for (int32_t i = 0; i < dl->count; i++) { int32_t restarts = 0; bool merged = true; @@ -92,6 +147,7 @@ void dirtyListMerge(DirtyListT *dl) { for (int32_t j = i + 1; j < dl->count; j++) { if (rectsOverlapOrAdjacent(&dl->rects[i], &dl->rects[j], DIRTY_MERGE_GAP)) { rectUnion(&dl->rects[i], &dl->rects[j], &dl->rects[i]); + // Swap-with-last removal: order doesn't matter for merging dl->rects[j] = dl->rects[dl->count - 1]; dl->count--; j--; @@ -108,6 +164,16 @@ void dirtyListMerge(DirtyListT *dl) { // ============================================================ // flushRect // ============================================================ +// +// Copies one dirty rect from the system RAM backbuffer to the VESA LFB. +// This is the single most bandwidth-sensitive operation in the entire GUI: +// the LFB lives behind the ISA/PCI bus, so every byte written here is a +// bus transaction. The platform layer (platformFlushRect) uses rep movsd +// on 486+ to move aligned 32-bit words, maximizing bus utilization. +// +// Crucially, we flush per dirty rect AFTER all painting for that rect is +// complete. This avoids visible tearing — the LFB is never in a half-painted +// state for any given region. void flushRect(DisplayT *d, const RectT *r) { platformFlushRect(d, r); @@ -117,6 +183,12 @@ void flushRect(DisplayT *d, const RectT *r) { // ============================================================ // rectIntersect // ============================================================ +// +// Used heavily in the compositing loop to test whether a window overlaps +// a dirty rect before painting it. The branch hint marks the non-overlapping +// case as unlikely because the compositing loop already does a coarse AABB +// check before calling this — when we get here, intersection is expected. +// The min/max formulation avoids branches in the hot path. bool rectIntersect(const RectT *a, const RectT *b, RectT *result) { int32_t ix1 = a->x > b->x ? a->x : b->x; @@ -149,6 +221,15 @@ bool rectIsEmpty(const RectT *r) { // ============================================================ // rectsOverlapOrAdjacent // ============================================================ +// +// Separating-axis test with a gap tolerance. Two rects merge if they +// overlap OR if the gap between them is <= DIRTY_MERGE_GAP pixels. +// The gap tolerance is the key tuning parameter for the merge algorithm: +// too small and you get many tiny rects (expensive per-rect flush overhead); +// too large and you merge distant rects into one huge bounding box +// (wasted repaint of clean pixels). The early-out on each axis makes this +// very cheap for non-overlapping rects, which is the common case during +// the inner loop of dirtyListMerge. static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_t gap) { if (a->x + a->w + gap < b->x) { return false; } @@ -162,6 +243,15 @@ static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_ // ============================================================ // rectUnion // ============================================================ +// +// Axis-aligned bounding box of two rects. Supports in-place operation +// (result == a) for the merge loop. Note that this may grow the rect +// substantially if the two inputs are far apart — this is the inherent +// cost of bounding-box merging vs. maintaining a true region (list of +// non-overlapping rects). Bounding-box was chosen because the merge +// list is bounded to 128 entries and the extra repaint cost of a few +// clean pixels is negligible compared to the complexity of a proper +// region algebra on 486-class hardware. static inline void rectUnion(const RectT *a, const RectT *b, RectT *result) { int32_t x1 = a->x < b->x ? a->x : b->x; diff --git a/dvx/dvxComp.h b/dvx/dvxComp.h index 127ba5d..494bc99 100644 --- a/dvx/dvxComp.h +++ b/dvx/dvxComp.h @@ -1,28 +1,56 @@ // dvx_comp.h — Layer 3: Dirty rectangle compositor for DVX GUI +// +// The compositor tracks which screen regions have changed and ensures +// only those regions are redrawn and flushed to video memory. This is +// the key to acceptable frame rates on 486/Pentium hardware — a full +// 640x480x32bpp frame is 1.2 MB, but a typical dirty region during +// normal interaction (e.g. typing in a text field) might be a few KB. +// +// The compositing pipeline each frame: +// 1. Layers above (WM, app) call dirtyListAdd() for changed regions +// 2. dirtyListMerge() consolidates overlapping/adjacent rects +// 3. For each merged dirty rect, the compositor clips and redraws +// the desktop, then each window (back-to-front, painter's algorithm) +// 4. flushRect() copies each dirty rect from backBuf to the LFB +// +// The merge step is important because many small dirty rects (e.g. from +// a window drag) often cluster together, and flushing one large rect is +// much faster than many small ones due to reduced per-rect overhead and +// better memory bus utilization. #ifndef DVX_COMP_H #define DVX_COMP_H #include "dvxTypes.h" -// Initialize the dirty list +// Zero the dirty rect count. Called at the start of each frame. void dirtyListInit(DirtyListT *dl); -// Add a rectangle to the dirty list +// Enqueue a dirty rectangle. If the list is full (MAX_DIRTY_RECTS), +// this should degrade gracefully (e.g. expand an existing rect). void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h); -// Merge overlapping/adjacent dirty rects to reduce redraw work +// Consolidate the dirty list by merging overlapping and adjacent rects. +// Uses an iterative pairwise merge: for each pair of rects, if merging +// them doesn't increase the total area by more than a threshold, they're +// combined. This reduces the number of compositor passes and LFB flush +// operations at the cost of potentially redrawing some clean pixels. void dirtyListMerge(DirtyListT *dl); -// Clear the dirty list +// Reset the dirty list to empty. void dirtyListClear(DirtyListT *dl); -// Flush a single rectangle from backbuffer to LFB +// Copy a rectangle from the system RAM backbuffer to the LFB (video memory). +// This is the final step — the only place where the real framebuffer is +// written. Uses platform-specific fast copy (rep movsd on DOS) for each +// scanline of the rect. void flushRect(DisplayT *d, const RectT *r); -// Intersect two rectangles, returns true if intersection is non-empty +// Compute the intersection of two rectangles. Returns true if they overlap, +// with the intersection written to result. Used extensively during +// compositing to clip window content to dirty regions. bool rectIntersect(const RectT *a, const RectT *b, RectT *result); -// Test if a rectangle is empty (zero or negative area) +// Returns true if the rectangle has zero or negative area. bool rectIsEmpty(const RectT *r); #endif // DVX_COMP_H diff --git a/dvx/dvxCursor.h b/dvx/dvxCursor.h index 409830e..ccd49de 100644 --- a/dvx/dvxCursor.h +++ b/dvx/dvxCursor.h @@ -1,4 +1,19 @@ // dvxCursor.h — Embedded mouse cursor bitmaps for DVX GUI +// +// All cursor shapes are compiled in as static const data — no external +// cursor files to load. This is intentional: the cursors are needed +// before any file I/O infrastructure is ready, and embedding them avoids +// a runtime dependency on a file path. +// +// Each cursor is a 16x16 bitmap defined as AND mask + XOR data arrays +// (one uint16_t per row). The AND/XOR encoding is the standard IBM VGA +// hardware cursor scheme: AND mask selects transparency, XOR data selects +// black vs. white for opaque pixels. See CursorT in dvxTypes.h for +// the full pixel-state truth table. +// +// The cursor table (dvxCursors[]) at the bottom provides all five shapes +// in an array indexed by CURSOR_xxx constants, with hotspot coordinates +// set appropriately (arrow hot spot at tip, resize cursors at center). #ifndef DVX_CURSOR_H #define DVX_CURSOR_H @@ -255,7 +270,8 @@ static const CursorT dvxCursors[CURSOR_COUNT] = { { 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW }; -// Legacy alias for backward compatibility +// Legacy alias — kept for backward compatibility with code that predates +// the multi-cursor support. static const CursorT dvxCursor = { 16, 16, 0, 0, cursorArrowAnd, cursorArrowXor }; #endif // DVX_CURSOR_H diff --git a/dvx/dvxDialog.c b/dvx/dvxDialog.c index bf795e1..2b33528 100644 --- a/dvx/dvxDialog.c +++ b/dvx/dvxDialog.c @@ -1,4 +1,24 @@ // dvxDialog.c — Modal dialogs for DVX GUI +// +// Provides two standard dialog types: message boxes and file dialogs. +// Both use the nested-event-loop modal pattern: the dialog creates a +// window, sets it as the AppContext's modalWindow, then runs dvxUpdate +// in a tight loop until the user dismisses the dialog. This blocks the +// caller's code flow, which is the simplest possible modal API — the +// caller gets the result as a return value, no callbacks needed. +// +// The nested loop approach works because dvxUpdate is re-entrant: it +// polls input, dispatches events, and composites. The modalWindow field +// causes handleMouseButton to reject clicks on non-modal windows, so +// only the dialog receives interaction. This is exactly how Windows +// MessageBox and GetOpenFileName work internally. +// +// State for each dialog type is stored in a single static struct (sMsgBox, +// sFd) rather than on the heap. This means only one dialog of each type +// can be active at a time, but that's fine — you never need two file +// dialogs simultaneously. The static approach avoids malloc/free and +// keeps the state accessible to callback functions without threading +// context pointers through every widget callback. #include "dvxDialog.h" #include "platform/dvxPlatform.h" @@ -17,12 +37,21 @@ // Constants // ============================================================ +// Message box layout constants. MSG_MAX_WIDTH caps dialog width so long +// messages wrap rather than producing absurdly wide dialogs. #define MSG_MAX_WIDTH 320 #define MSG_PADDING 8 #define ICON_AREA_WIDTH 40 #define BUTTON_WIDTH 80 #define BUTTON_HEIGHT 24 #define BUTTON_GAP 8 + +// Icon glyph constants. Icons are drawn procedurally (pixel by pixel) +// rather than using bitmap resources. This avoids needing to ship and +// load icon files, and the glyphs scale-by-design since they're +// defined in terms of geometric shapes. The circle shapes use the +// distance-squared test (d2 between INNER_R2 and OUTER_R2) to draw +// a ring without needing floating-point sqrt. #define ICON_GLYPH_SIZE 24 // icon glyph drawing area (pixels) #define ICON_GLYPH_CENTER 12 // center of icon glyph (ICON_GLYPH_SIZE / 2) #define ICON_OUTER_R2 144 // outer circle radius squared (12*12) @@ -58,17 +87,23 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo // ============================================================ // Message box state (one active at a time) // ============================================================ +// +// Static because only one message box can be active at a time. +// The 'done' flag is set by button clicks or window close, which +// breaks the nested dvxUpdate loop in dvxMessageBox. Layout values +// (textX, textY, textMaxW, msgAreaH) are computed once when the +// dialog opens and reused by the paint callback. typedef struct { AppContextT *ctx; - int32_t result; - bool done; + int32_t result; // ID_OK, ID_CANCEL, ID_YES, ID_NO, etc. + bool done; // set true to break the modal loop const char *message; int32_t iconType; - int32_t textX; + int32_t textX; // pre-computed text origin (accounts for icon) int32_t textY; - int32_t textMaxW; - int32_t msgAreaH; + int32_t textMaxW; // max text width for word wrapping + int32_t msgAreaH; // height of the message+icon area above the buttons } MsgBoxStateT; static MsgBoxStateT sMsgBox; @@ -77,6 +112,26 @@ static MsgBoxStateT sMsgBox; // ============================================================ // drawIconGlyph — draw a simple icon shape // ============================================================ +// +// Procedurally draws message box icons using only integer math: +// MB_ICONINFO: circle with 'i' (information) +// MB_ICONWARNING: triangle with '!' (warning) +// MB_ICONERROR: circle with 'X' (error) +// MB_ICONQUESTION: circle with '?' (question) +// +// Circles use the integer distance-squared test: for each pixel, compute +// dx*dx + dy*dy and check if it falls between INNER_R2 and OUTER_R2 +// to draw a 2-pixel-wide ring. This is a brute-force O(n^2) approach +// but n is only 24 pixels, so it's 576 iterations — trivial even on a 486. +// +// The inner symbols (i, !, X, ?) are drawn with hardcoded rectFill calls +// at specific pixel offsets. This is less elegant than using the font +// renderer, but it gives precise control over the glyph appearance at +// this small size where the 8x16 bitmap font would look too blocky. +// +// Drawing 1x1 rects for individual pixels is intentional: it goes +// through the clip rect check in rectFill, so we don't need separate +// bounds checking here. static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color) { if (iconType == MB_ICONINFO) { @@ -155,6 +210,20 @@ static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y // ============================================================ // dvxMessageBox // ============================================================ +// +// Creates and runs a modal message box. The flags parameter is a bitmask: +// low nibble selects button set (MB_OK, MB_YESNO, etc.), next nibble +// selects icon type (MB_ICONINFO, MB_ICONERROR, etc.). This is the same +// flag encoding Windows MessageBox uses, which makes porting code easier. +// +// The dialog is auto-sized to fit the word-wrapped message text plus the +// button row. Non-resizable (maxW/maxH clamped to initial size) because +// resizing a message box serves no purpose. +// +// Button labels use '&' to mark accelerator keys (e.g., "&OK" makes +// Alt+O activate the button). Button IDs are stored in widget->userData +// via intptr_t cast — a common pattern when you need to associate a +// small integer with a widget without allocating a separate struct. int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) { int32_t btnFlags = flags & 0x000F; @@ -310,16 +379,20 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, RectT fullRect = { 0, 0, win->contentW, win->contentH }; win->onPaint(win, &fullRect); - // Set as modal (save previous in case we're stacking modals) + // Save previous modal so stacked modals work correctly. This happens + // when a message box opens from within a file dialog (e.g., overwrite + // confirmation) — the file dialog's modal is pushed and restored + // when the message box closes. WindowT *prevModal = ctx->modalWindow; ctx->modalWindow = win; - // Nested event loop + // Nested event loop — blocks here until user clicks a button or closes. + // dvxUpdate handles all input/rendering; sMsgBox.done is set by the + // button onClick callback or the window close callback. while (!sMsgBox.done && ctx->running) { dvxUpdate(ctx); } - // Clean up — restore previous modal ctx->modalWindow = prevModal; dvxDestroyWindow(ctx, win); @@ -351,6 +424,15 @@ static void onMsgBoxClose(WindowT *win) { // ============================================================ // onMsgBoxPaint — custom paint: background + text/icon + widgets // ============================================================ +// +// The message box uses a custom onPaint callback rather than pure widgets +// because the message text area with optional icon doesn't map cleanly to +// the widget model. The paint callback creates a temporary display context +// that points at the window's content buffer (not the screen backbuffer), +// draws the background/text/icon directly, then runs the widget layout +// and paint for the button row. The separator line between the message +// area and buttons uses a highlight-over-shadow pair to create a Motif +// etched-line effect. static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) { (void)dirtyArea; @@ -358,7 +440,9 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) { MsgBoxStateT *state = (MsgBoxStateT *)win->userData; AppContextT *ctx = state->ctx; - // Set up display context pointing at content buffer + // Create a temporary display context targeting the window's content + // buffer. This is the standard pattern for drawing into a window's + // private buffer rather than the screen backbuffer. DisplayT cd = ctx->display; cd.lfb = win->contentBuf; cd.backBuf = win->contentBuf; @@ -406,6 +490,13 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) { // ============================================================ // wordWrapDraw — draw word-wrapped text // ============================================================ +// +// Simple greedy word-wrap: fill each line with as many characters as fit +// within maxW, breaking at the last space if the line overflows. If a +// single word is longer than maxW, it gets its own line (may be clipped). +// Explicit newlines are honored. This is a fixed-width font, so "max +// chars per line" is just maxW / charWidth — no per-character width +// accumulation needed. static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) { int32_t charW = font->charWidth; @@ -461,6 +552,12 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo // ============================================================ // wordWrapHeight — compute height of word-wrapped text // ============================================================ +// +// Duplicates the word-wrap logic from wordWrapDraw but only counts lines +// instead of drawing. This is needed to compute the dialog height before +// creating the window. The duplication is intentional — combining them +// into a single function with a "just measure" flag would add branching +// to the draw path and make both harder to read. static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) { int32_t charW = font->charWidth; @@ -517,24 +614,31 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t // ============================================================ // File dialog // ============================================================ +// +// File open/save dialog with directory navigation, file type filtering, +// and overwrite/create confirmation. The dialog is built from standard +// widgets (listbox, text inputs, dropdown, buttons) composed in the +// widget system. Directory entries are prefixed with "[brackets]" in the +// listbox to distinguish them from files, following the DOS convention. +// FD_MAX_PATH is 260 to match DOS MAX_PATH (including null terminator) #define FD_MAX_ENTRIES 512 #define FD_MAX_PATH 260 #define FD_NAME_LEN 64 typedef struct { AppContextT *ctx; - bool done; - bool accepted; - int32_t flags; + bool done; // set true to break modal loop + bool accepted; // true if user clicked OK/Open/Save + int32_t flags; // FD_SAVE, etc. char curDir[FD_MAX_PATH]; - const FileFilterT *filters; + const FileFilterT *filters; // caller-provided filter list int32_t filterCount; - int32_t activeFilter; - char *entryNames[FD_MAX_ENTRIES]; + int32_t activeFilter; // index into filters[] + char *entryNames[FD_MAX_ENTRIES]; // heap-allocated, freed by fdFreeEntries bool entryIsDir[FD_MAX_ENTRIES]; int32_t entryCount; - const char *listItems[FD_MAX_ENTRIES]; + const char *listItems[FD_MAX_ENTRIES]; // pointers into entryNames, for listbox API WidgetT *fileList; WidgetT *pathInput; WidgetT *nameInput; @@ -548,6 +652,11 @@ static FileDialogStateT sFd; // ============================================================ // fdFilterMatch — check if filename matches a glob pattern // ============================================================ +// +// Supports only the most common DOS file filter patterns: "*.*", "*", +// and "*.ext". Full glob matching isn't needed because file dialogs +// historically only use extension-based filters. The case-insensitive +// extension compare handles DOS's case-insensitive filesystem behavior. static bool fdFilterMatch(const char *name, const char *pattern) { if (!pattern || pattern[0] == '\0') { @@ -603,6 +712,12 @@ static void fdFreeEntries(void) { // ============================================================ // fdEntryCompare — sort: dirs first, then alphabetical // ============================================================ +// +// Sort comparator for the indirect sort in fdLoadDir. Uses an index +// array rather than sorting the entryNames/entryIsDir arrays directly +// because qsort would need a struct or the comparator would need to +// move both arrays in sync. Indirect sort with an index array is +// simpler and avoids that coordination problem. static int fdEntryCompare(const void *a, const void *b) { int32_t ia = *(const int32_t *)a; @@ -620,6 +735,13 @@ static int fdEntryCompare(const void *a, const void *b) { // ============================================================ // fdLoadDir — read directory contents into state // ============================================================ +// +// Reads the current directory, applies the active file filter, sorts +// (directories first, then alphabetical), and updates the listbox widget. +// Entry names are strdup'd because dirent buffers are reused by readdir. +// The sort is done via an index array to avoid shuffling two parallel +// arrays; after sorting the index array, the actual arrays are rebuilt +// in sorted order with a single memcpy pass. static void fdLoadDir(void) { fdFreeEntries(); @@ -716,9 +838,14 @@ static void fdLoadDir(void) { // ============================================================ // fdNavigate — change to a new directory // ============================================================ +// +// Handles both absolute and relative paths. Relative paths are resolved +// against curDir. realpath is used to canonicalize the result (resolve +// ".." components, symlinks) so the path display always shows a clean +// absolute path. The stat check ensures we don't try to navigate into +// a file or nonexistent path. static void fdNavigate(const char *path) { - // Resolve relative paths char resolved[FD_MAX_PATH]; if (path[0] == '/' || path[0] == '\\' || @@ -773,6 +900,12 @@ static bool fdValidateFilename(const char *name) { // ============================================================ // fdAcceptFile — confirm and accept the selected filename // ============================================================ +// +// Validates the filename (platform-specific rules), then checks for +// confirmation scenarios: save dialog + existing file = "overwrite?", +// open dialog + missing file = "create?". The nested dvxMessageBox calls +// work because the modal system supports stacking (prevModal is saved and +// restored). Returns false if the user cancelled at any confirmation step. static bool fdAcceptFile(const char *name) { if (!fdValidateFilename(name)) { @@ -829,6 +962,11 @@ static void fdOnListClick(WidgetT *w) { // ============================================================ // fdOnListDblClick — file list double-click // ============================================================ +// +// Double-click on a directory navigates into it. Double-click on a file +// accepts it immediately (equivalent to selecting + clicking OK). The +// bracket stripping (removing "[" and "]") is needed because directory +// names in the list are displayed as "[dirname]" for visual distinction. static void fdOnListDblClick(WidgetT *w) { int32_t sel = wgtListBoxGetSelected(w); @@ -838,7 +976,6 @@ static void fdOnListDblClick(WidgetT *w) { } if (sFd.entryIsDir[sel]) { - // Double-click on directory — navigate into it const char *display = sFd.entryNames[sel]; char dirName[FD_NAME_LEN]; @@ -868,11 +1005,16 @@ static void fdOnListDblClick(WidgetT *w) { // ============================================================ // fdOnOk — OK button clicked // ============================================================ +// +// OK has three behaviors depending on context: +// 1. If a directory is selected in the list, navigate into it +// 2. If the typed filename resolves to a directory, navigate there +// 3. Otherwise, accept the filename (with overwrite/create confirmation) +// This matches Windows file dialog behavior where Enter/OK on a directory +// navigates rather than accepting. static void fdOnOk(WidgetT *w) { (void)w; - - // Check if a directory is selected in the list int32_t sel = wgtListBoxGetSelected(sFd.fileList); if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) { @@ -969,6 +1111,18 @@ static void fdOnPathSubmit(WidgetT *w) { // ============================================================ // dvxFileDialog // ============================================================ +// +// Creates a modal file dialog using the widget system. The layout is: +// - Path input (with Enter-to-navigate) +// - File listbox (single-click selects, double-click opens/accepts) +// - Optional filter dropdown (only shown if filters are provided) +// - Filename input +// - OK/Cancel button row +// +// The filter dropdown uses a static label array because the dropdown +// widget takes const char** and the filter labels need to persist for +// the dialog's lifetime. Static is safe since only one file dialog can +// be active at a time. bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize) { memset(&sFd, 0, sizeof(sFd)); diff --git a/dvx/dvxDialog.h b/dvx/dvxDialog.h index 7aff01d..c392063 100644 --- a/dvx/dvxDialog.h +++ b/dvx/dvxDialog.h @@ -1,11 +1,20 @@ // dvxDialog.h — Modal dialogs for DVX GUI +// +// Provides pre-built modal dialog boxes (message box, file dialog) that +// block the caller and run their own event loop via dvxUpdate() until the +// user dismisses them. Modal dialogs set ctx->modalWindow to prevent input +// from reaching other windows during the dialog's lifetime. +// +// The flag encoding uses separate bit fields for button configuration +// (low nibble) and icon type (high nibble) so they can be OR'd together +// in a single int32_t argument, matching the Win16 MessageBox() convention. #ifndef DVX_DIALOG_H #define DVX_DIALOG_H #include "dvxApp.h" // ============================================================ -// Message box button flags +// Message box button flags (low nibble) // ============================================================ #define MB_OK 0x0000 @@ -15,7 +24,7 @@ #define MB_RETRYCANCEL 0x0004 // ============================================================ -// Message box icon flags +// Message box icon flags (high nibble, OR with button flags) // ============================================================ #define MB_ICONINFO 0x0010 @@ -33,8 +42,10 @@ #define ID_NO 4 #define ID_RETRY 5 -// Show a modal message box and return which button was pressed. -// flags = MB_xxx button flag | MB_ICONxxx icon flag +// Display a modal message box with the specified button and icon combination. +// Blocks the caller by running dvxUpdate() in a loop until a button is +// pressed or the dialog is closed. Returns the ID_xxx value of the button +// that was pressed. The dialog window is automatically destroyed on return. int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags); // ============================================================ @@ -47,17 +58,23 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, // ============================================================ // File dialog filter // ============================================================ +// +// Filters are displayed in a dropdown at the bottom of the file dialog. +// Pattern matching is case-insensitive and supports only single glob +// patterns (no semicolon-separated lists). This keeps the matching code +// trivial for a DOS filesystem where filenames are short and simple. typedef struct { const char *label; // e.g. "Text Files (*.txt)" const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern) } FileFilterT; -// Show a modal file open/save dialog. Returns true if the user selected -// a file, false if cancelled. The selected path is written to outPath -// (buffer must be at least outPathSize bytes). -// initialDir may be NULL (defaults to current directory). -// filters/filterCount may be NULL/0 for no filter dropdown. +// Display a modal file open/save dialog. The dialog shows a directory +// listing with navigation (parent directory, drive letters on DOS), a +// filename text input, and an optional filter dropdown. Blocks the caller +// via dvxUpdate() loop. Returns true if the user selected a file (path +// written to outPath), false if cancelled or closed. initialDir may be +// NULL to start in the current working directory. bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize); #endif // DVX_DIALOG_H diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index 825c658..d0e3f59 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -1,4 +1,53 @@ // dvx_draw.c — Layer 2: Drawing primitives for DVX GUI (optimized) +// +// This is the second layer of the DVX compositor stack, sitting on top +// of dvxVideo (layer 1) and below dvxComp (layer 3). It provides all +// rasterization primitives: filled rects, buffer copies, beveled +// frames, bitmap font text, masked bitmaps (cursors/icons), and +// single-pixel operations. +// +// Every function here draws into the system-RAM backbuffer (d->backBuf), +// never directly to the LFB. The compositor layer is responsible for +// flushing changed regions to the hardware framebuffer via rep movsd. +// This separation means draw operations benefit from CPU cache (the +// backbuffer lives in cacheable system RAM) while LFB writes are +// batched into large sequential bursts. +// +// Performance strategy overview: +// +// The core tension on 486/Pentium is between generality and speed. +// The draw layer resolves this with a two-tier approach: +// +// 1) Span operations (spanFill/spanCopy) are dispatched through +// function pointers in BlitOpsT, set once at init based on bpp. +// The platform implementations use rep stosl/rep movsd inline asm +// for maximum throughput (the 486 executes rep stosl at 1 dword +// per clock after startup; the Pentium pairs it in the U-pipe). +// Using function pointers here costs one indirect call per span +// but avoids a bpp switch in the inner loop of rectFill, which +// would otherwise be a branch per scanline. +// +// 2) Character rendering (drawChar, drawTextN, drawTermRow) uses +// explicit if/else chains on bpp rather than function pointers. +// This is deliberate: the per-pixel work inside glyph rendering +// is a tight bit-test loop where an indirect call per pixel would +// be catastrophic, and the bpp branch is taken once per glyph row +// (hoisted out of the pixel loop). The compiler can also inline +// the pixel store when the bpp is a compile-time constant within +// each branch. +// +// 3) For the most critical glyph paths (unclipped 32bpp and 16bpp), +// the pixel loops are fully unrolled into 8 direct array stores +// with literal bit masks. This eliminates the sGlyphBit[] table +// lookup, the loop counter, and the loop branch — saving ~3 cycles +// per pixel on a 486. The clipped path falls back to the table. +// +// Clip rectangle handling: All draw functions clip against +// d->clipX/Y/W/H (set by setClipRect in layer 1). The clipRect() +// helper is marked static inline so it compiles to straight-line +// compare-and-clamp code at each call site with no function call +// overhead. __builtin_expect hints mark the clipping branches as +// unlikely, helping the branch predictor on Pentium and later. #include "dvxDraw.h" #include "platform/dvxPlatform.h" @@ -13,7 +62,13 @@ char accelParse(const char *text); static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h); static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp); -// Bit lookup tables — avoids per-pixel shift on 486 (40+ cycle savings per shift) +// Bit lookup tables for glyph and mask rendering. On a 486, a variable +// shift (1 << (7 - col)) costs 4 cycles per bit position; a table +// lookup is a fixed 1-cycle load from L1. The 8-entry sGlyphBit table +// maps column index 0..7 to the corresponding bit mask in a 1bpp glyph +// byte (MSB-first, matching standard VGA/bitmap font layout). The +// 16-entry sMaskBit table does the same for 16-pixel-wide cursor/icon +// masks. static const uint8_t sGlyphBit[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01}; static const uint16_t sMaskBit[16] = {0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, 0x0004, 0x0002, 0x0001}; @@ -21,6 +76,13 @@ static const uint16_t sMaskBit[16] = {0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0 // ============================================================ // accelParse // ============================================================ +// +// Scans a menu/button label for the & accelerator marker and returns +// the character after it (lowercased). Follows the Windows/Motif +// convention: "&File" means Alt+F activates it, "&&" is a literal &. +// Returns 0 if no accelerator is found. The result is always +// lowercased so the WM can do a single case-insensitive compare +// against incoming Alt+key events. char accelParse(const char *text) { if (!text) { @@ -68,6 +130,19 @@ char accelParse(const char *text) { // ============================================================ // clipRect // ============================================================ +// +// Intersects a rectangle with the display's current clip rect, +// modifying the rect in place. If the rect is fully outside the +// clip region, w or h will be <= 0 and callers bail out. +// +// Marked static inline because this is called on every rectFill, +// rectCopy, and indirectly on every glyph — it must compile to +// straight-line clamp instructions with zero call overhead. +// __builtin_expect(…, 0) marks clipping as unlikely; in the +// common case windows are fully within the clip rect and all +// four branches fall through untaken. On Pentium this keeps the +// branch predictor happy (static not-taken prediction for forward +// branches), and on 486 it at least avoids the taken-branch penalty. static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h) { int32_t cx2 = d->clipX + d->clipW; @@ -93,6 +168,28 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t * // ============================================================ // drawBevel // ============================================================ +// +// Draws a Motif/DESQview-style beveled rectangular frame. The bevel +// creates the illusion of a raised or sunken 3D surface by drawing +// lighter "highlight" edges on the top and left, and darker "shadow" +// edges on the bottom and right. Swapping highlight and shadow gives +// a sunken appearance (see BEVEL_RAISED/BEVEL_SUNKEN macros in +// dvxTypes.h). +// +// BevelStyleT.width controls the border thickness. DV/X uses 2px +// bevels for most window chrome (matching the original DESQview/X +// and Motif look), 1px for inner borders and scrollbar elements. +// +// The implementation has special-cased fast paths for bw==2 and bw==1 +// that emit exact spans via rectFill rather than looping. This +// matters because drawBevel is called for every window frame, button, +// menu, and scrollbar element on every repaint — the loop overhead +// and extra rectFill calls in the general case add up. Each rectFill +// call already handles clipping internally, so the bevels clip +// correctly even when a window is partially off-screen. +// +// face==0 means "don't fill the interior", which is used for frame-only +// bevels where the content area is painted separately by a callback. void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style) { int32_t bw = style->width; @@ -142,6 +239,39 @@ void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w // ============================================================ // drawChar // ============================================================ +// +// Renders a single fixed-width bitmap font character into the +// backbuffer. Returns the character advance width (always +// font->charWidth) so callers can accumulate cursor position. +// +// Font format: each glyph is charHeight bytes of 1bpp data, MSB-first +// (bit 7 = leftmost pixel). This is the standard VGA/PC BIOS font +// format. We use 8-pixel-wide glyphs exclusively because 8 bits fit +// in one byte per scanline, making the inner loop a single byte load +// plus 8 bit tests — no multi-byte glyph row assembly needed. +// +// The function has six specialized code paths (3 bpp x 2 modes), +// chosen with if/else chains rather than function pointers. On 486 +// and Pentium, an indirect call through a function pointer stalls the +// pipeline (no branch target buffer for indirect calls on 486, and +// a mandatory bubble on Pentium). The if/else chain resolves at the +// outer loop level (once per glyph, not per pixel), so the per-pixel +// inner code is branch-free within each path. +// +// Opaque vs transparent mode: +// opaque=true: Fills the entire character cell (bg then fg). Used +// for normal text where the background must overwrite +// whatever was previously in the cell. +// opaque=false: Only writes foreground pixels; background shows +// through. Used for overlay text on existing content. +// +// The "unclipped fast path" (colStart==0, colEnd==cw) avoids the +// sGlyphBit[] table lookup by testing literal bit masks directly. +// This matters because the table lookup involves an indexed load +// (base + index * element_size), while the literal mask is an +// immediate operand in the compare instruction. At 8 pixels per row +// and 14-16 rows per glyph, saving even 1 cycle per pixel adds up +// across a full screen of text (~6400 characters at 80x80). int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, char ch, uint32_t fg, uint32_t bg, bool opaque) { int32_t cw = font->charWidth; @@ -180,12 +310,18 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 if (x < clipX1) { colStart = clipX1 - x; } if (x + cw > clipX2) { colEnd = clipX2 - x; } - // Unclipped fast path: full 8-pixel character cell with direct bit - // tests eliminates loop overhead and sGlyphBit[] lookups (Item 4) + // Unclipped fast path: when the character cell is fully within the + // clip rect we can skip per-pixel clip checks and use the fully + // unrolled 8-store sequences below. This is the hot path for all + // text that isn't at the edge of a window. bool unclipped = (colStart == 0 && colEnd == cw); if (opaque) { - // Opaque mode: fill entire cell with bg, then overwrite fg pixels + // Opaque mode: every pixel in the cell gets written (fg or bg). + // The unclipped 32bpp and 16bpp paths use branchless ternary + // stores — the compiler emits cmov or conditional-set sequences + // that avoid branch misprediction penalties. Each row is 8 + // direct array stores with no loop, no table lookup. if (unclipped && bpp == 4) { for (int32_t row = rowStart; row < rowEnd; row++) { uint32_t *dst32 = (uint32_t *)(d->backBuf + (y + row) * pitch + x * 4); @@ -218,7 +354,11 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 dst16[7] = (bits & 0x01) ? fg16 : bg16; } } else { - // Clipped path or 8bpp: spanFill bg then overwrite fg + // Clipped path or 8bpp: use spanFill for bg (leveraging + // rep stosl), then iterate visible columns with sGlyphBit[] + // table for fg. 8bpp always takes this path because 8-bit + // stores can't be branchlessly ternary'd as efficiently — + // the compiler can't cmov into a byte store. for (int32_t row = rowStart; row < rowEnd; row++) { int32_t py = y + row; uint8_t *dst = d->backBuf + py * pitch + (x + colStart) * bpp; @@ -256,7 +396,11 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 } } } else { - // Transparent mode: only write foreground pixels + // Transparent mode: only fg pixels are written; bg is untouched. + // The "bits == 0" early-out per row is important here: blank + // rows in the glyph (common in the top/bottom padding of most + // characters) skip all pixel work entirely. In opaque mode + // blank rows still need the bg fill so we can't skip them. if (unclipped && bpp == 4) { for (int32_t row = rowStart; row < rowEnd; row++) { uint8_t bits = glyph[row]; @@ -343,6 +487,21 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // Same idea as drawTermRow but for uniform fg/bg text runs. // Avoids per-character function call overhead, redundant clip // calculation, and spanFill startup costs. +// +// The key optimization over calling drawChar() in a loop is the +// bg fill strategy: in opaque mode, instead of calling spanFill +// once per character cell per row (count * charHeight spanFill +// calls), we fill the entire visible span's background in one +// spanFill per scanline (just charHeight calls total). Then we +// overlay only the fg glyph pixels. For an 80-column line this +// reduces spanFill calls from 80*16=1280 to just 16. Each +// spanFill maps to a single rep stosl, so we're also getting +// better write-combine utilization from the larger sequential +// stores. +// +// Horizontal clipping is done at the character level (firstChar/ +// lastChar) to avoid iterating invisible characters, with per-pixel +// edge clipping only for the partially visible first and last chars. void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque) { if (count <= 0) { @@ -492,6 +651,19 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_ // ============================================================ // drawFocusRect // ============================================================ +// +// Draws a dotted (every-other-pixel) rectangle to indicate keyboard +// focus, matching the Windows/Motif convention. Uses putPixel per +// dot rather than spanFill because the alternating pattern can't be +// expressed as a span fill (which writes uniform color). +// +// The parity calculations on the bottom and right edges ensure the +// dot pattern is visually continuous around corners — the starting +// pixel of each edge is offset so dots don't double up or gap at +// the corner where two edges meet. +// +// This is not performance-critical; focus rects are drawn at most +// once per focused widget per repaint. void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { int32_t bpp = ops->bytesPerPixel; @@ -550,6 +722,10 @@ void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32 // ============================================================ // drawHLine // ============================================================ +// +// Thin convenience wrapper — a horizontal line is just a 1px-tall rect. +// Delegates to rectFill which handles clipping and uses spanFill (rep +// stosl) for the actual write. void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color) { rectFill(d, ops, x, y, w, 1, color); @@ -559,6 +735,26 @@ void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w // ============================================================ // drawInit // ============================================================ +// +// Wires up the BlitOpsT function pointers to the correct +// platform-specific span operations for the active pixel format. +// Called once during startup after videoInit determines the bpp. +// +// The span ops are the only place where function pointers are used +// in the draw layer. This is a deliberate performance tradeoff: +// spanFill and spanCopy are called per-scanline (not per-pixel), +// so the indirect call overhead (~5 cycles on Pentium for the +// mispredicted first call, then predicted afterward) is amortized +// over an entire row of pixels. The alternative — a switch inside +// rectFill's inner loop — would branch every scanline for no gain. +// +// The platform implementations (dvxPlatformDos.c) use inline asm: +// spanFill8/16/32 -> rep stosl (fills 4 bytes per clock) +// spanCopy8/16/32 -> rep movsd (copies 4 bytes per clock) +// These are the fastest bulk memory operations available on 486/ +// Pentium without SSE. The 8-bit and 16-bit variants handle +// alignment preambles to get to dword boundaries, then use +// rep stosl/movsd for the bulk. void drawInit(BlitOpsT *ops, const DisplayT *d) { ops->bytesPerPixel = d->format.bytesPerPixel; @@ -588,6 +784,22 @@ void drawInit(BlitOpsT *ops, const DisplayT *d) { // ============================================================ // drawMaskedBitmap // ============================================================ +// +// Renders a 1-bit masked bitmap (used for mouse cursors and icons). +// The two-plane format mirrors the hardware cursor format used by +// VGA and early SVGA cards: +// +// andMask bit=1, xorData bit=X -> transparent (pixel unchanged) +// andMask bit=0, xorData bit=0 -> bgColor +// andMask bit=0, xorData bit=1 -> fgColor +// +// Each row is a uint16_t (supporting up to 16 pixels wide), stored +// MSB-first. This is sufficient for standard 16x16 mouse cursors. +// +// The colMask optimization pre-computes which bits in each row fall +// within the visible (clipped) columns. For fully transparent rows +// (all visible bits have andMask=1), the entire row is skipped with +// a single bitwise AND + compare — no per-pixel iteration needed. void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor) { int32_t bpp = ops->bytesPerPixel; @@ -669,7 +881,25 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in // lineData points to (ch, attr) pairs. palette is a 16-entry // packed-color table. This avoids per-character function call // overhead, redundant clip calculation, and spanFill startup -// costs that make drawChar expensive when called 80× per row. +// costs that make drawChar expensive when called 80x per row. +// +// This is the primary rendering function for the terminal emulator. +// The attribute byte uses the standard CGA/VGA format: +// bits 0-3: foreground color (0-15) +// bits 4-6: background color (0-7) +// bit 7: blink flag +// +// Unlike drawTextN (which handles uniform fg/bg), every cell here +// can have a different fg/bg pair, so the bg can't be filled in a +// single bulk pass. Instead each cell is rendered individually, +// always in opaque mode (every pixel gets a write). The bpp branch +// is still hoisted outside the per-pixel loop — the outer loop +// selects the bpp path once, then iterates cells within it. +// +// blinkVisible controls the blink phase: when false, fg is replaced +// with bg for characters that have bit 7 set, effectively hiding them. +// cursorCol specifies which cell (if any) should be drawn with +// inverted fg/bg to show the text cursor. void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol) { int32_t cw = font->charWidth; @@ -801,6 +1031,19 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // ============================================================ // drawText // ============================================================ +// +// Renders a null-terminated string by calling drawChar per character. +// Simpler than drawTextN but slower for long runs because each +// drawChar call independently clips, computes row bounds, and +// dispatches on bpp. Used for short labels and ad-hoc text where +// the call overhead doesn't matter; drawTextN is preferred for +// bulk text (editor buffers, list views, etc.). +// +// The left-of-clip skip avoids calling drawChar for characters that +// are entirely to the left of the visible area. The right-of-clip +// early-out breaks the loop as soon as we've passed the right edge. +// These are both marked unlikely (__builtin_expect) because the +// common case is text fully within the clip rect. void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque) { int32_t cw = font->charWidth; @@ -828,6 +1071,16 @@ void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t // ============================================================ // drawTextAccel // ============================================================ +// +// Like drawText but interprets & markers in the string: the character +// following & is drawn with an underline to indicate it's the keyboard +// accelerator (e.g. "&File" draws "File" with F underlined). "&&" +// draws a literal &. This matches the Windows/Motif convention for +// menu and button labels. +// +// The underline is drawn as a 1px horizontal line at the bottom of +// the character cell (y + charHeight - 1), which is the standard +// placement for accelerator underlines. void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque) { int32_t cw = font->charWidth; @@ -880,6 +1133,17 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in // ============================================================ // drawVLine // ============================================================ +// +// Draws a vertical line pixel-by-pixel. Unlike drawHLine (which +// delegates to rectFill → spanFill for a single-row span), a +// vertical line can't use spanFill because each pixel is on a +// different scanline. Instead we advance by d->pitch per pixel +// and write directly, branching on bpp once at the top. +// +// The ops parameter is unused (suppressed with (void)ops) because +// spanFill operates on contiguous horizontal runs and is useless +// for vertical lines. We keep the parameter for API consistency +// with the rest of the draw layer. void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color) { (void)ops; @@ -923,6 +1187,12 @@ void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h // ============================================================ // putPixel // ============================================================ +// +// Writes a single pixel at an already-computed buffer address. +// Only used by drawFocusRect for its alternating dot pattern. +// Marked static inline so it compiles to a direct store at the +// call site with no function call overhead. The bpp chain here +// is acceptable because focus rect drawing is infrequent. static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) { if (bpp == 2) { @@ -938,6 +1208,23 @@ static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) { // ============================================================ // rectCopy // ============================================================ +// +// Copies a rectangular region from an arbitrary source buffer into +// the display backbuffer. Used by the compositor to blit per-window +// content buffers (win->contentBuf) into the shared backbuffer during +// the composite pass. +// +// Clipping adjusts both the destination and source positions by the +// same delta so the visible portion maps to the correct source pixels. +// When the source and destination pitches match and equal the row byte +// count, the entire block is copied in a single memcpy (which the +// compiler/libc can optimize to rep movsd). Otherwise it falls back +// to per-row memcpy. +// +// This function does NOT handle overlapping source and destination +// regions (no memmove). That's fine because the source is always a +// per-window content buffer and the destination is the shared +// backbuffer — they never overlap. void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, const uint8_t *srcBuf, int32_t srcPitch, int32_t srcX, int32_t srcY, int32_t w, int32_t h) { int32_t bpp = ops->bytesPerPixel; @@ -977,6 +1264,18 @@ void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, cons // ============================================================ // rectFill // ============================================================ +// +// The workhorse fill primitive. Clips to the display clip rect, +// then fills one scanline at a time via the spanFill function +// pointer (which routes to rep stosl on DOS). This is the most +// frequently called function in the draw layer — it backs rectFill +// directly, plus drawHLine, drawBevel interior fills, and the bg +// fill in opaque text rendering. +// +// The clip + early-out pattern (clipRect then check w/h <= 0) is +// the same in every draw function. The __builtin_expect marks the +// zero-size case as unlikely to avoid a taken-branch penalty in the +// common case where the rect is visible after clipping. void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { clipRect(d, &x, &y, &w, &h); @@ -998,6 +1297,12 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, // ============================================================ // textWidth // ============================================================ +// +// Returns the pixel width of a null-terminated string. Because all +// fonts are fixed-width, this is just strlen * charWidth — but we +// iterate manually rather than calling strlen to avoid a second pass +// over the string. This is used heavily for layout calculations +// (centering text in buttons, sizing menu popups, etc.). int32_t textWidth(const BitmapFontT *font, const char *text) { int32_t w = 0; @@ -1014,6 +1319,12 @@ int32_t textWidth(const BitmapFontT *font, const char *text) { // ============================================================ // textWidthAccel // ============================================================ +// +// Like textWidth but accounts for & accelerator markers: a single & +// is not rendered (it just marks the next character as the accelerator), +// so it doesn't contribute to width. "&&" renders as one "&" character. +// Used to compute the correct pixel width for menu items and button +// labels that contain accelerator markers. int32_t textWidthAccel(const BitmapFontT *font, const char *text) { int32_t w = 0; diff --git a/dvx/dvxDraw.h b/dvx/dvxDraw.h index 9560565..e39b44f 100644 --- a/dvx/dvxDraw.h +++ b/dvx/dvxDraw.h @@ -1,61 +1,95 @@ // dvx_draw.h — Layer 2: Drawing primitives for DVX GUI +// +// Provides all 2D drawing operations: rectangle fills, bitmap blits, text +// rendering, bevels, lines, and cursor/icon rendering. Every function in +// this layer draws into the display's backbuffer and clips to the display's +// current clip rectangle — no direct LFB writes happen here. +// +// This layer is deliberately stateless beyond the clip rect on DisplayT. +// All drawing context (colors, fonts, bevel styles) is passed explicitly +// to each function. This means the draw layer can be used by any caller +// (compositor, WM, widgets) without worrying about shared mutable state. +// +// Performance strategy: the hot inner loops (span fill, span copy) are +// dispatched through BlitOpsT function pointers that resolve to asm on +// DOS. The text rendering functions (drawTextN, drawTermRow) are optimized +// to batch operations per scanline rather than per glyph, which matters +// when rendering 80-column terminal screens on a 486. #ifndef DVX_DRAW_H #define DVX_DRAW_H #include "dvxTypes.h" -// Initialize blit operations based on pixel format +// Populate a BlitOpsT with the correct span functions for the display's +// pixel depth. Must be called once after videoInit(). void drawInit(BlitOpsT *ops, const DisplayT *d); -// Solid color rectangle fill (clips to display clip rect) +// Fill a rectangle with a solid color. Clips to the display clip rect. +// This is the workhorse for backgrounds, window fills, and clear operations. void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); -// Copy rectangle from source buffer to backbuffer (clips to display clip rect) +// Copy a rectangle from an arbitrary source buffer into the backbuffer. +// Used to blit per-window content buffers during compositing. srcX/srcY +// specify the origin within the source buffer, allowing sub-rectangle blits. void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, const uint8_t *srcBuf, int32_t srcPitch, int32_t srcX, int32_t srcY, int32_t w, int32_t h); -// Draw a beveled frame +// Draw a beveled frame. The bevel is drawn as overlapping horizontal and +// vertical spans — top/left in highlight color, bottom/right in shadow. +// The face color fills the interior if non-zero. void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const BevelStyleT *style); -// Draw a single character, returns advance width +// Draw a single character glyph. When opaque is true, the background color +// fills the entire cell; when false, only foreground pixels are drawn +// (transparent background). Returns the advance width (always charWidth). int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, char ch, uint32_t fg, uint32_t bg, bool opaque); -// Draw a null-terminated string +// Draw a null-terminated string. Calls drawChar per character. void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque); -// Draw exactly 'count' characters from a buffer (not null-terminated). -// Much faster than calling drawChar per character: computes clip once, -// fills background in bulk, then overlays glyph foreground pixels. +// Optimized batch text rendering for a known character count. Computes +// clip bounds once for the entire run, fills the background in a single +// rectFill, then overlays glyph foreground pixels. Significantly faster +// than drawChar-per-character for long runs (e.g. terminal rows, list items). void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque); -// Measure text width in pixels +// Returns the pixel width of a null-terminated string (count * charWidth). int32_t textWidth(const BitmapFontT *font, const char *text); -// Parse accelerator key from text with & markers (e.g. "E&xit" -> 'x') -// Returns the lowercase accelerator character, or 0 if none found. +// Scan text for an & prefix and return the following character as a +// lowercase accelerator key. "&File" -> 'f', "E&xit" -> 'x'. +// Used by menu and button construction to extract keyboard shortcuts. char accelParse(const char *text); -// Draw text with & accelerator processing (underlines the accelerator character) +// Draw text with & accelerator markers. The character after & is drawn +// with an underline to indicate the keyboard shortcut. && produces a +// literal &. Used for menu items and button labels. void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque); -// Measure text width in pixels, skipping & markers +// Measure text width excluding & markers (so "&File" measures as 4 chars). int32_t textWidthAccel(const BitmapFontT *font, const char *text); -// Draw a 1-bit bitmap with mask (for cursors, icons) -// andMask/xorData are arrays of uint16_t, one per row +// Draw a 1-bit AND/XOR masked bitmap. Used for software-rendered mouse +// cursors. See CursorT for the mask encoding conventions. void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor); -// Draw a row of terminal character cells (ch/attr pairs with a 16-color palette). -// Renders 'cols' cells starting at (x,y). Much faster than calling drawChar per cell. -// cursorCol: column index to draw inverted (cursor), or -1 for no cursor. +// Render an entire row of terminal character cells (ch/attr byte pairs) +// in a single pass. Each cell's foreground and background colors are +// looked up from a 16-color palette. Attribute bit 7 controls blink: +// when blinkVisible is false, blinking characters are hidden. cursorCol +// specifies which column to draw inverted as the text cursor (-1 for none). +// This function exists because rendering an 80-column terminal row +// character-by-character would be unacceptably slow on target hardware. void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol); -// Dotted focus rectangle (every other pixel) +// Draw a 1px dotted rectangle (alternating pixels). Used for keyboard +// focus indicators on buttons and other focusable widgets, matching the +// Windows 3.x focus rectangle convention. void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); -// Horizontal line +// Horizontal line (1px tall rectangle fill, but with simpler clipping). void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); -// Vertical line +// Vertical line (1px wide, drawn pixel-by-pixel down each scanline). void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color); #endif // DVX_DRAW_H diff --git a/dvx/dvxFont.h b/dvx/dvxFont.h index 748d0a9..e59f1e7 100644 --- a/dvx/dvxFont.h +++ b/dvx/dvxFont.h @@ -1,4 +1,28 @@ // dvx_font.h — Embedded VGA bitmap font data (CP437) for DVX GUI +// +// Contains the raw glyph bitmaps for two standard VGA ROM fonts (8x14 and +// 8x16) covering the full IBM Code Page 437 character set (256 glyphs). +// These are byte-for-byte copies of the fonts stored in the VGA BIOS ROM. +// +// Embedding them as static const arrays (rather than reading from the VGA +// ROM at INT 10h) serves two purposes: +// 1. The data is available on non-DOS platforms (Linux/SDL) where no +// VGA BIOS exists. +// 2. On DOS, reading the VGA ROM font requires a real-mode interrupt +// call during init, and the data would need to be copied somewhere +// anyway. Compiling it in is simpler and more portable. +// +// The glyph format is 1 bit per pixel, 8 pixels wide, with MSB = leftmost +// pixel. Each glyph occupies charHeight consecutive bytes (14 or 16). +// The drawing code in dvxDraw.c processes one byte per scanline, testing +// each bit to decide foreground vs. background — the 8-pixel width means +// no bit-shifting across byte boundaries is ever needed. +// +// CP437 includes the full ASCII printable range (32-126) plus box-drawing +// characters (176-223), which are essential for the Motif/DV/X look +// (scrollbar arrows, window close gadget, etc.), as well as international +// characters, math symbols, and the classic smiley/card suit characters +// in positions 1-31. #ifndef DVX_FONT_H #define DVX_FONT_H @@ -1051,6 +1075,13 @@ static const uint8_t font8x16[256 * 16] = { // ============================================================ // Font descriptor structs // ============================================================ +// +// Pre-built BitmapFontT instances ready to pass to dvxInit() or use +// directly with draw functions. The 8x14 font fits more rows on screen +// (34 rows at 480 pixels) while 8x16 matches the standard VGA text mode +// cell height (30 rows at 480). DVX defaults to 8x14 for maximum usable +// content area, but 8x16 is available for applications that want the +// classic DOS text mode look. static const BitmapFontT dvxFont8x14 = { .charWidth = 8, diff --git a/dvx/dvxIcon.c b/dvx/dvxIcon.c index 114dca8..7428a1e 100644 --- a/dvx/dvxIcon.c +++ b/dvx/dvxIcon.c @@ -1,4 +1,22 @@ // dvxIcon.c — stb_image implementation for DVX GUI +// +// This file exists solely to instantiate the stb_image implementation. +// stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION +// in exactly one .c file to generate the actual function bodies. Putting +// this in its own translation unit keeps the stb code isolated so it: +// 1. Compiles once, not in every file that #includes stb_image.h +// 2. Can have its own warning suppressions without affecting project code +// 3. Doesn't slow down incremental rebuilds (only recompiles if stb changes) +// +// stb_image was chosen over libpng/libjpeg because: +// - Single header, no external dependencies — critical for DJGPP cross-compile +// - Supports BMP, PNG, JPEG, GIF with one include +// - Public domain license, no linking restrictions +// - Small code footprint suitable for DOS targets +// +// STBI_ONLY_* defines strip out support for unused formats (TGA, PSD, HDR, +// PIC, PNM) to reduce binary size. STBI_NO_SIMD is required because DJGPP +// targets 486/Pentium which lack SSE; on the Linux/SDL build it's harmless. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" diff --git a/dvx/dvxImageWrite.c b/dvx/dvxImageWrite.c index 15cb28a..4403ecb 100644 --- a/dvx/dvxImageWrite.c +++ b/dvx/dvxImageWrite.c @@ -1,4 +1,14 @@ // dvxImageWrite.c — stb_image_write implementation for DVX GUI +// +// Companion to dvxIcon.c: instantiates stb_image_write for PNG output +// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as +// dvxIcon.c for using stb — zero external dependencies, single header, +// public domain. Kept in a separate translation unit from the read side +// so projects that don't need screenshot support can omit this file and +// save the code size. +// +// STBI_WRITE_NO_SIMD disables SSE codepaths for the same reason as +// STBI_NO_SIMD in dvxIcon.c: the DOS target lacks SSE support. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-function" diff --git a/dvx/dvxPalette.h b/dvx/dvxPalette.h index e82b4d1..d823c4b 100644 --- a/dvx/dvxPalette.h +++ b/dvx/dvxPalette.h @@ -1,4 +1,20 @@ // dvx_palette.h — 8-bit mode palette definition for DVX GUI +// +// Defines the 256-color palette used in 8-bit (VGA Mode 13h / VESA 8bpp) +// video modes. The palette layout follows the same strategy as the X11 +// default colormap and Netscape's web-safe colors: +// +// 0-215: 6x6x6 color cube — 6 levels per channel (0, 51, 102, 153, +// 204, 255), giving 216 uniformly distributed colors. The index +// formula (r*36 + g*6 + b) enables O(1) color lookup without +// searching. +// 216-231: 16-step grey ramp for smooth gradients in window chrome. +// 232-239: Dedicated UI chrome colors (highlight, shadow, active title, +// etc.) that need exact values not available in the color cube. +// 240-255: Reserved for future use (currently black). +// +// This partition gives good general-purpose color reproduction while +// guaranteeing the UI has pixel-perfect colors for its chrome elements. #ifndef DVX_PALETTE_H #define DVX_PALETTE_H @@ -95,10 +111,18 @@ static inline void dvxGeneratePalette(uint8_t *pal) } } -// Find nearest palette entry for an RGB color (for 8-bit mode) +// Find the nearest palette entry for an RGB color using minimum Euclidean +// distance in RGB space. The two-phase approach avoids a full 256-entry +// linear scan in the common case: +// 1. Fast path: snap to the nearest 6x6x6 color cube entry via integer +// division (O(1), no branching). +// 2. Refinement: linearly scan only the 24 grey ramp and chrome entries +// (indices 216-239) to see if any is closer than the cube match. +// Entries 240-255 are reserved (black) and skipped to avoid false matches. +// This is called by packColor() in 8-bit mode, so it needs to be fast. static inline uint8_t dvxNearestPalEntry(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b) { - // Try the color cube first — fast path + // Snap to nearest cube vertex: +25 rounds to nearest 51-step level int32_t ri = (r + 25) / 51; int32_t gi = (g + 25) / 51; int32_t bi = (b + 25) / 51; diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 619eb74..f75c2ae 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -1,4 +1,10 @@ // dvx_types.h — Shared type definitions for DVX GUI +// +// Central type definitions shared across all five layers of the DVX GUI +// stack (video, draw, comp, wm, app). Every header includes this file, +// so it must remain free of implementation details and function definitions. +// The design uses a single-include-tree rooted here to avoid circular +// dependencies between layers. #ifndef DVX_TYPES_H #define DVX_TYPES_H @@ -8,6 +14,14 @@ // ============================================================ // Pixel format descriptor // ============================================================ +// +// Describes the pixel encoding for the active VESA video mode. +// Populated once at startup from the VBE mode info block and then +// treated as read-only. Storing shift/mask/bits separately avoids +// repeated bit-scanning when packing colors — packColor() uses these +// fields directly for shift-and-mask arithmetic rather than computing +// them on the fly, which matters when called per-glyph during text +// rendering on a 486. typedef struct { int32_t bitsPerPixel; // 8, 15, 16, or 32 @@ -26,6 +40,25 @@ typedef struct { // ============================================================ // Display context // ============================================================ +// +// The single display context passed by pointer through every layer. +// Using one struct instead of globals makes the entire stack reentrant +// in principle and simplifies testing under Linux (where lfb points to +// an SDL surface instead of real VESA memory). +// +// The double-buffer strategy (backBuf in system RAM, lfb is the real +// framebuffer) exists because writes to video memory over the PCI bus +// are dramatically slower than writes to main RAM on 486/Pentium hardware +// — often 10-50x slower for random-access patterns. All drawing goes to +// backBuf, and only dirty rectangles are flushed to lfb via fast aligned +// copies (rep movsd). This is the single most important performance +// decision in the entire compositor. +// +// The clip rectangle is mutable state set before each draw call so that +// the drawing layer doesn't need per-window context — it just clips to +// whatever rectangle is currently set on the display. This avoids an +// extra parameter on every draw function and mirrors how classic windowing +// systems (X11, GDI) handle clipping. typedef struct { int32_t width; @@ -44,6 +77,11 @@ typedef struct { // ============================================================ // Rectangle // ============================================================ +// +// x/y/w/h representation chosen over x1/y1/x2/y2 because most operations +// (clipping, blit sizing, layout) naturally work with origin + extent. +// The compositor, window manager, and widget layout engine all traffic +// in RectT, so keeping one canonical representation avoids conversion bugs. typedef struct { int32_t x; @@ -55,6 +93,19 @@ typedef struct { // ============================================================ // Span operation function pointers // ============================================================ +// +// BlitOpsT is a vtable for the two hot-path memory operations: filling +// a horizontal span with a solid color, and copying a span from one +// buffer to another. The function pointers are resolved once at init +// time based on the active pixel depth (8/16/32 bpp), and on DOS they +// dispatch to hand-written asm inner loops using rep stosl / rep movsd. +// Every rectangle fill, blit, and text draw ultimately bottlenecks +// through these two functions, so even a small speedup here multiplies +// across the entire frame. +// +// Storing bytesPerPixel and pitch alongside the function pointers keeps +// the hot data in one cache line and avoids re-dereferencing the DisplayT +// on every scanline of a fill or copy. typedef void (*SpanFillFnT)(uint8_t *dst, uint32_t color, int32_t count); typedef void (*SpanCopyFnT)(uint8_t *dst, const uint8_t *src, int32_t count); @@ -69,6 +120,16 @@ typedef struct { // ============================================================ // Bevel style // ============================================================ +// +// Bevels are the defining visual element of the Motif/DESQview/X aesthetic. +// Swapping highlight and shadow colors flips between raised and sunken +// appearance — the BEVEL_RAISED and BEVEL_SUNKEN macros encode this +// convention so callers don't get the colors backwards. +// +// A width of 2 pixels is the standard Motif bevel size. Using 1 creates +// a thinner look for scrollbar troughs and other subtle borders. +// face = 0 means "don't fill the interior", which is used when drawing +// a bevel frame around content that's already been painted. typedef struct { uint32_t highlight; // lighter color (top/left edges) @@ -85,6 +146,18 @@ typedef struct { // ============================================================ // Bitmap font // ============================================================ +// +// Fixed-width 8-pixel-wide bitmap fonts only. This simplifies text +// rendering enormously: character positions are pure multiplication +// (x = col * 8), glyph lookup is a single array index, and the 1bpp +// format means each scanline of a glyph is exactly one byte. +// +// Two font sizes are provided (8x14 and 8x16), matching the standard +// VGA ROM fonts and CP437 encoding. Proportional fonts would require +// glyph-width tables, kerning, and per-character positioning — none of +// which is worth the complexity for a DOS-era window manager targeting +// 640x480 screens. The 8-pixel width also aligns nicely with byte +// boundaries, enabling per-scanline glyph rendering without bit shifting. #define FONT_CHAR_WIDTH 8 // fixed glyph width in pixels @@ -99,6 +172,18 @@ typedef struct { // ============================================================ // Color scheme // ============================================================ +// +// All UI colors are pre-packed into display pixel format at init time. +// This means every color here is a uint32_t that can be written directly +// to the framebuffer without any per-pixel conversion. The tradeoff is +// that the scheme must be regenerated if the video mode changes, but +// since mode changes require re-init anyway, this is free in practice. +// +// The color set mirrors classic Motif/Windows 3.x conventions: +// windowHighlight/windowShadow form bevel pairs, activeTitleBg gives +// the focused window's title bar its distinctive color, and so on. +// Keeping all colors in one struct makes theme support trivial — just +// swap the struct. typedef struct { uint32_t desktop; @@ -124,6 +209,13 @@ typedef struct { // ============================================================ // Dirty rectangle list // ============================================================ +// +// Fixed-size array rather than a linked list because: (a) allocation +// in a DOS protected-mode environment should be predictable, (b) 128 +// rects is more than enough for any realistic frame (window moves, +// menu opens, etc.), and (c) linear scanning for merge candidates is +// cache-friendly at this size. If the list fills up, the compositor +// can merge aggressively or fall back to a full-screen repaint. #define MAX_DIRTY_RECTS 128 @@ -135,6 +227,17 @@ typedef struct { // ============================================================ // Window chrome constants // ============================================================ +// +// These define the pixel geometry of the window frame (border, title bar, +// menu bar). They're compile-time constants because the DV/X look demands +// a fixed, uniform chrome thickness across all windows — there's no +// per-window customization of frame geometry. +// +// CHROME_TOTAL_TOP/SIDE/BOTTOM are the total chrome insets from the +// outer frame rectangle to the content area. The content area position +// is computed from these constants, so they must be kept in sync with +// the drawing code in wmDrawChrome(). Menu bar height is added +// dynamically only for windows that have menus. #define CHROME_BORDER_WIDTH 4 #define CHROME_TITLE_HEIGHT 20 @@ -149,6 +252,16 @@ typedef struct { // ============================================================ // Menu types // ============================================================ +// +// Fixed-size arrays with inline char buffers rather than heap-allocated +// strings. This avoids malloc/free churn for menus that are typically +// built once at window creation and never change. The limits (16 items +// per menu, 8 menus per bar, 32-char labels) are generous for a DOS +// application; exceeding them is silently capped. +// +// Cascading submenus are supported by the subMenu pointer on MenuItemT. +// The forward declaration of MenuT is needed because MenuItemT contains +// a pointer to its parent type (circular reference between item and menu). #define MAX_MENU_ITEMS 16 #define MAX_MENUS 8 @@ -174,6 +287,8 @@ typedef struct { MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item) } MenuItemT; +// MenuT is a named struct (not anonymous typedef) because MenuItemT +// needs a forward pointer to it for cascading submenus. struct MenuT { char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File") MenuItemT items[MAX_MENU_ITEMS]; @@ -183,6 +298,9 @@ struct MenuT { char accelKey; // lowercase accelerator character, 0 if none }; +// positionsDirty defers the O(n) barX/barW recomputation until the +// menu bar is actually drawn, avoiding redundant work when multiple +// menus are added in sequence during window setup. typedef struct { MenuT menus[MAX_MENUS]; int32_t menuCount; @@ -192,6 +310,14 @@ typedef struct { // ============================================================ // Scrollbar types // ============================================================ +// +// Window-level scrollbars (separate from the widget-internal scrollbar +// drawing code). These are attached to WindowT and managed by the WM +// layer. The thumb size is computed proportionally from pageSize relative +// to the total range (max - min), matching the Windows/Motif convention. +// +// x/y/length are computed by wmUpdateContentRect() and cached here so +// hit-testing and drawing don't need to recompute them each frame. typedef enum { ScrollbarVerticalE, @@ -214,6 +340,16 @@ typedef struct { // ============================================================ // Accelerator table (global hotkeys) // ============================================================ +// +// Per-window accelerator tables for keyboard shortcuts (e.g. Ctrl+S for +// save). The modifier flags intentionally match the BIOS INT 16h shift +// state bits so that the raw keyboard data can be compared directly +// without translation. +// +// normKey and normMods are precomputed at registration time: the key is +// uppercased and shift is stripped from modifiers. This lets the runtime +// match in O(n) with just two integer comparisons per entry, avoiding +// case-folding and modifier normalization on every keystroke. #define MAX_ACCEL_ENTRIES 32 @@ -222,7 +358,8 @@ typedef struct { #define ACCEL_CTRL 0x04 #define ACCEL_ALT 0x08 -// Key codes for non-ASCII keys (scancode | 0x100) +// Non-ASCII keys are encoded as (scancode | 0x100) to distinguish them +// from regular ASCII values in a single int32_t keycode space. #define KEY_F1 (0x3B | 0x100) #define KEY_F2 (0x3C | 0x100) #define KEY_F3 (0x3D | 0x100) @@ -258,6 +395,20 @@ typedef struct { // ============================================================ // Window // ============================================================ +// +// WindowT is the central object of the window manager. Each window owns +// its own content backbuffer, which persists across frames so that +// applications don't need to repaint on every expose event — only when +// their actual content changes. This is crucial for performance on slow +// hardware: dragging a window across others doesn't force every obscured +// window to repaint. +// +// The content area (contentX/Y/W/H) is computed from the frame dimensions +// minus chrome. This separation lets the compositor blit chrome and content +// independently during dirty-rect repainting. +// +// RESIZE_xxx flags are bitfields so that corner resizes can combine two +// edges (e.g. RESIZE_LEFT | RESIZE_TOP for the top-left corner). #define MAX_WINDOWS 64 #define MAX_TITLE_LEN 128 @@ -271,6 +422,8 @@ typedef struct { #define MIN_WINDOW_W 80 #define MIN_WINDOW_H 60 +// Minimized windows display as icons at the bottom of the screen, +// similar to DESQview/X's original icon bar behavior. #define ICON_SIZE 64 #define ICON_BORDER 2 #define ICON_TOTAL_SIZE (ICON_SIZE + ICON_BORDER * 2) @@ -297,6 +450,8 @@ typedef struct WindowT { bool contentDirty; // true when contentBuf has changed since last icon refresh int32_t maxW; // maximum width (-1 = screen width) int32_t maxH; // maximum height (-1 = screen height) + // Pre-maximize geometry is saved so wmRestore() can put the window + // back exactly where it was. This matches Windows 3.x behavior. int32_t preMaxX; // saved position before maximize int32_t preMaxY; int32_t preMaxW; @@ -328,7 +483,11 @@ typedef struct WindowT { // Accelerator table (NULL if none, caller owns allocation) AccelTableT *accelTable; - // Callbacks + // Callbacks — the application's interface to the window manager. + // Using function pointers rather than a message queue keeps latency + // low (no queuing/dispatching overhead) and matches the callback-driven + // model that DOS programs expect. The widget system installs its own + // handlers here via wgtInitWindow() and dispatches to individual widgets. void *userData; void (*onPaint)(struct WindowT *win, RectT *dirtyArea); void (*onKey)(struct WindowT *win, int32_t key, int32_t mod); @@ -342,6 +501,19 @@ typedef struct WindowT { // ============================================================ // Window stack // ============================================================ +// +// The window stack is an array of pointers ordered front-to-back: index +// count-1 is the topmost (frontmost) window. This ordering means the +// compositor can iterate back-to-front for painting (painter's algorithm) +// and front-to-back for hit testing (first hit wins). +// +// Using an array of pointers (not embedded structs) lets us reorder +// windows by swapping pointers instead of copying entire WindowT structs, +// which matters because WindowT is large. +// +// Drag/resize/scroll state lives here rather than on individual windows +// because only one interaction can be active at a time system-wide — +// you can't drag two windows simultaneously with a single mouse. typedef struct { WindowT *windows[MAX_WINDOWS]; @@ -360,6 +532,17 @@ typedef struct { // ============================================================ // Mouse cursor // ============================================================ +// +// Software-rendered cursor using the classic AND/XOR mask approach from +// the original IBM VGA hardware cursor spec. This two-mask scheme enables +// four pixel states: transparent (AND=1, XOR=0), inverted (AND=1, XOR=1), +// black (AND=0, XOR=0), and white (AND=0, XOR=1). The cursor is drawn +// in software on top of the composited frame because VESA VBE doesn't +// provide a hardware sprite — the cursor is painted into the backbuffer +// and the affected region is flushed to the LFB each frame. +// +// 16 pixels wide using uint16_t rows, one word per scanline. This keeps +// cursor data compact and aligned, and 16x16 is the standard DOS cursor size. typedef struct { int32_t width; @@ -373,6 +556,17 @@ typedef struct { // ============================================================ // Popup state for dropdown menus (with cascading submenu support) // ============================================================ +// +// Only one popup chain can be active at a time (menus are modal). +// Cascading submenus are tracked as a stack of PopupLevelT frames, +// where each level saves the parent menu's state so it can be restored +// when the submenu closes. MAX_SUBMENU_DEPTH of 4 allows File > Recent > +// Category > Item style nesting, which is deeper than any sane DOS UI needs. +// +// The popup's screen coordinates are stored directly rather than computed +// relative to the window, because the popup is painted on top of +// everything during the compositor's overlay pass and needs absolute +// screen positioning. #define MAX_SUBMENU_DEPTH 4 @@ -405,6 +599,12 @@ typedef struct { // ============================================================ // System menu (control menu / close box menu) // ============================================================ +// +// The system menu appears when clicking the close gadget once (double-click +// closes). This matches classic Windows 3.x/Motif behavior where the control +// menu provides Move, Size, Minimize, Maximize, Restore, and Close. The +// menu items are dynamically enabled/disabled based on window state (e.g. +// Restore is disabled if the window isn't maximized). typedef enum { SysMenuRestoreE = 1, @@ -440,6 +640,12 @@ typedef struct { // ============================================================ // Keyboard move/resize mode // ============================================================ +// +// Supports moving and resizing windows via arrow keys (triggered from +// the system menu's Move/Size commands). The original geometry is saved +// so that Escape can cancel the operation and restore the window to its +// prior position/size. This is the standard Windows 3.x keyboard +// accessibility mechanism for window management. typedef enum { KbModeNoneE = 0, diff --git a/dvx/dvxVideo.c b/dvx/dvxVideo.c index 5fdceb5..409b33a 100644 --- a/dvx/dvxVideo.c +++ b/dvx/dvxVideo.c @@ -3,6 +3,43 @@ // Platform-independent video utilities. The actual VESA/VBE code // now lives in dvxPlatformDos.c (or the platform file for whatever // OS we're targeting). +// +// This is the lowest layer of the 5-layer DVX compositor stack +// (video -> draw -> comp -> wm -> app). It owns the DisplayT context +// that threads through every layer above: screen dimensions, pixel +// format, the backbuffer pointer, and the current clip rectangle. +// +// Design decisions at this layer: +// +// LFB-only (no bank switching): VBE 2.0 LFB gives the CPU a flat +// address window over the entire framebuffer. Bank switching (the +// VBE 1.x fallback) requires an INT 10h or port-I/O call every 64 KB, +// which is lethal to throughput in a compositor that redraws hundreds +// of dirty rects per frame. Requiring LFB eliminates per-scanline +// bank math, allows rep movsd bulk copies, and simplifies every span +// operation in the draw layer. The tradeoff is dropping cards that +// only expose banked modes — acceptable since the target is 486+ with +// a VESA 2.0 BIOS (virtually universal by 1994-95). +// +// System-RAM backbuffer with dirty-rect flushing: Drawing directly +// to the LFB is slow because VESA framebuffers sit behind the PCI/ISA +// bus and every write-combine flush costs dozens of cycles. Reads from +// LFB are catastrophically slow (uncached MMIO). Instead, all draw +// operations target a malloc'd backbuffer in system RAM, which lives +// in the CPU's L1/L2 cache hierarchy. The compositor (layer 3) then +// copies only the dirty rectangles to the LFB with rep movsd, making +// the bus transfer a single sequential burst per rect. This also +// means overdraw from occluded windows never touches the bus at all. +// +// Clip rectangle on DisplayT: Rather than passing a clip rect to +// every draw call (4 extra parameters, extra register pressure on +// i386 calling convention where only a few args go in registers), the +// clip rect is set once on DisplayT before a batch of draw calls. The +// window manager sets it to the current window's content area before +// calling onPaint, and the compositor sets it to each dirty rect during +// the composite pass. This keeps the draw API narrow and the common +// case (many draws within the same clip) free of redundant parameter +// shuffling. #include "dvxVideo.h" #include "platform/dvxPlatform.h" @@ -14,12 +51,30 @@ // ============================================================ // packColor // ============================================================ +// +// Converts an 8-bit-per-channel RGB triple into whatever pixel +// representation the current display mode uses. The result is +// always returned as a uint32_t regardless of actual pixel width, +// so callers can store it and pass it through the draw layer without +// caring about the active format. +// +// For 8-bit indexed mode, this falls through to dvxNearestPalEntry +// which searches the 6x6x6 color cube, grey ramp, and chrome slots. +// For direct-color modes (15/16/32-bit), we right-shift each channel +// to discard the low bits that don't fit in the mode's color field, +// then left-shift into position. This approach truncates rather than +// rounds, which is a deliberate simplicity tradeoff — dithering or +// rounding would add per-pixel branches on a 486 for minimal visual +// benefit in a desktop GUI. uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) { if (d->format.bitsPerPixel == 8) { return dvxNearestPalEntry(d->palette, r, g, b); } + // Shift each channel: first discard the low bits that won't fit in + // the mode's field width (e.g. 8-bit red -> 5-bit red for 565), + // then shift left to the field's position within the pixel word. uint32_t rv = ((uint32_t)r >> (8 - d->format.redBits)) << d->format.redShift; uint32_t gv = ((uint32_t)g >> (8 - d->format.greenBits)) << d->format.greenShift; uint32_t bv = ((uint32_t)b >> (8 - d->format.blueBits)) << d->format.blueShift; @@ -31,6 +86,9 @@ uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) { // ============================================================ // resetClipRect // ============================================================ +// +// Restores the clip rect to the full screen. Called after the +// compositor finishes flushing dirty rects, and during init. void resetClipRect(DisplayT *d) { d->clipX = 0; @@ -43,6 +101,19 @@ void resetClipRect(DisplayT *d) { // ============================================================ // setClipRect // ============================================================ +// +// Sets the active clip rectangle, clamping to screen bounds. +// The clip rect is stored directly on DisplayT so every draw call +// in layer 2 can read it without an extra parameter. This is the +// central mechanism that makes per-window and per-dirty-rect +// clipping cheap: the WM sets clip to the window's content area +// before calling onPaint, and the compositor narrows it further +// to each dirty rect during the composite pass. +// +// Coordinates are converted to min/max form internally, clamped, +// then stored back as x/y/w/h. If the caller passes a rect that +// lies entirely outside the screen, clipW or clipH will be zero +// and all subsequent draw calls will trivially reject. void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) { int32_t x2 = x + w; @@ -63,6 +134,13 @@ void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) { // ============================================================ // videoInit // ============================================================ +// +// Thin wrapper that delegates to the platform layer. All the heavy +// lifting — VBE enumeration, mode scoring, LFB DPMI mapping, +// backbuffer allocation — lives in dvxPlatformDos.c so this file +// stays portable. The platform layer fills in every field of +// DisplayT (dimensions, pitch, pixel format, lfb pointer, +// backBuf pointer, palette, initial clip rect). int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { return platformVideoInit(d, requestedW, requestedH, preferredBpp); @@ -72,6 +150,10 @@ int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t p // ============================================================ // videoShutdown // ============================================================ +// +// Restores text mode, frees the backbuffer and palette, unmaps the +// LFB, and disables DJGPP near pointers. Must be called before +// exit or the console will be left in graphics mode. void videoShutdown(DisplayT *d) { platformVideoShutdown(d); diff --git a/dvx/dvxVideo.h b/dvx/dvxVideo.h index 4ed6935..877f127 100644 --- a/dvx/dvxVideo.h +++ b/dvx/dvxVideo.h @@ -1,25 +1,48 @@ // dvx_video.h — Layer 1: VESA VBE video backend for DVX GUI +// +// The lowest layer in the DVX stack. Responsible for VESA VBE mode +// negotiation, linear framebuffer (LFB) mapping via DPMI, system RAM +// backbuffer allocation, and pixel format discovery. +// +// LFB-only design: bank switching is deliberately unsupported. Every +// VESA 2.0+ card provides LFB, and the code complexity of managing +// 64K bank windows (with scanlines that straddle bank boundaries) is +// not worth supporting ancient VESA 1.x hardware. If videoInit() can't +// find an LFB-capable mode, it fails immediately. +// +// This layer also owns color packing (RGB -> native pixel format) and +// the display-wide clip rectangle. These live here rather than in the +// draw layer because they depend on pixel format knowledge that only +// the video layer has. #ifndef DVX_VIDEO_H #define DVX_VIDEO_H #include "dvxTypes.h" -// Initialize VESA video mode and map LFB -// Returns 0 on success, -1 on failure (error message printed to stderr) +// Probes VBE for a mode matching the requested resolution and depth, +// enables it, maps the LFB into the DPMI linear address space, and +// allocates a system RAM backbuffer of the same size. preferredBpp is +// a hint — if the exact depth isn't available, the closest match is used. int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); -// Shut down video — restore text mode, unmap LFB, free backbuffer +// Restores VGA text mode (INT 10h AH=0, mode 3), unmaps the LFB, and +// frees the backbuffer. Safe to call even if videoInit() failed. void videoShutdown(DisplayT *d); -// Pack an RGB color into the display's pixel format -// For 15/16/32-bit: returns packed pixel value -// For 8-bit: returns nearest palette index +// Pack an RGB triplet into the display's native pixel format. +// For direct-color modes (15/16/32 bpp): returns a packed pixel value +// using the shift/mask fields from PixelFormatT. +// For 8-bit mode: returns the nearest palette index using Euclidean +// distance in RGB space (fast path through the 6x6x6 color cube, then +// linear scan of the grey ramp and chrome entries). uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b); -// Set the clip rectangle on the display +// Set the clip rectangle on the display. All subsequent draw operations +// will be clipped to this rectangle. The caller is responsible for +// saving and restoring the clip rect around scoped operations. void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h); -// Reset clip rectangle to full display +// Reset clip rectangle to the full display dimensions. void resetClipRect(DisplayT *d); #endif // DVX_VIDEO_H diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index b4a02ac..0b78731 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -1,4 +1,33 @@ // dvxWidget.h — Widget system for DVX GUI +// +// A retained-mode widget toolkit layered on top of the DVX window manager. +// Widgets form a tree (parent-child via firstChild/lastChild/nextSibling +// pointers) rooted at a per-window VBox container. Layout is automatic: +// the engine measures minimum sizes bottom-up, then allocates space top-down +// using a flexbox-like algorithm with weights for extra-space distribution. +// +// Design decisions and rationale: +// +// - Single WidgetT struct with a tagged union: avoids the overhead of a +// class hierarchy and virtual dispatch (no C++ vtable indirection). +// The wclass pointer provides vtable-style dispatch only for the few +// operations that genuinely differ per widget type (paint, layout, events). +// Type-specific data lives in the `as` union to keep the struct compact. +// +// - Tagged size values (wgtPixels/wgtChars/wgtPercent): encoding the unit +// in the high bits of a single int32_t avoids extra struct fields for +// unit type and lets size hints be passed as plain integers. The 30-bit +// value range (up to ~1 billion) is more than sufficient for pixel counts. +// +// - Tree linkage uses firstChild/lastChild/nextSibling (no prevSibling): +// this halves the pointer overhead per widget and insertion/removal is +// still O(n) in the worst case, which is acceptable given typical tree +// depths of 5-10 nodes. +// +// - Large widget data (AnsiTermDataT, ListViewDataT) is separately +// allocated and stored as a pointer in the union rather than inlined, +// because these structures are hundreds of bytes and would bloat every +// WidgetT even for simple labels and buttons. #ifndef DVX_WIDGET_H #define DVX_WIDGET_H @@ -14,9 +43,15 @@ struct WidgetClassT; // Size specifications // ============================================================ // -// Tagged size values encode both a unit type and a numeric value. -// Use wgtPixels(), wgtChars(), or wgtPercent() to create them. -// A raw 0 means "auto" (use the widget's natural size). +// Tagged size values encode both a unit type and a numeric value in a +// single int32_t. The top 2 bits select the unit (pixels, character widths, +// or percentage of parent), and the low 30 bits hold the numeric value. +// A raw 0 means "auto" (use the widget's natural/minimum size). +// +// This encoding avoids a separate enum field for the unit type, keeping +// size hints as simple scalar assignments: w->minW = wgtChars(40); +// The wgtResolveSize() function in the layout engine decodes these tagged +// values back into pixel counts using the font metrics and parent dimensions. #define WGT_SIZE_TYPE_MASK 0xC0000000 #define WGT_SIZE_VAL_MASK 0x3FFFFFFF @@ -39,6 +74,11 @@ static inline int32_t wgtPercent(int32_t v) { // ============================================================ // Widget type enum // ============================================================ +// +// Used as the index into widgetClassTable[] (in widgetInternal.h) to +// look up the vtable for each widget type. Adding a new widget type +// requires adding an enum value here, a corresponding union member in +// WidgetT, and a WidgetClassT entry in widgetClassTable[]. typedef enum { WidgetVBoxE, @@ -135,7 +175,19 @@ typedef enum { // ============================================================ // Large widget data (separately allocated to reduce WidgetT size) // ============================================================ +// +// AnsiTermDataT and ListViewDataT are heap-allocated and stored as +// pointers in the WidgetT union. Inlining them would add hundreds of +// bytes to every WidgetT instance (even labels and buttons), wasting +// memory on the 30+ simple widgets that don't need terminal or listview +// state. The pointer indirection adds one dereference but saves +// significant memory across a typical widget tree. +// AnsiTermDataT — full VT100/ANSI terminal emulator state. +// Implements a subset of DEC VT100 escape sequences sufficient for BBS +// and DOS ANSI art rendering: cursor movement, color attributes (16-color +// with bold-as-bright), scrolling regions, and blink. The parser is a +// simple state machine (normal -> ESC -> CSI) with parameter accumulation. typedef struct { uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes int32_t cols; // columns (default 80) @@ -156,7 +208,10 @@ typedef struct { // Scrolling region (0-based, inclusive) int32_t scrollTop; // top row of scroll region int32_t scrollBot; // bottom row of scroll region - // Scrollback + // Scrollback — circular buffer so old lines age out naturally without + // memmove. Each line is cols*2 bytes (same ch/attr format as cells). + // scrollPos tracks the view offset: when equal to scrollbackCount, + // the user sees the live screen; when less, they're viewing history. uint8_t *scrollback; // circular buffer of scrollback lines int32_t scrollbackMax; // max lines in scrollback buffer int32_t scrollbackCount; // current number of lines stored @@ -168,11 +223,17 @@ typedef struct { // Cursor blink bool cursorOn; // current cursor blink phase clock_t cursorTime; // timestamp of last cursor toggle - // Dirty tracking for fast repaint + // Dirty tracking — a 32-bit bitmask where each bit corresponds to one + // terminal row. Only dirty rows are repainted, which is critical because + // the ANSI terminal can receive data every frame (at 9600+ baud) and + // re-rendering all 25 rows of 80 columns each frame would dominate the + // CPU budget. Limits terminal height to 32 rows, which is fine for the + // 25-row default target. uint32_t dirtyRows; // bitmask of rows needing repaint int32_t lastCursorRow; // cursor row at last repaint int32_t lastCursorCol; // cursor col at last repaint - // Cached packed palette (avoids packColor per repaint) + // Pre-packed 16-color palette avoids calling packColor() (which involves + // shift/mask arithmetic) 80*25 = 2000 times per full repaint. uint32_t packedPalette[16]; bool paletteValid; // Selection (line indices in scrollback+screen space) @@ -181,12 +242,21 @@ typedef struct { int32_t selEndLine; int32_t selEndCol; bool selecting; - // Communications interface (all NULL = disconnected) + // Communications interface — abstracted so the terminal can connect to + // different backends (serial port, secLink channel, local pipe) without + // knowing the transport details. When all are NULL, the terminal is in + // offline/disconnected mode (useful for viewing .ANS files). void *commCtx; int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); } AnsiTermDataT; +// ListViewDataT — multi-column list with sortable headers and optional +// multi-select. cellData is a flat array of strings indexed as +// cellData[row * colCount + col]. The sortIndex is an indirection array +// that maps displayed row numbers to data row numbers, allowing sort +// without rearranging the actual data. resolvedColW[] caches pixel widths +// after resolving tagged column sizes, avoiding re-resolution on every paint. typedef struct { const ListViewColT *cols; int32_t colCount; @@ -217,8 +287,15 @@ typedef struct { typedef struct WidgetT { WidgetTypeE type; + // wclass points to the vtable for this widget type. Looked up once at + // creation from widgetClassTable[type]. This avoids a switch on type + // in every paint/layout/event dispatch — the cost is one pointer per + // widget, which is negligible. const struct WidgetClassT *wclass; char name[MAX_WIDGET_NAME]; + // djb2 hash of the name string, computed at wgtSetName() time. + // wgtFind() compares hashes before strcmp, making name lookups fast + // for the common no-match case in a tree with many unnamed widgets. uint32_t nameHash; // djb2 hash of name, 0 if unnamed // Tree linkage @@ -234,17 +311,27 @@ typedef struct WidgetT { int32_t w; int32_t h; - // Computed minimum size (set by layout engine) + // Computed minimum size — set bottom-up by calcMinSize during layout. + // These represent the smallest possible size for this widget (including + // its children if it's a container). The layout engine uses these as + // the starting point for space allocation. int32_t calcMinW; int32_t calcMinH; - // Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto) + // Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto). + // These are set by the application and influence the layout engine: + // minW/minH override calcMinW/H if larger, maxW/maxH clamp the final + // size, and prefW/prefH request a specific size (layout may override). int32_t minW; int32_t minH; int32_t maxW; // 0 = no limit int32_t maxH; int32_t prefW; // preferred size, 0 = auto int32_t prefH; + // weight controls how extra space beyond minimum is distributed among + // siblings in a VBox/HBox. weight=0 means fixed size (no stretching), + // weight=100 is the default for flexible widgets. A widget with + // weight=200 gets twice as much extra space as one with weight=100. int32_t weight; // extra-space distribution (0 = fixed, 100 = normal) // Container properties @@ -270,7 +357,12 @@ typedef struct WidgetT { void (*onChange)(struct WidgetT *w); void (*onDblClick)(struct WidgetT *w); - // Type-specific data + // Type-specific data — tagged union keyed by the `type` field. + // Only the member corresponding to `type` is valid. This is the C + // equivalent of a discriminated union / variant type. Using a union + // instead of separate structs per widget type keeps all widget data + // in a single allocation, which simplifies memory management and + // avoids pointer chasing during layout/paint traversal. union { struct { const char *text; @@ -295,6 +387,11 @@ typedef struct WidgetT { int32_t index; } radio; + // Text input has its own edit buffer (not a pointer to external + // storage) so the widget fully owns its text lifecycle. The undo + // buffer holds a single-level snapshot taken before each edit + // operation — Ctrl+Z restores to the snapshot. This is simpler + // than a full undo stack but sufficient for single-line fields. struct { char *buf; int32_t bufSize; @@ -310,6 +407,11 @@ typedef struct WidgetT { const char *mask; // format mask for InputMaskedE } textInput; + // Multi-line text editor. desiredCol implements the "sticky column" + // behavior where pressing Up/Down tries to return to the column + // the cursor was at before traversing shorter lines (standard + // text editor UX). cachedLines/cachedMaxLL cache values that + // require full-buffer scans, invalidated to -1 on any text change. struct { char *buf; int32_t bufSize; @@ -404,6 +506,11 @@ typedef struct WidgetT { const char *title; } tabPage; + // TreeView uses widget children (WidgetTreeItemE) as its items, + // unlike ListBox which uses a string array. This allows nested + // hierarchies with expand/collapse state per item. The tree is + // rendered by flattening visible items during paint, with TREE_INDENT + // pixels of indentation per nesting level. struct { int32_t scrollPos; int32_t scrollPosH; @@ -488,13 +595,23 @@ typedef struct WidgetT { // Window integration // ============================================================ -// Set up a window for widgets. Returns the root container (VBox). -// Automatically installs onPaint, onMouse, onKey, and onResize handlers. +// Initialize the widget system for a window. Creates a root VBox container +// that fills the window's content area, and installs callback handlers +// (onPaint, onMouse, onKey, onResize) that dispatch events to the widget +// tree. After this call, the window is fully managed by the widget system +// — the application builds its UI by adding child widgets to the returned +// root container. The window's userData is set to the AppContextT pointer. WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win); // ============================================================ // Container creation // ============================================================ +// +// VBox and HBox are the primary layout containers, analogous to CSS +// flexbox with column/row direction. Children are laid out sequentially +// along the main axis (vertical for VBox, horizontal for HBox) with +// spacing between them. Extra space is distributed according to each +// child's weight. Frame is a titled groupbox container with a bevel border. WidgetT *wgtVBox(WidgetT *parent); WidgetT *wgtHBox(WidgetT *parent); @@ -514,6 +631,11 @@ WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask); // ============================================================ // Radio buttons // ============================================================ +// +// Radio buttons must be children of a RadioGroup container. The group +// tracks which child is selected (by index). Clicking a radio button +// automatically deselects the previously selected sibling. This parent- +// tracking approach avoids explicit "radio group ID" parameters. WidgetT *wgtRadioGroup(WidgetT *parent); WidgetT *wgtRadio(WidgetT *parent, const char *text); @@ -521,6 +643,11 @@ WidgetT *wgtRadio(WidgetT *parent, const char *text); // ============================================================ // Spacing and visual dividers // ============================================================ +// +// Spacer is an invisible flexible widget (weight=100) that absorbs +// extra space — useful for pushing subsequent siblings to the end of +// a container (like CSS flex: 1 auto). Separators are thin beveled +// lines for visual grouping. WidgetT *wgtSpacer(WidgetT *parent); WidgetT *wgtHSeparator(WidgetT *parent); @@ -702,11 +829,19 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y); // ============================================================ // ANSI Terminal // ============================================================ +// +// A VT100/ANSI terminal emulator widget. Supports the subset of escape +// sequences needed for DOS BBS programs and ANSI art: cursor positioning, +// SGR color/attribute codes, scrolling regions, and text blink. Pairs +// with the secLink communications layer for remote serial connections +// or can be used standalone for viewing .ANS files. // Create an ANSI terminal widget (0 for cols/rows = 80x25 default) WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows); -// Write data through the ANSI parser (for loading .ANS files or feeding data without a connection) +// Write raw data through the ANSI escape sequence parser. Used for +// loading .ANS files or feeding data from a source that isn't using +// the comm interface. void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); // Clear the terminal screen and reset cursor to home @@ -721,23 +856,35 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); // Poll the comm interface for incoming data and process it. Returns bytes processed. int32_t wgtAnsiTermPoll(WidgetT *w); -// Fast repaint: renders only dirty rows directly into the window's content -// buffer, bypassing the full widget paint pipeline. Returns number of rows -// repainted (0 if nothing was dirty). If outY/outH are non-NULL, they receive -// the content-buffer-relative Y and height of the repainted region. +// Fast-path repaint for the terminal widget. Instead of going through the +// full widget paint pipeline (which would repaint the entire widget), this +// renders only the dirty rows (tracked via the dirtyRows bitmask) directly +// into the window's content buffer. This is essential for responsive terminal +// output — incoming serial data can dirty a few rows per frame, and +// repainting only those rows keeps the cost proportional to the actual +// change rather than the full 80x25 grid. Returns the number of rows +// repainted; outY/outH report the affected region for dirty-rect tracking. int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH); // ============================================================ // Operations // ============================================================ -// Get the AppContextT from any widget (walks to root) +// Walk from any widget up the tree to the root, then retrieve the +// AppContextT stored in the window's userData. This lets any widget +// access the full application context without passing it through every +// function call. struct AppContextT *wgtGetContext(const WidgetT *w); -// Mark a widget (and ancestors) for relayout and repaint +// Mark a widget as needing both re-layout (measure + position) and +// repaint. Propagates upward to ancestors since a child's size change +// can affect parent layout. Use this after structural changes (adding/ +// removing children, changing text that affects size). void wgtInvalidate(WidgetT *w); -// Repaint only — skip measure/layout (use for visual-only changes) +// Mark a widget as needing repaint only, without re-layout. Use this +// for visual-only changes that don't affect geometry (e.g. checkbox +// toggle, selection highlight change, cursor blink). void wgtInvalidatePaint(WidgetT *w); // Set/get widget text (label, button, textInput, etc.) @@ -792,13 +939,24 @@ void wgtSetDebugLayout(struct AppContextT *ctx, bool enabled); // Layout (called internally; available for manual trigger) // ============================================================ -// Resolve a tagged size to pixels +// Decode a tagged size value (WGT_SIZE_PIXELS/CHARS/PERCENT) into a +// concrete pixel count. For CHARS, multiplies by charWidth; for PERCENT, +// computes the fraction of parentSize. Returns 0 for a raw 0 input +// (meaning "auto"). int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth); -// Run layout on the entire widget tree +// Execute the full two-pass layout algorithm on the widget tree: +// Pass 1 (bottom-up): calcMinSize on every widget to compute minimum sizes. +// Pass 2 (top-down): allocate space within availW/availH, distributing +// extra space according to weights and respecting min/max constraints. +// Normally called automatically by the paint handler; exposed here for +// cases where layout must be forced before the next paint (e.g. dvxFitWindow). void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font); -// Paint the entire widget tree +// Paint the entire widget tree by depth-first traversal. Each widget's +// clip rect is set to its bounds before calling its paint function. +// Overlays (dropdown popups, tooltips) are painted in a second pass +// after the main tree so they render on top of everything. void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); #endif // DVX_WIDGET_H diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index 772b865..332228a 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -1,4 +1,33 @@ // dvx_wm.c — Layer 4: Window manager for DVX GUI +// +// This layer manages the window stack (z-order), window chrome rendering +// (title bars, borders, bevels, gadgets, menu bars, scrollbars), and user +// interactions like drag, resize, minimize, maximize, and focus. It sits +// between the compositor (layer 3) and the application event loop (layer 5). +// +// Architecture decisions: +// +// - Z-order is a simple pointer array (windows[0] = back, windows[count-1] +// = front). This was chosen over a linked list because hit-testing walks +// the stack front-to-back every mouse event, and array iteration has +// better cache behavior on 486/Pentium. Raising a window is O(N) shift +// but N is bounded by MAX_WINDOWS=64 and raise is infrequent. +// +// - Window chrome uses a Motif/GEOS Ensemble visual style with fixed 4px +// outer bevels and 2px inner bevels. The fixed bevel widths avoid per- +// window style negotiation and let chrome geometry be computed with simple +// arithmetic from the frame rect, shared between drawing and hit-testing +// via computeTitleGeom(). +// +// - Each window has its own content backbuffer (contentBuf). The WM blits +// this to the display backbuffer during compositing. This means window +// content survives being occluded — apps don't get expose events and don't +// need to repaint when uncovered, which hugely simplifies app code and +// avoids the latency of synchronous repaint-on-expose. +// +// - Scrollbar and menu bar state are allocated on-demand (NULL if absent). +// This keeps the base WindowT struct small for simple windows while +// supporting rich chrome when needed. #include "dvxWm.h" #include "dvxVideo.h" @@ -15,6 +44,10 @@ // Constants // ============================================================ +// Title bar gadget layout constants. These mirror the GEOS Ensemble/Motif +// look: small beveled squares inset from the title bar edges, with specific +// icon sizes that remain legible at the 16px gadget size (CHROME_TITLE_HEIGHT +// minus 2*GADGET_INSET). #define GADGET_PAD 2 #define GADGET_INSET 2 // inset from title bar edges #define MENU_BAR_GAP 8 // horizontal gap between menu bar labels @@ -29,6 +62,11 @@ // ============================================================ // Title bar gadget geometry // ============================================================ +// +// Extracted into a struct so that both drawTitleBar() and wmHitTest() compute +// identical geometry from the same code path (computeTitleGeom). Without this, +// a discrepancy between draw and hit-test coordinates would cause clicks to +// land on the wrong gadget — a subtle bug that's hard to reproduce visually. typedef struct { int32_t titleX; @@ -68,12 +106,18 @@ static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH); // computeMenuBarPositions // ============================================================ +// Lays out menu bar label positions left-to-right. Each label gets +// padding (CHROME_TITLE_PAD) on both sides and MENU_BAR_GAP between labels. +// Positions are cached and only recomputed when positionsDirty is set (after +// adding/removing a menu), avoiding redundant textWidthAccel calls on every +// paint — font metric calculation is expensive relative to a simple flag check. +// barX values are relative to the window, not the screen; the draw path adds +// the window's screen position. static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { if (!win->menuBar) { return; } - // Skip recomputation if positions are already up to date (Item 5) if (!win->menuBar->positionsDirty) { return; } @@ -99,6 +143,15 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { // // Compute title bar gadget positions. Used by both drawTitleBar() // and wmHitTest() to keep geometry in sync. +// +// Layout follows the DESQview/X + GEOS Ensemble convention: +// [Close] ---- Title Text ---- [Minimize] [Maximize] +// Close is on the far left (also doubles as system menu in DV/X style), +// minimize and maximize pack right-to-left from the far right edge. +// Modal windows suppress the minimize gadget since they must stay visible. +// Non-resizable windows suppress the maximize gadget. +// The text area occupies whatever space remains between the gadgets, +// with the title centered within that span. static void computeTitleGeom(const WindowT *win, TitleGeomT *g) { g->titleX = win->x + CHROME_BORDER_WIDTH; @@ -108,10 +161,9 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) { g->gadgetS = g->titleH - GADGET_INSET * 2; g->gadgetY = g->titleY + GADGET_INSET; - // Close gadget on the left g->closeX = g->titleX + GADGET_PAD; - // Rightmost gadget position (decremented as gadgets are placed) + // Pack right-side gadgets from right edge inward int32_t rightX = g->titleX + g->titleW - GADGET_PAD - g->gadgetS; if (win->resizable) { @@ -128,7 +180,7 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) { rightX -= g->gadgetS + GADGET_PAD; } - // Text area between close gadget and first right-side gadget + // Text area is the remaining span between close and rightmost gadgets g->textLeftEdge = g->closeX + g->gadgetS + GADGET_PAD; g->textRightEdge = rightX + g->gadgetS; } @@ -137,12 +189,28 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) { // ============================================================ // drawBorderFrame // ============================================================ +// // 4px raised Motif-style border using 3 shades: // highlight (outer top/left), face (middle), shadow (outer bottom/right) // with inner crease lines for 3D depth. // -// Top/Left, outside to inside: highlight, highlight, face, shadow -// Bottom/Right, outside to inside: shadow, shadow, face, highlight +// Cross-section of the top edge (4 rows, outside to inside): +// Row 0: highlight (bright edge catches the "light" from top-left) +// Row 1: highlight (doubled for visual weight at low resolutions) +// Row 2: face (flat middle band) +// Row 3: shadow (inner crease — makes the border feel like two beveled +// ridges rather than one flat edge) +// +// Bottom/right are mirror-reversed (shadow, shadow, face, highlight). This +// crease pattern is what distinguishes the Motif "ridge" look from a simple +// 2-shade bevel. The 4px width was chosen because it's the minimum that +// shows the crease effect clearly at 640x480; narrower borders lose the +// middle face band and look flat. +// +// Each edge is drawn as individual 1px rectFill calls rather than a single +// drawBevel call because the 3-shade crease pattern doesn't map to the +// 2-shade BevelStyleT. The cost is 16 rectFill calls + 1 interior fill, +// which is negligible since border drawing is clipped to dirty rects. static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h) { uint32_t hi = colors->windowHighlight; @@ -181,7 +249,12 @@ static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT // ============================================================ // drawTitleGadget // ============================================================ -// Draws a small raised beveled square gadget (GEOS Motif style). +// +// Draws a small raised beveled square (1px bevel) used as the base for +// close, minimize, and maximize buttons. The icon (bar, box, dot) is drawn +// on top by the caller. Using 1px bevels on gadgets (vs 4px on the frame) +// keeps them visually subordinate to the window border — a Motif convention +// that helps users distinguish clickable controls from structural chrome. static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size) { BevelStyleT bevel; @@ -196,6 +269,17 @@ static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT // ============================================================ // drawMenuBar // ============================================================ +// +// Renders the menu bar strip immediately below the title bar. The menu bar +// lives inside the window's outer border but above the inner sunken bevel +// and content area. +// +// The clip rect is narrowed to the menu bar area before drawing labels so +// that long label text doesn't bleed into the window border. The clip is +// saved/restored rather than set to the full dirty rect because this +// function is called from wmDrawChrome which has already set the clip to +// the dirty rect — we need to intersect with both. The separator line at +// the bottom visually separates the menu bar from the content area. static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) { if (!win->menuBar) { @@ -205,11 +289,10 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; int32_t barH = CHROME_MENU_HEIGHT; - // Fill menu bar background rectFill(d, ops, win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH, colors->menuBg); - // Clip menu labels to the menu bar area + // Tighten clip to menu bar bounds to prevent label overflow int32_t savedClipX = d->clipX; int32_t savedClipY = d->clipY; int32_t savedClipW = d->clipW; @@ -217,7 +300,6 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon setClipRect(d, win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH); - // Draw each menu label for (int32_t i = 0; i < win->menuBar->menuCount; i++) { MenuT *menu = &win->menuBar->menus[i]; int32_t textX = win->x + menu->barX + CHROME_TITLE_PAD; @@ -229,7 +311,6 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH); - // Draw bottom separator line drawHLine(d, ops, win->x + CHROME_BORDER_WIDTH, barY + barH - 1, win->w - CHROME_BORDER_WIDTH * 2, colors->windowShadow); } @@ -238,11 +319,20 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon // ============================================================ // drawResizeBreaks // ============================================================ -// GEOS Ensemble Motif-style: perpendicular grooves that cut across -// the window border near each corner to indicate resizable edges. -// Two breaks per corner — one on each edge meeting at that corner. -// Each groove is a shadow+highlight line pair cutting across the -// border width, perpendicular to the edge direction. +// +// GEOS Ensemble Motif-style resize indicators: perpendicular grooves that +// cut across the window border near each corner. Two breaks per corner — +// one on each edge meeting at that corner. Each groove is a 2px sunken +// notch (shadow line + highlight line) cutting across the full 4px border +// width, perpendicular to the edge direction. +// +// These serve as a visual affordance telling the user which edges are +// resizable. Unlike CUA/Windows which uses a diagonal hatch in the +// bottom-right corner only, the Motif style marks all four corners +// symmetrically, indicating that all edges are draggable. The breaks +// are positioned RESIZE_BREAK_INSET (16px) from each corner, which +// also defines the boundary between corner-resize (diagonal) and +// edge-resize (single axis) hit zones in wmResizeEdgeHit. static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win) { if (!win->resizable) { @@ -293,9 +383,26 @@ static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorScheme // ============================================================ // drawScaledRect // ============================================================ +// +// Nearest-neighbor scale blit used to render minimized window icons. +// Icons are ICON_SIZE x ICON_SIZE (64x64); the source can be the window's +// content buffer (live thumbnail) or a loaded icon image of any size. +// +// Optimization strategy for 486/Pentium: +// - Fixed-point 16.16 reciprocals replace per-pixel division with +// multiply+shift. Division is 40+ cycles on 486; multiply is 13. +// - Source coordinate lookup tables (srcXTab/srcYTab) are pre-computed +// once per call and indexed in the inner loop, eliminating repeated +// multiply+shift per pixel. +// - Static lookup table buffers avoid stack frame growth (ICON_SIZE=64 +// entries * 4 bytes = 256 bytes that would otherwise be allocated on +// every call during the compositing loop). +// - The bpp switch is outside the column loop so the branch is taken +// once per row rather than once per pixel. +// - Clipping is done once up front by adjusting row/col start/end ranges, +// so the inner loop runs with zero per-pixel clip checks. static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp) { - // Pre-compute visible row/col range (clip against display clip rect) int32_t rowStart = 0; int32_t rowEnd = dstH; int32_t colStart = 0; @@ -310,17 +417,16 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW return; } - // Pre-compute source lookup tables using fixed-point reciprocals - // to replace per-entry division with multiply+shift (Item 1). - // Static buffers avoid per-call stack allocation (Item 9). static int32_t srcXTab[ICON_SIZE]; static int32_t srcYTab[ICON_SIZE]; int32_t visibleCols = colEnd - colStart; int32_t visibleRows = rowEnd - rowStart; + // Fixed-point 16.16 scale factors uint32_t recipW = ((uint32_t)srcW << 16) / (uint32_t)dstW; uint32_t recipH = ((uint32_t)srcH << 16) / (uint32_t)dstH; + // Pre-multiply X offsets by bpp so the inner loop does a single array index for (int32_t dx = 0; dx < visibleCols; dx++) { srcXTab[dx] = (int32_t)(((uint32_t)(colStart + dx) * recipW) >> 16) * bpp; } @@ -329,7 +435,6 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW srcYTab[dy] = (int32_t)(((uint32_t)(rowStart + dy) * recipH) >> 16); } - // Blit with pre-computed lookups — no per-pixel divisions or clip checks for (int32_t dy = rowStart; dy < rowEnd; dy++) { int32_t sy = srcYTab[dy - rowStart]; uint8_t *dstRow = d->backBuf + (dstY + dy) * d->pitch + (dstX + colStart) * bpp; @@ -355,6 +460,20 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW // ============================================================ // drawScrollbar // ============================================================ +// +// Renders a complete scrollbar: trough background, sunken bevel around the +// trough, arrow buttons at each end, and a draggable thumb. +// +// The layering order matters: trough fill first, then sunken bevel over it +// (so bevel edges aren't overwritten), then arrow button bevels + glyphs +// over the trough ends, and finally the thumb bevel in the middle. Arrow +// buttons and thumb use 1px raised bevels for a clickable appearance; +// the trough uses a 1px sunken bevel (swapped highlight/shadow) to appear +// recessed — standard Motif scrollbar convention. +// +// winX/winY are the window's screen position; sb->x/y are relative to the +// window origin. This split lets scrollbar positions survive window drags +// without recalculation — only winX/winY change. static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) { int32_t x = winX + sb->x; @@ -426,8 +545,16 @@ static void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * // ============================================================ // drawScrollbarArrow // ============================================================ +// // Draws a small triangle arrow glyph inside a scrollbar button. // dir: 0=up, 1=down, 2=left, 3=right +// +// The triangle is built row-by-row from the tip outward: each successive +// row is 2 pixels wider (1 pixel on each side), producing a symmetric +// isoceles triangle. SB_ARROW_ROWS (4) rows gives a 7-pixel-wide base, +// which fits well inside SCROLLBAR_WIDTH (16px) buttons. The glyph is +// centered on the button using integer division — no sub-pixel alignment +// needed at these sizes. static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, int32_t dir) { int32_t cx = x + size / 2; @@ -460,6 +587,21 @@ static void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSche // ============================================================ // drawTitleBar // ============================================================ +// +// Renders the title bar: background fill, close gadget (left), minimize +// and maximize gadgets (right), and centered title text. +// +// The title bar background uses active/inactive colors to provide a strong +// visual cue for which window has keyboard focus — the same convention used +// by Windows 3.x, Motif, and CDE. Only the title bar changes color on +// focus change; the rest of the chrome stays the same. This is why +// wmSetFocus dirties only the title bar area, not the entire window. +// +// Gadget icons are drawn as simple geometric shapes (horizontal bar for +// close, filled square for minimize, box outlines for maximize/restore) +// rather than bitmaps. This avoids loading icon resources, works at any +// color depth, and the shapes are recognizable even at the small 16x16 +// gadget size on a 640x480 display. static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win) { TitleGeomT g; @@ -513,7 +655,10 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo MINIMIZE_ICON_SIZE, MINIMIZE_ICON_SIZE, colors->contentFg); } - // Title text — centered between close gadget and minimize, truncated to fit + // Title text is centered in the available space between gadgets. If the + // title is too long, it's truncated by character count (not pixel width) + // since the font is fixed-width. No ellipsis is added — at these sizes, + // ellipsis would consume 3 characters that could show useful text instead. int32_t availW = g.textRightEdge - g.textLeftEdge; if (availW > 0) { @@ -543,6 +688,11 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo // ============================================================ // freeMenuRecursive // ============================================================ +// +// Walks the submenu tree depth-first, freeing heap-allocated child MenuT +// nodes. The top-level MenuT structs are embedded in the MenuBarT array +// and are freed when the MenuBarT is freed, so this only needs to handle +// dynamically allocated submenu children. static void freeMenuRecursive(MenuT *menu) { for (int32_t i = 0; i < menu->itemCount; i++) { @@ -558,6 +708,15 @@ static void freeMenuRecursive(MenuT *menu) { // ============================================================ // minimizedIconPos // ============================================================ +// +// Computes the screen position of a minimized window icon. Icons are laid +// out in a horizontal strip along the bottom of the screen, left to right. +// This mirrors the DESQview/X and Windows 3.x convention of showing minimized +// windows as icons at the bottom of the desktop. +// +// The index is the ordinal among minimized windows (not the stack index), +// so icon positions stay packed when non-minimized windows exist between +// minimized ones in the stack. This avoids gaps in the icon strip. static void minimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32_t *y) { *x = ICON_SPACING + index * (ICON_TOTAL_SIZE + ICON_SPACING); @@ -568,6 +727,22 @@ static void minimizedIconPos(const DisplayT *d, int32_t index, int32_t *x, int32 // ============================================================ // scrollbarThumbInfo // ============================================================ +// +// Computes thumb position and size within the scrollbar track. The track +// is the total scrollbar length minus the two arrow buttons at each end. +// +// Thumb size is proportional to the visible page relative to the total +// scrollable range: thumbSize = trackLen * pageSize / (range + pageSize). +// This gives a thumb that shrinks as content grows, matching user +// expectations from Motif/Windows scrollbars. The minimum thumb size is +// clamped to SCROLLBAR_WIDTH so it remains grabbable even with huge content. +// +// The int64_t casts prevent overflow: trackLen can be ~400px, pageSize and +// range can be arbitrarily large (e.g. a 10000-line text buffer), and +// 400 * 10000 exceeds int32_t range. +// +// When range <= 0 (all content visible), the thumb fills the entire track, +// visually indicating there's nothing to scroll. static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize) { int32_t trackLen = sb->length - SCROLLBAR_WIDTH * 2; @@ -585,6 +760,8 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32 *thumbSize = SCROLLBAR_WIDTH; } + // Map value to pixel offset: value maps linearly to the range + // [0, trackLen - thumbSize] *thumbPos = (int32_t)(((int64_t)(sb->value - sb->min) * (trackLen - *thumbSize)) / range); return trackLen; @@ -594,6 +771,17 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32 // ============================================================ // wmAddHScrollbar // ============================================================ +// +// Adds a horizontal scrollbar to a window. The scrollbar steals +// SCROLLBAR_WIDTH pixels from the bottom of the content area (handled by +// wmUpdateContentRect). The caller specifies the logical scroll range; the +// WM handles all geometry, rendering, and hit-testing from there. +// +// Calling wmUpdateContentRect immediately ensures that the content area +// shrinks to accommodate the scrollbar and the scrollbar's position/length +// fields are initialized before the next paint. This matters because the +// caller may add both scrollbars before any paint occurs, and the second +// scrollbar's geometry depends on the first one already being accounted for. ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize) { win->hScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); @@ -608,7 +796,6 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page win->hScroll->value = min; win->hScroll->pageSize = pageSize; - // Position will be updated by wmUpdateContentRect wmUpdateContentRect(win); return win->hScroll; @@ -618,6 +805,17 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page // ============================================================ // wmAddMenu // ============================================================ +// +// Adds a top-level menu to a window's menu bar. The MenuT struct is +// stored inline in the MenuBarT's fixed-size array (MAX_MENUS=8) rather +// than heap-allocated, since menus are created once at window setup and +// the count is small. The positionsDirty flag is set so that the next +// paint triggers computeMenuBarPositions to lay out all labels. +// +// The accelerator key is parsed from & markers in the label (e.g. +// "&File" -> Alt+F), following the CUA/Windows convention. This is +// stored on the MenuT so the event loop can match keyboard shortcuts +// without re-parsing the label string on every keypress. MenuT *wmAddMenu(MenuBarT *bar, const char *label) { if (bar->menuCount >= MAX_MENUS) { @@ -639,6 +837,12 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) { // ============================================================ // wmAddMenuBar // ============================================================ +// +// Allocates and attaches a menu bar to a window. The menu bar is heap- +// allocated separately from the window because most windows don't have +// one, and keeping it out of WindowT saves ~550 bytes per window +// (MAX_MENUS * sizeof(MenuT)). wmUpdateContentRect is called to shrink +// the content area by CHROME_MENU_HEIGHT to make room for the bar. MenuBarT *wmAddMenuBar(WindowT *win) { win->menuBar = (MenuBarT *)malloc(sizeof(MenuBarT)); @@ -740,6 +944,12 @@ void wmAddMenuSeparator(MenuT *menu) { // ============================================================ // wmAddSubMenu // ============================================================ +// +// Creates a cascading submenu. Unlike top-level menus (inline in MenuBarT), +// submenus are heap-allocated because the nesting depth is unpredictable and +// the submenu is owned by its parent item. The item's id is set to -1 to +// distinguish it from leaf items during event dispatch — clicking a submenu +// item opens the child rather than firing a command. MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) { if (parentMenu->itemCount >= MAX_MENU_ITEMS) { @@ -770,6 +980,10 @@ MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) { // ============================================================ // wmAddVScrollbar // ============================================================ +// +// Adds a vertical scrollbar to a window. Mirrors wmAddHScrollbar; the +// scrollbar steals SCROLLBAR_WIDTH pixels from the right side of the +// content area. See wmAddHScrollbar for design notes. ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize) { win->vScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); @@ -794,6 +1008,25 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page // ============================================================ // wmCreateWindow // ============================================================ +// +// Creates a window, allocates its content backbuffer, and places it at the +// top of the z-order stack. The caller specifies the outer frame dimensions +// (x, y, w, h); the content area is computed by subtracting chrome from +// the frame. +// +// Each window gets a unique monotonic ID (static nextId) used for lookup +// by the event loop and DXE app system. IDs are never reused — with 2^31 +// IDs available and windows being created/destroyed interactively, this +// will never wrap in practice. +// +// The content buffer is initialized to 0xFF (white) so newly created +// windows have a clean background before the app's first onPaint fires. +// maxW/maxH default to -1 meaning "use screen dimensions" — apps can +// override this to constrain maximized size (e.g. for dialog-like windows). +// +// New windows are added at stack->count (the top), so they appear in front +// of all existing windows. The caller is responsible for calling wmSetFocus +// and wmRaiseWindow if desired. WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { if (stack->count >= MAX_WINDOWS) { @@ -829,7 +1062,6 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int wmUpdateContentRect(win); - // Allocate content buffer win->contentPitch = win->contentW * d->format.bytesPerPixel; int32_t bufSize = win->contentPitch * win->contentH; @@ -842,10 +1074,9 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int return NULL; } - memset(win->contentBuf, 0xFF, bufSize); // white background + memset(win->contentBuf, 0xFF, bufSize); } - // Add to top of stack stack->windows[stack->count] = win; stack->count++; @@ -856,19 +1087,27 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int // ============================================================ // wmDestroyWindow // ============================================================ +// +// Removes a window from the stack and frees all associated resources. +// The stack is compacted by shifting entries down (O(N), but N <= 64 and +// destroy is infrequent). focusedIdx is adjusted to track the same logical +// window after the shift: if the focused window was above the destroyed one, +// its index decreases by 1; if the destroyed window was focused, focus moves +// to the new topmost window. +// +// Resource cleanup order: widget tree first (may reference window fields), +// then content buffer, menus (with recursive submenu cleanup), scrollbars, +// icon data, and finally the window struct itself. void wmDestroyWindow(WindowStackT *stack, WindowT *win) { - // Find and remove from stack for (int32_t i = 0; i < stack->count; i++) { if (stack->windows[i] == win) { - // Shift remaining windows down for (int32_t j = i; j < stack->count - 1; j++) { stack->windows[j] = stack->windows[j + 1]; } stack->count--; - // Adjust focusedIdx if (stack->focusedIdx == i) { stack->focusedIdx = stack->count > 0 ? stack->count - 1 : -1; } else if (stack->focusedIdx > i) { @@ -916,6 +1155,11 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) { // ============================================================ // wmDragBegin // ============================================================ +// +// Initiates a window drag by recording the mouse offset from the window +// origin. This offset is maintained throughout the drag so that the window +// tracks the mouse cursor without jumping — the window position under the +// cursor stays consistent from mousedown to mouseup. void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouseY) { stack->dragWindow = idx; @@ -936,6 +1180,18 @@ void wmDragEnd(WindowStackT *stack) { // ============================================================ // wmDragMove // ============================================================ +// +// Called on each mouse move during a drag. Dirties both the old and new +// positions so the compositor repaints the area the window vacated (exposing +// the desktop or windows behind it) and the area it now occupies. These two +// rects will often overlap and get merged by dirtyListMerge, reducing the +// actual flush work. +// +// Unlike some WMs that use an "outline drag" (drawing a wireframe while +// dragging and moving the real window on mouse-up), we do full content +// dragging. This is feasible because the content buffer is persistent — +// we don't need to ask the app to repaint during the drag, just blit from +// its buffer at the new position. void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mouseY) { if (stack->dragWindow < 0 || stack->dragWindow >= stack->count) { @@ -944,14 +1200,11 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou WindowT *win = stack->windows[stack->dragWindow]; - // Mark old position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); - // Update position win->x = mouseX - stack->dragOffX; win->y = mouseY - stack->dragOffY; - // Mark new position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); } @@ -959,9 +1212,23 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou // ============================================================ // wmDrawChrome // ============================================================ +// +// Paints all window chrome (everything that's not content) clipped to a +// dirty rect. Called once per window per dirty rect during compositing. +// +// Paint order: outer border -> title bar -> menu bar -> inner bevel -> +// resize breaks. This is bottom-to-top layering: the title bar overwrites +// the border's interior, and resize breaks are drawn last so they cut +// across the already-painted border. +// +// The clip rect is set to the dirty rect so all draw operations are +// automatically clipped. This is the mechanism by which partial chrome +// repaints work — if only the title bar is dirty, the border fill runs +// but its output is clipped away for scanlines outside the dirty rect. +// The clip rect is saved/restored because the caller (compositeAndFlush) +// manages its own clip state across multiple windows. void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) { - // Save and set clip rect int32_t savedClipX = d->clipX; int32_t savedClipY = d->clipY; int32_t savedClipW = d->clipW; @@ -969,19 +1236,18 @@ void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, con setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h); - // Outer raised border — 4px, 3-shade Motif bevel drawBorderFrame(d, ops, colors, win->x, win->y, win->w, win->h); - // Title bar drawTitleBar(d, ops, font, colors, win); - // Menu bar (if present) if (win->menuBar) { computeMenuBarPositions(win, font); drawMenuBar(d, ops, font, colors, win); } - // Inner sunken bevel around content area + // Inner sunken bevel frames the content area (and scrollbars if present). + // The bevel extends around scrollbar space so the entire inset area looks + // recessed, which is the Motif convention for content wells. int32_t innerX = win->x + win->contentX - CHROME_INNER_BORDER; int32_t innerY = win->y + win->contentY - CHROME_INNER_BORDER; int32_t innerW = win->contentW + CHROME_INNER_BORDER * 2; @@ -1013,13 +1279,24 @@ void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, con // ============================================================ // wmDrawContent // ============================================================ +// +// Blits the window's persistent content buffer to the display backbuffer, +// clipped to the current dirty rect. This is the payoff of the per-window +// content buffer architecture: the app's rendered output is always available +// in contentBuf, so we can composite it at any time without calling back +// into the app. This eliminates expose/repaint events entirely. +// +// The blit uses direct memcpy rather than going through the rectCopy +// drawing primitive, because we've already computed the exact intersection +// and don't need the general-purpose clip logic. On 486/Pentium, memcpy +// compiles to rep movsd which saturates the memory bus — no further +// optimization is possible at this level. void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo) { if (__builtin_expect(!win->contentBuf, 0)) { return; } - // Calculate intersection of content area with clip rect RectT contentRect; contentRect.x = win->x + win->contentX; contentRect.y = win->y + win->contentY; @@ -1032,7 +1309,6 @@ void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT * return; } - // Direct blit — we already know the exact intersection, skip clipRect overhead int32_t bpp = ops->bytesPerPixel; int32_t srcX = isect.x - contentRect.x; int32_t srcY = isect.y - contentRect.y; @@ -1052,6 +1328,23 @@ void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT * // ============================================================ // wmDrawMinimizedIcons // ============================================================ +// +// Draws the minimized window icon strip at the bottom of the screen. +// For each minimized window, draws a beveled ICON_TOTAL_SIZE square +// containing either: +// 1. A loaded icon image (scaled to fit via drawScaledRect) +// 2. A live thumbnail of the window's content buffer (also scaled) +// 3. A grey fill if neither is available +// +// The live thumbnail approach (option 2) is a DESQview/X homage — DV/X +// showed miniature window contents in its icon/task view. The thumbnail +// is rendered from the existing content buffer, so no extra rendering +// pass is needed. contentDirty tracks whether the content has changed +// since the last icon refresh. +// +// Icons are drawn before windows in the compositing loop (painter's +// algorithm) so that any windows positioned over the icon strip +// correctly occlude the icons. void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const WindowStackT *stack, const RectT *clipTo) { int32_t iconIdx = 0; @@ -1130,9 +1423,31 @@ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colo // ============================================================ // wmHitTest // ============================================================ +// +// Determines which window (and which part of that window) is under the +// mouse cursor. Walks the stack front-to-back (highest index = frontmost) +// so the first hit wins, correctly handling overlapping windows. +// +// Hit part codes: +// 0 = content area (pass events to app) +// 1 = title bar (initiate drag) +// 2 = close gadget (close or system menu) +// 3 = resize edge (initiate resize, edge flags in wmResizeEdgeHit) +// 4 = menu bar (open menu dropdown) +// 5 = vertical scroll (scrollbar interaction) +// 6 = horiz scroll (scrollbar interaction) +// 7 = minimize gadget (minimize window) +// 8 = maximize gadget (maximize/restore toggle) +// +// The test order matters: gadgets are tested before the title bar so that +// clicks on close/min/max aren't consumed as drag initiations. Scrollbars +// are tested before resize edges so that the scrollbar area (which overlaps +// the inner border) doesn't trigger a resize. Content is tested last; +// if the point is inside the frame but doesn't match any specific part, +// it falls through to hitPart=1 (title/chrome), which is correct for +// clicks on the border face between the outer and inner bevels. int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hitPart) { - // Walk stack top-to-bottom (front-to-back) for (int32_t i = stack->count - 1; i >= 0; i--) { const WindowT *win = stack->windows[i]; @@ -1243,6 +1558,10 @@ int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hi // ============================================================ // wmInit // ============================================================ +// +// Initializes the window stack to empty state. All tracking indices are set +// to -1 (sentinel for "no active operation"). The stack itself is zeroed +// which sets count=0 and all window pointers to NULL. void wmInit(WindowStackT *stack) { memset(stack, 0, sizeof(*stack)); @@ -1256,6 +1575,20 @@ void wmInit(WindowStackT *stack) { // ============================================================ // wmMaximize // ============================================================ +// +// Maximizes a window to fill the screen (or up to maxW/maxH if constrained). +// The pre-maximize geometry is saved in preMax* fields so wmRestore can +// return the window to its original position and size. +// +// The content buffer must be reallocated because the content area changes +// size. After reallocation, onResize notifies the app of the new dimensions, +// then onPaint requests a full repaint into the new buffer. This is +// synchronous — the maximize completes in one frame, avoiding flicker. +// +// Both old and new positions are dirtied: old to expose what was behind the +// window at its previous size, new to paint the window at its maximized size. +// When maximizing to full screen, the old rect is a subset of the new one, +// so dirtyListMerge will collapse them into a single full-screen rect. void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) { (void)stack; @@ -1264,16 +1597,13 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT return; } - // Save current geometry win->preMaxX = win->x; win->preMaxY = win->y; win->preMaxW = win->w; win->preMaxH = win->h; - // Mark old position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); - // Compute effective maximum size int32_t newW = (win->maxW < 0) ? d->width : DVX_MIN(win->maxW, d->width); int32_t newH = (win->maxH < 0) ? d->height : DVX_MIN(win->maxH, d->height); @@ -1296,7 +1626,6 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT win->contentDirty = true; } - // Mark new position dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); } @@ -1304,18 +1633,27 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT // ============================================================ // wmMinimize // ============================================================ +// +// Minimizes a window: marks it minimized so it's skipped during compositing +// (except for icon drawing) and moves focus to the next available window. +// The window's geometry and content buffer are preserved — no reallocation +// is needed since the window retains its size and will be restored to the +// same position. +// +// Focus is moved to the topmost non-minimized window by walking the stack +// from top down. If no such window exists, focusedIdx becomes -1 (no focus). +// The dirtied area covers the window's full frame so the compositor repaints +// the exposed region behind it. void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) { if (win->minimized) { return; } - // Mark window area dirty dirtyListAdd(dl, win->x, win->y, win->w, win->h); win->minimized = true; - // Find next non-minimized window to focus for (int32_t i = stack->count - 1; i >= 0; i--) { if (stack->windows[i]->visible && !stack->windows[i]->minimized) { wmSetFocus(stack, dl, i); @@ -1330,6 +1668,12 @@ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) { // ============================================================ // wmMinimizedIconHit // ============================================================ +// +// Tests whether a mouse click landed on a minimized window icon. Returns +// the stack index of the hit window (NOT the icon index) so the caller can +// directly use it with wmRestoreMinimized. The icon index is computed +// internally to get the position, but the stack index is what the caller +// needs to identify the window. int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t mx, int32_t my) { int32_t iconIdx = 0; @@ -1359,18 +1703,31 @@ int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t // ============================================================ // wmMinWindowSize // ============================================================ +// +// Computes the minimum allowed window size based on the window's current +// chrome configuration. This is dynamic rather than a fixed constant because +// the minimum depends on which optional chrome elements are present: menu +// bars add minimum width (must show all labels), scrollbars add minimum +// height (must fit arrow buttons + minimum thumb), and the title bar gadget +// set varies between resizable and non-resizable windows. +// +// The minimum width is the larger of: +// - Title bar minimum: close + 1 char of title + min + (max if resizable) +// - Menu bar minimum: must show all menu labels (if present) +// The minimum height accounts for top chrome + bottom chrome + 1px content +// + menu bar (if present) + scrollbar space (if present). +// +// This function is called on every resize move event, so it must be cheap. +// No allocations, no string operations — just arithmetic on cached values. static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { - // Title bar: close gadget + padding + 1 char of title + padding int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2; int32_t gadgetPad = GADGET_PAD; int32_t charW = FONT_CHAR_WIDTH; - // Minimum title bar width: close gadget + 1 char + minimize gadget + padding int32_t titleMinW = gadgetPad + gadgetS + gadgetPad + charW + gadgetPad + gadgetS + gadgetPad; if (win->resizable) { - // Add maximize gadget + padding titleMinW += gadgetS + gadgetPad; } @@ -1455,32 +1812,42 @@ static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { // ============================================================ // wmRaiseWindow // ============================================================ +// +// Moves a window to the top of the z-order stack. Implemented by shifting +// all windows above it down by one slot and placing the raised window at +// count-1. This is O(N) but N <= 64 and raise only happens on user clicks, +// so the cost is negligible. +// +// The window area is dirtied because its position in the paint order changed: +// it was previously occluded by windows above it, and now it's on top. +// The compositor will repaint the affected region with the correct z-order. +// +// focusedIdx and dragWindow are index-based references into the stack array, +// so they must be adjusted when entries shift. Any index above the raised +// window's old position decreases by 1 (entries shifted down); the raised +// window itself moves to count-1. void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) { if (idx < 0 || idx >= stack->count - 1) { - return; // already on top or invalid + return; } WindowT *win = stack->windows[idx]; - // Shift everything above it down for (int32_t i = idx; i < stack->count - 1; i++) { stack->windows[i] = stack->windows[i + 1]; } stack->windows[stack->count - 1] = win; - // Mark the window area dirty since z-order changed dirtyListAdd(dl, win->x, win->y, win->w, win->h); - // Update focusedIdx if it was affected if (stack->focusedIdx == idx) { stack->focusedIdx = stack->count - 1; } else if (stack->focusedIdx > idx) { stack->focusedIdx--; } - // Update dragWindow if affected if (stack->dragWindow == idx) { stack->dragWindow = stack->count - 1; } else if (stack->dragWindow > idx) { @@ -1492,6 +1859,15 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) { // ============================================================ // wmReallocContentBuf // ============================================================ +// +// Frees and reallocates the content buffer to match the current contentW/H. +// Called after any geometry change (resize, maximize, restore). The old +// buffer contents are discarded — the caller is expected to trigger +// onResize + onPaint to refill it. This is simpler (and on 486, faster) +// than copying and scaling the old content. +// +// The buffer is initialized to 0xFF (white) so any area the app doesn't +// paint will show a clean background rather than garbage. int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { if (win->contentBuf) { @@ -1521,6 +1897,13 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { // ============================================================ // wmResizeBegin // ============================================================ +// +// Initiates a window resize. Unlike drag (which stores mouse-to-origin +// offset), resize stores the absolute mouse position. wmResizeMove computes +// delta from this position each frame, then resets it — this incremental +// approach avoids accumulating floating-point-like drift and handles the +// case where the resize is clamped (min/max size) without the window +// "catching up" when the mouse returns to range. void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY) { stack->resizeWindow = idx; @@ -1533,10 +1916,19 @@ void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouse // ============================================================ // wmResizeEdgeHit // ============================================================ +// +// Determines which edge(s) of a window the mouse is over. Returns a bitmask +// of RESIZE_LEFT/RIGHT/TOP/BOTTOM flags. Corner hits (e.g. top-left) return +// two flags OR'd together, enabling diagonal resize. +// +// The grab area extends 2 pixels beyond the visual border (CHROME_BORDER_WIDTH +// + 2 = 6px) to make edges easier to grab, especially the 4px-wide border +// which would be frustratingly small on its own. This is a common usability +// trick — the visual border is narrower than the hit zone. int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my) { int32_t edge = RESIZE_NONE; - int32_t border = CHROME_BORDER_WIDTH + 2; // resize grab area + int32_t border = CHROME_BORDER_WIDTH + 2; if (mx >= win->x && mx < win->x + border) { edge |= RESIZE_LEFT; @@ -1571,6 +1963,26 @@ void wmResizeEnd(WindowStackT *stack) { // ============================================================ // wmResizeMove // ============================================================ +// +// Called on each mouse move during a resize operation. Computes the delta +// from the last mouse position (stored in dragOffX/Y) and applies it to +// the appropriate edges based on resizeEdge flags. +// +// Left and top edges are special: resizing from the left/top moves the +// window origin AND changes the size, so both x/y and w/h are adjusted. +// Right and bottom edges only change w/h. Each axis is independently +// clamped to [minW/minH, maxW/maxH]. +// +// After resizing, the content buffer is reallocated and the app is notified +// via onResize + onPaint. dragOffX/Y are reset to the current mouse position +// so the next frame's delta is incremental. This incremental approach means +// that if the resize is clamped (window at minimum size, user still dragging), +// the window stays put and doesn't snap when the mouse re-enters range. +// +// If the user resizes while maximized, the maximized flag is cleared. +// This prevents wmRestore from snapping back to the pre-maximize geometry, +// which would be confusing — the user's manual resize represents their +// new intent. void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_t mouseX, int32_t mouseY) { if (stack->resizeWindow < 0 || stack->resizeWindow >= stack->count) { @@ -1677,6 +2089,11 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_ // ============================================================ // wmRestore // ============================================================ +// +// Restores a maximized window to its pre-maximize geometry. This is the +// inverse of wmMaximize: saves nothing (the pre-max geometry was already +// saved), just restores x/y/w/h from preMax* fields, reallocates the +// content buffer, and triggers repaint. void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) { (void)stack; @@ -1716,18 +2133,24 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT * // ============================================================ // wmRestoreMinimized // ============================================================ +// +// Restores a minimized window: clears the minimized flag, raises the window +// to the top of the z-order, and gives it focus. No content buffer +// reallocation is needed because the buffer was preserved while minimized. +// +// The icon strip area is implicitly dirtied by the raise/focus operations +// and by the window area dirty at the end. The compositor repaints the +// entire icon strip on any dirty rect that intersects it, so icon positions +// are always correct after restore even though we don't dirty the icon +// strip explicitly. void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win) { if (!win->minimized) { return; } - // Dirty the icon area at the bottom of the screen - // (we don't know our exact icon index here, so dirty the entire icon strip) - // The compositeAndFlush will repaint correctly win->minimized = false; - // Raise and focus the restored window for (int32_t i = 0; i < stack->count; i++) { if (stack->windows[i] == win) { wmRaiseWindow(stack, dl, i); @@ -1736,7 +2159,6 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win) { } } - // Dirty the window area dirtyListAdd(dl, win->x, win->y, win->w, win->h); } @@ -1744,6 +2166,22 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win) { // ============================================================ // wmScrollbarClick // ============================================================ +// +// Handles a mouse click on a scrollbar. Determines which sub-region was +// clicked and applies the appropriate scroll action: +// - Arrow buttons: step by 1 unit +// - Thumb: begin drag (stores offset, returns without changing value) +// - Trough above/left of thumb: page up/left +// - Trough below/right of thumb: page down/right +// +// The thumb drag case is special: it doesn't change the scroll value +// immediately but instead records the drag state in the WindowStackT. +// Subsequent mouse moves are handled by wmScrollbarDrag until mouseup +// calls wmScrollbarEnd. +// +// The scroll value is clamped to [min, max] and the scrollbar area is +// dirtied only if the value actually changed, avoiding unnecessary +// repaints when clicking at the min/max limit. void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my) { if (idx < 0 || idx >= stack->count) { @@ -1856,6 +2294,14 @@ void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t // ============================================================ // wmScrollbarDrag // ============================================================ +// +// Handles ongoing thumb drag during scrollbar interaction. Converts the +// mouse pixel position (relative to the track) into a scroll value using +// linear interpolation: value = min + (mousePos * range) / (trackLen - thumbSize). +// +// scrollDragOff is the offset from the mouse to the thumb's leading edge, +// captured at drag start. This keeps the thumb anchored to the original +// click point rather than snapping its top/left edge to the cursor. void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my) { if (stack->scrollWindow < 0 || stack->scrollWindow >= stack->count) { @@ -1931,6 +2377,16 @@ void wmScrollbarEnd(WindowStackT *stack) { // ============================================================ // wmSetIcon // ============================================================ +// +// Loads an icon image from disk and converts it to the display's pixel +// format for fast blitting during minimized icon rendering. stb_image +// handles format decoding (PNG, BMP, etc.) and the RGB->display format +// conversion happens once at load time, so drawScaledRect can blit +// directly without per-pixel format conversion during compositing. +// +// The icon is stored at its original resolution; drawScaledRect handles +// scaling to ICON_SIZE at draw time. This avoids quality loss from +// double-scaling if the icon is a different size than ICON_SIZE. int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d) { int imgW; @@ -1987,6 +2443,12 @@ int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d) { // ============================================================ // wmSetFocus // ============================================================ +// +// Transfers keyboard focus from the current window to a new one. Only the +// title bar areas of the old and new windows are dirtied (not the entire +// windows), because focus only changes the title bar color (active vs +// inactive). This is a significant optimization during click-to-focus: +// painting two title bars is much cheaper than repainting two entire windows. void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) { if (idx < 0 || idx >= stack->count) { @@ -2014,13 +2476,14 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) { } -// ============================================================ -// wmSetTitle -// ============================================================ - // ============================================================ // wmCreateMenu // ============================================================ +// +// Allocates a standalone menu for use as a context menu (right-click popup). +// Unlike menu bar menus which are embedded in MenuBarT, context menus are +// independently allocated and freed with wmFreeMenu. calloc ensures all +// fields start zeroed (no items, no accel keys). MenuT *wmCreateMenu(void) { MenuT *m = (MenuT *)calloc(1, sizeof(MenuT)); @@ -2031,6 +2494,10 @@ MenuT *wmCreateMenu(void) { // ============================================================ // wmFreeMenu // ============================================================ +// +// Frees a standalone context menu and all its submenus recursively. +// Unlike freeMenuRecursive (which only frees submenu children because the +// top-level struct is embedded), this also frees the root MenuT itself. void wmFreeMenu(MenuT *menu) { if (!menu) { @@ -2051,6 +2518,10 @@ void wmFreeMenu(MenuT *menu) { // ============================================================ // wmSetTitle // ============================================================ +// +// Updates a window's title text and dirties only the title bar area. The +// dirty rect is precisely the title bar strip (border width + title height), +// avoiding a full-window repaint for what is purely a chrome change. void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) { strncpy(win->title, title, MAX_TITLE_LEN - 1); @@ -2067,6 +2538,23 @@ void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) { // ============================================================ // wmUpdateContentRect // ============================================================ +// +// Recomputes the content area position and dimensions from the window's +// frame dimensions, accounting for all chrome: outer border, title bar, +// optional menu bar, inner border, and optional scrollbars. +// +// The content rect is expressed as an offset (contentX/Y) from the window +// origin plus width/height. This is relative to the window, not the screen, +// so it doesn't change when the window is dragged. +// +// Scrollbar positions are also computed here because they depend on the +// content area geometry. The order matters: vertical scrollbar steals from +// contentW first, then horizontal scrollbar steals from contentH, then +// the vertical scrollbar's length is adjusted to account for the horizontal +// scrollbar's presence. This creates the standard L-shaped layout where the +// scrollbars meet at the bottom-right corner with a small dead zone between +// them (the corner square where both scrollbars would overlap is simply not +// covered — it shows the window face color from drawBorderFrame). void wmUpdateContentRect(WindowT *win) { int32_t topChrome = CHROME_TOTAL_TOP; @@ -2080,7 +2568,6 @@ void wmUpdateContentRect(WindowT *win) { win->contentW = win->w - CHROME_TOTAL_SIDE * 2; win->contentH = win->h - topChrome - CHROME_TOTAL_BOTTOM; - // Account for scrollbars if (win->vScroll) { win->contentW -= SCROLLBAR_WIDTH; win->vScroll->x = win->contentX + win->contentW; @@ -2094,13 +2581,12 @@ void wmUpdateContentRect(WindowT *win) { win->hScroll->y = win->contentY + win->contentH; win->hScroll->length = win->contentW; - // If both scrollbars, adjust vertical scrollbar length + // Vertical scrollbar must stop short of the horizontal scrollbar if (win->vScroll) { win->vScroll->length = win->contentH; } } - // Clamp to non-negative if (win->contentW < 0) { win->contentW = 0; } if (win->contentH < 0) { win->contentH = 0; } } diff --git a/dvx/dvxWm.h b/dvx/dvxWm.h index 04a47be..f66c405 100644 --- a/dvx/dvxWm.h +++ b/dvx/dvxWm.h @@ -1,106 +1,170 @@ // dvx_wm.h — Layer 4: Window manager for DVX GUI +// +// Manages the window lifecycle, Z-order stack, chrome drawing, hit testing, +// and interactive operations (drag, resize, scroll). This layer bridges the +// low-level drawing/compositing layers below with the high-level application +// API above. +// +// Design philosophy: the WM owns window geometry and chrome, but the +// window's content is owned by the application (via callbacks or the widget +// system). This separation means the WM can move, resize, and repaint +// window frames without involving the application at all — only content +// changes trigger application callbacks. +// +// All WM operations that change visible screen state accept a DirtyListT* +// so they can mark affected regions for compositor repaint. This push-based +// dirty tracking avoids the need for a full-screen diff each frame. #ifndef DVX_WM_H #define DVX_WM_H #include "dvxTypes.h" -// Initialize window stack +// Zero the window stack. Must be called before any other wm function. void wmInit(WindowStackT *stack); -// Create a new window and add it to the stack (raised to top) +// Allocate a new WindowT, initialize its geometry and content buffer, and +// push it to the top of the Z-order stack. The returned window has default +// callbacks (all NULL) — the caller should set onPaint/onKey/etc. before +// the next event loop iteration. WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable); -// Destroy a window and remove from stack +// Free the window's content buffer and all attached resources (menu bar, +// scrollbars, widget tree), then remove it from the stack and dirty the +// region it occupied. void wmDestroyWindow(WindowStackT *stack, WindowT *win); -// Raise a window to the top of the stack +// Move window at stack index idx to the top of the Z-order. Dirties both +// the old and new top positions so overlapping windows get repainted. void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx); -// Set focus to a window (by stack index) +// Transfer keyboard focus to the window at stack index idx. Unfocuses the +// previously focused window and dirties both title bars (since active/ +// inactive title colors differ). void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx); -// Recalculate content area geometry from frame dimensions +// Recompute contentX/Y/W/H from the window's outer frame dimensions, +// accounting for chrome borders, title bar, menu bar, and scrollbars. +// Must be called after any change to frame size or chrome configuration +// (e.g. adding a menu bar or scrollbar). void wmUpdateContentRect(WindowT *win); -// Reallocate content buffer after resize +// Reallocate the per-window content backbuffer to match the current +// contentW/H. Returns 0 on success, -1 if allocation fails. The old +// buffer contents are lost — the caller should trigger a full repaint +// via onPaint after a successful realloc. int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d); -// Add a menu bar to a window +// Allocate and attach a menu bar to a window. Adjusts the content area +// to make room for the menu bar (CHROME_MENU_HEIGHT pixels). Only one +// menu bar per window is supported. MenuBarT *wmAddMenuBar(WindowT *win); -// Add a menu to a menu bar +// Append a dropdown menu to the menu bar. Returns the MenuT to populate +// with items. The label supports & accelerator markers (e.g. "&File"). MenuT *wmAddMenu(MenuBarT *bar, const char *label); -// Add an item to a menu +// Append a clickable item to a menu. The id is passed to the window's +// onMenu callback when the item is selected. void wmAddMenuItem(MenuT *menu, const char *label, int32_t id); -// Add a checkbox item to a menu +// Add a checkbox-style item. Check state is toggled on click and +// rendered with a checkmark glyph. void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked); -// Add a radio item to a menu (radio group = consecutive radio items) +// Add a radio-style item. Radio groups are defined implicitly by +// consecutive radio items in the same menu — selecting one unchecks +// the others in the group. void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked); -// Add a separator to a menu +// Insert a horizontal separator line. Separators are not interactive. void wmAddMenuSeparator(MenuT *menu); -// Add a submenu item (returns the child MenuT to populate, or NULL on failure) +// Create a cascading submenu attached to the parent menu. Returns the +// child MenuT to populate, or NULL if MAX_MENU_ITEMS is exhausted. +// The child MenuT is heap-allocated and freed when the parent window +// is destroyed. MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label); -// Add a vertical scrollbar to a window +// Attach a vertical scrollbar to the right edge of the window's content +// area. Shrinks contentW by SCROLLBAR_WIDTH pixels to make room. The +// scrollbar's value range and page size determine thumb proportions. ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize); -// Add a horizontal scrollbar to a window +// Attach a horizontal scrollbar to the bottom edge. Shrinks contentH +// by SCROLLBAR_WIDTH pixels. ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize); -// Draw window chrome (frame, title bar, menu bar) clipped to a dirty rect +// Draw the window frame (outer bevel, title bar with text, close/minimize/ +// maximize gadgets, and menu bar if present). clipTo restricts drawing to +// only the intersection with the given dirty rectangle, avoiding redundant +// pixel writes outside the damaged region. void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo); -// Draw window content clipped to a dirty rect +// Blit the window's content backbuffer into the display backbuffer, +// clipped to the dirty rect. This is a pure copy (no drawing) — the +// content was already rendered by the application into contentBuf. void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *clipTo); -// Draw scrollbars for a window +// Draw scrollbars (track, arrows, proportional thumb) for a window. +// Drawn after content so scrollbars overlay the content area edge. void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo); -// Draw minimized window icons at bottom of screen +// Draw icons for all minimized windows along the bottom of the screen. +// Each icon shows a scaled-down preview of the window's content (or a +// default icon) with a beveled border, similar to DESQview/X's icon bar. void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const WindowStackT *stack, const RectT *clipTo); -// Hit test: which part of which window is at screen position (mx, my)? -// Returns stack index or -1 if no window hit -// Sets *hitPart: 0=content, 1=title, 2=close button, 3=resize edge, -// 4=menu bar, 5=vscroll, 6=hscroll, 7=minimize, 8=maximize +// Determine which window and which part of that window is under the given +// screen coordinates. Iterates the stack front-to-back (highest Z first) +// so the topmost window wins. Returns the stack index, or -1 if no window +// was hit (i.e. the desktop). hitPart identifies the chrome region: +// 0=content, 1=title bar, 2=close button, 3=resize edge, +// 4=menu bar, 5=vscroll, 6=hscroll, 7=minimize, 8=maximize int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hitPart); -// Detect which resize edge(s) are hit (returns RESIZE_xxx flags) +// For a point within a window's border zone, determine which edge(s) +// are being targeted for resize. Returns a bitmask of RESIZE_xxx flags. +// Corner hits combine two edges (e.g. RESIZE_LEFT | RESIZE_TOP). int32_t wmResizeEdgeHit(const WindowT *win, int32_t mx, int32_t my); -// Handle window drag (call each mouse move during drag) +// Update window position during an active drag. Called on each mouse move +// event while dragWindow is active. Dirties both the old and new window +// positions. The drag offset (mouse position relative to window origin at +// drag start) is applied so the window tracks the mouse smoothly. void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mouseY); -// Handle window resize (call each mouse move during resize) +// Update window dimensions during an active resize. Enforces MIN_WINDOW_W/H +// and optional maxW/maxH constraints. Reallocates the content buffer if the +// content area size changed, then calls onResize to notify the application. void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_t mouseX, int32_t mouseY); -// Begin dragging a window +// Begin a window drag operation. Records the mouse offset from the window +// origin so the window doesn't jump to the cursor position. void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouseY); -// End dragging +// End the current drag operation. Clears dragWindow state. void wmDragEnd(WindowStackT *stack); -// Begin resizing a window +// Begin a window resize operation. Records which edge(s) are being dragged +// and the initial mouse position for delta computation. void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY); -// End resizing +// End the current resize operation. Clears resizeWindow state. void wmResizeEnd(WindowStackT *stack); // Set window title void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title); -// Handle scrollbar click (arrow buttons, trough, or begin thumb drag) +// Handle an initial click on a scrollbar. Determines what was hit (up/down +// arrows, page trough area, or thumb) and either adjusts the value +// immediately (arrows, trough) or begins a thumb drag operation. void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my); -// Handle ongoing scrollbar thumb drag +// Update the scroll value during an active thumb drag. Maps the mouse +// position along the track to a scroll value proportional to the range. void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my); -// End scrollbar thumb drag +// End an active scrollbar thumb drag. void wmScrollbarEnd(WindowStackT *stack); // Maximize a window (saves current geometry, expands to screen/max bounds) @@ -122,10 +186,14 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win); // Load an icon image for a window (converts to display pixel format) int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d); -// Allocate a standalone menu (for use as a context menu). Free with wmFreeMenu(). +// Allocate a heap-resident MenuT for use as a context menu (right-click). +// Unlike menu bar menus (which are embedded in MenuBarT), context menus +// are standalone allocations because they may be shared across windows or +// attached/detached dynamically. Free with wmFreeMenu(). MenuT *wmCreateMenu(void); -// Free a standalone menu allocated with wmCreateMenu() +// Free a standalone menu allocated with wmCreateMenu(). Also frees any +// heap-allocated submenu children recursively. void wmFreeMenu(MenuT *menu); #endif // DVX_WM_H diff --git a/dvx/platform/dvxPlatform.h b/dvx/platform/dvxPlatform.h index 7470ea8..b23962a 100644 --- a/dvx/platform/dvxPlatform.h +++ b/dvx/platform/dvxPlatform.h @@ -1,8 +1,24 @@ // dvxPlatform.h — Platform abstraction layer for DVX GUI // // All OS-specific and CPU-specific code is isolated behind this -// interface. To port DVX to a new platform, implement a new +// interface. To port DVX to a new platform, implement a new // dvxPlatformXxx.c against this header. +// +// Currently two implementations exist: +// dvxPlatformDos.c — DJGPP/DPMI: real VESA VBE, INT 33h mouse, +// INT 16h keyboard, rep movsd/stosl asm spans +// dvxPlatformLinux.c — SDL2: software rendering to an SDL window, +// used for development and testing on Linux +// +// The abstraction covers five areas: video mode setup, framebuffer +// flushing, optimized memory spans, mouse input, and keyboard input. +// File system operations are minimal (just filename validation) because +// the C standard library handles most file I/O portably. +// +// Design rule: functions in this header must be stateless or manage their +// own internal state. They must not reference AppContextT or any layer +// above dvxTypes.h. This ensures the platform layer can be compiled and +// tested independently. #ifndef DVX_PLATFORM_H #define DVX_PLATFORM_H @@ -11,6 +27,10 @@ // ============================================================ // Keyboard event // ============================================================ +// +// Separating ASCII value from scancode handles the DOS keyboard model +// where extended keys (arrows, F-keys) produce a zero ASCII byte followed +// by a scancode. The platform layer normalizes this into a single struct. typedef struct { int32_t ascii; // ASCII value, 0 for extended/function keys @@ -21,40 +41,68 @@ typedef struct { // System lifecycle // ============================================================ -// One-time platform initialisation (signal handling, etc.) +// One-time platform initialisation. On DOS this installs signal handlers +// for clean shutdown on Ctrl+C/Ctrl+Break. On Linux this initializes SDL. void platformInit(void); -// Cooperative multitasking yield (give up CPU when idle) +// Cooperative yield — give up the CPU timeslice when the event loop has +// nothing to do. On DOS this calls __dpmi_yield() to be friendly to +// multitaskers (Windows 3.x, OS/2, DESQview). On Linux this calls +// SDL_Delay(1) to avoid busy-spinning at 100% CPU. void platformYield(void); // ============================================================ // Video // ============================================================ -// Initialise video mode, map framebuffer, allocate backbuffer. -// Fills in all DisplayT fields on success. Returns 0/-1. +// Probe for a suitable video mode, enable it, map the framebuffer, and +// allocate the system RAM backbuffer. On DOS this involves VBE BIOS calls +// and DPMI physical memory mapping. On Linux this creates an SDL window +// and software surface. Fills in all DisplayT fields on success. int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); -// Shut down video — restore text mode, unmap framebuffer, free backbuffer. +// Restore the previous video mode and free all video resources. On DOS +// this restores VGA text mode (mode 3) and frees the DPMI memory mapping. void platformVideoShutdown(DisplayT *d); -// Enumerate available graphics modes. Calls cb(w, h, bpp, userData) for -// each LFB-capable graphics mode found. +// Enumerate LFB-capable graphics modes. The callback is invoked for each +// available mode. Used by videoInit() to find the best match for the +// requested resolution and depth. On Linux, this reports a fixed set of +// common resolutions since SDL doesn't enumerate modes the same way. void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData); -// Set palette entries (8-bit mode). pal is R,G,B triplets. +// Program the VGA/VESA DAC palette registers (8-bit mode only). pal +// points to RGB triplets (3 bytes per entry). On Linux this is a no-op +// since the SDL surface is always truecolor. void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count); // ============================================================ // Framebuffer flush // ============================================================ -// Copy a rectangle from d->backBuf to the display surface (d->lfb). +// Copy a rectangle from the system RAM backbuffer (d->backBuf) to the +// display surface (d->lfb). On DOS this copies to real video memory via +// the LFB mapping — the critical path where PCI bus write speed matters. +// On Linux this copies to the SDL surface, then SDL_UpdateRect is called. +// Each scanline is copied as a contiguous block; rep movsd on DOS gives +// near-optimal bus utilization for aligned 32-bit writes. void platformFlushRect(const DisplayT *d, const RectT *r); // ============================================================ // Optimised memory operations (span fill / copy) // ============================================================ +// +// These are the innermost loops of the renderer — called once per +// scanline of every rectangle fill, blit, and text draw. On DOS they +// use inline assembly: rep stosl for fills (one instruction fills an +// entire scanline) and rep movsd for copies. On Linux they use memset/ +// memcpy which the compiler can auto-vectorize. +// +// Three variants per operation (8/16/32 bpp) because the fill semantics +// differ by depth: 8-bit fills a byte per pixel, 16-bit fills a word +// (must handle odd counts), and 32-bit fills a dword. The copy variants +// differ only in the byte count computation (count * bytesPerPixel). +// drawInit() selects the right function pointers into BlitOpsT at startup. void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count); void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count); @@ -67,36 +115,52 @@ void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count); // Input — Mouse // ============================================================ -// Initialise mouse driver, set movement range, centre cursor. +// Initialize the mouse driver and constrain movement to the screen bounds. +// On DOS this calls INT 33h functions to detect the mouse, set the X/Y +// range, and center the cursor. On Linux this initializes SDL mouse state. void platformMouseInit(int32_t screenW, int32_t screenH); -// Read current mouse position and button state. +// Poll the current mouse state. Buttons is a bitmask: bit 0 = left, +// bit 1 = right, bit 2 = middle. Polling (rather than event-driven +// callbacks) is the natural model for a cooperative event loop — the +// main loop polls once per frame and compares with the previous state +// to detect press/release edges. void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons); // ============================================================ // Input — Keyboard // ============================================================ -// Return current modifier flags (BIOS shift-state format: -// bits 0-1 = shift, bit 2 = ctrl, bit 3 = alt). +// Return the current modifier key state in BIOS shift-state format: +// bits 0-1 = either shift, bit 2 = ctrl, bit 3 = alt. On DOS this +// reads the BIOS data area at 0040:0017. On Linux this queries SDL +// modifier state and translates to the same bit format. int32_t platformKeyboardGetModifiers(void); -// Read the next key from the keyboard buffer. -// Returns true if a key was available, false if the buffer was empty. -// Normalises extended-key markers (e.g. 0xE0 → 0). +// Non-blocking read of the next key from the keyboard buffer. Returns +// true if a key was available. On DOS this uses INT 16h AH=11h (check) +// + AH=10h (read). Extended keys (0xE0 prefix from enhanced keyboard) +// are normalized by zeroing the ASCII byte so the scancode identifies +// them unambiguously. bool platformKeyboardRead(PlatformKeyEventT *evt); -// Map a scan code to its Alt+letter ASCII character. -// Returns the lowercase letter if the scan code corresponds to an -// Alt+letter/digit combo, or 0 if it does not. +// Translate an Alt+key scancode to its corresponding ASCII character. +// When Alt is held, DOS doesn't provide the ASCII value — only the +// scancode. This function contains a lookup table mapping scancodes +// to their unshifted letter/digit. Returns 0 for scancodes that don't +// map to a printable character (e.g. Alt+F1). char platformAltScanToChar(int32_t scancode); // ============================================================ // File system // ============================================================ -// Validate a filename for the current platform. -// Returns NULL if valid, or a human-readable error string if invalid. +// Validate a filename against platform-specific rules. On DOS this +// enforces 8.3 naming (no long filenames), checks for reserved device +// names (CON, PRN, etc.), and rejects characters illegal in FAT filenames. +// On Linux the rules are much more permissive (just no slashes or NUL). +// Returns NULL if the filename is valid, or a human-readable error string +// describing why it's invalid. Used by the file dialog's save-as validation. const char *platformValidateFilename(const char *name); #endif // DVX_PLATFORM_H diff --git a/dvx/platform/dvxPlatformDos.c b/dvx/platform/dvxPlatformDos.c index e475d3b..f184164 100644 --- a/dvx/platform/dvxPlatformDos.c +++ b/dvx/platform/dvxPlatformDos.c @@ -2,6 +2,29 @@ // // All BIOS calls, DPMI functions, port I/O, inline assembly, and // DOS-specific file handling are isolated in this single file. +// +// This file is the ONLY place where DJGPP headers (dpmi.h, go32.h, +// sys/nearptr.h, etc.) appear. Every other DVX module calls through +// the dvxPlatform.h interface, so porting to a new OS (Linux/SDL, +// Win32, or bare-metal ARM) requires replacing only this file and +// nothing else. The abstraction covers five domains: +// 1. VESA VBE video init / mode set / LFB mapping +// 2. Backbuffer-to-LFB flush using rep movsl +// 3. Span fill/copy primitives using inline asm (rep stosl / rep movsl) +// 4. Mouse input via INT 33h driver +// 5. Keyboard input via BIOS INT 16h +// +// Why BIOS INT 16h for keyboard instead of direct port I/O (scancode +// reading from port 0x60): BIOS handles typematic repeat, keyboard +// translation tables, and extended key decoding. Direct port I/O would +// require reimplementing all of that, and the DPMI host already hooks +// IRQ1 to feed the BIOS buffer. The BIOS approach is simpler and more +// portable across emulators (DOSBox, 86Box, PCem all handle it correctly). +// +// Why INT 33h for mouse: same rationale — the mouse driver handles +// PS/2 and serial mice transparently, and every DOS emulator provides +// a compatible driver. Polling via function 03h avoids the complexity +// of installing a real-mode callback for mouse events. #include "dvxPlatform.h" #include "../dvxPalette.h" @@ -12,6 +35,7 @@ #include #include +// DJGPP-specific headers — this is the ONLY file that includes these #include #include #include @@ -30,6 +54,10 @@ static int32_t setVesaMode(uint16_t mode); // Alt+key scan code to ASCII lookup table (indexed by BIOS scan code). // INT 16h returns these scan codes with ascii=0 for Alt+key combos. +// Using a 256-byte lookup table instead of a switch or if-chain because +// this is called on every keypress and the table fits in a single cache +// line cluster. The designated initializer syntax leaves all other +// entries as zero, which is the "no mapping" sentinel. static const char sAltScanToAscii[256] = { // Alt+letters [0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r', @@ -49,6 +77,25 @@ static const char sAltScanToAscii[256] = { // ============================================================ // findBestMode // ============================================================ +// +// Enumerates all VESA VBE modes and selects the best match for the +// requested resolution and color depth using a scoring algorithm. +// +// The approach is: call VBE function 0x4F00 to get the controller +// info block (which contains a pointer to the mode list), then call +// VBE function 0x4F01 for each mode to get its attributes. Each mode +// is scored by getModeInfo() and the highest-scoring mode wins. +// +// Why scoring instead of exact-match: real VESA BIOSes vary wildly in +// what modes they expose. Some have 640x480x16 but not x32; some only +// have 800x600. The scoring heuristic picks the closest usable mode +// rather than failing outright if the exact requested mode is absent. +// +// All VBE info block reads use DJGPP's far pointer API (_farpeekb/w/l) +// to access the DPMI transfer buffer (__tb), which lives in the first +// 1MB of address space (conventional memory). VBE BIOS calls use +// real-mode interrupts via __dpmi_int(), so all data must pass through +// the transfer buffer. static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d) { __dpmi_regs r; @@ -58,11 +105,14 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref memset(&bestDisplay, 0, sizeof(bestDisplay)); - // Get VBE controller info + // Get VBE controller info — the transfer buffer (__tb) is the DJGPP- + // provided region in conventional memory that real-mode BIOS calls + // can read/write. We split it into seg:off for the INT 10h call. uint32_t infoSeg = __tb >> 4; uint32_t infoOff = __tb & 0x0F; - // Write "VBE2" at transfer buffer to request VBE 2.0 info + // Writing "VBE2" tells the BIOS we want VBE 2.0+ extended info. + // Without this, we'd get VBE 1.x info which lacks LFB addresses. _farpokeb(_dos_ds, __tb + 0, 'V'); _farpokeb(_dos_ds, __tb + 1, 'B'); _farpokeb(_dos_ds, __tb + 2, 'E'); @@ -74,12 +124,14 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref r.x.di = infoOff; __dpmi_int(0x10, &r); + // VBE functions return 0x004F in AX on success. Any other value + // means the function failed or isn't supported. if (r.x.ax != 0x004F) { fprintf(stderr, "VBE: Function 0x4F00 failed (AX=0x%04X)\n", r.x.ax); return -1; } - // Verify VBE signature + // On success the BIOS overwrites "VBE2" with "VESA" in the buffer char sig[5]; for (int32_t i = 0; i < 4; i++) { sig[i] = _farpeekb(_dos_ds, __tb + i); @@ -91,7 +143,9 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref return -1; } - // Check VBE version (need 2.0+) + // VBE 2.0+ is required for LFB (Linear Frame Buffer) support. + // VBE 1.x only supports bank switching, which we explicitly don't + // implement — the complexity isn't worth it for 486+ targets. uint16_t vbeVersion = _farpeekw(_dos_ds, __tb + 4); if (vbeVersion < 0x0200) { fprintf(stderr, "VBE: Version %d.%d too old (need 2.0+)\n", @@ -99,12 +153,15 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref return -1; } - // Get mode list pointer (far pointer at offset 14) + // The mode list is a far pointer (seg:off) at offset 14 in the info + // block. It points to a null-terminated (0xFFFF) array of mode numbers + // in conventional memory. uint16_t modeListOff = _farpeekw(_dos_ds, __tb + 14); uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16); uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff; - // Walk mode list + // Walk the mode list. Cap at 256 to prevent runaway on corrupt BIOS + // data (real hardware rarely has more than ~50 modes). for (int32_t i = 0; i < 256; i++) { uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2); @@ -201,6 +258,27 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void * // ============================================================ // getModeInfo // ============================================================ +// +// Queries VBE mode info (function 0x4F01) for a single mode and +// scores it against the requested parameters. The scoring algorithm: +// +// Base score by bpp: 16-bit=100, 15-bit=90, 32-bit=85, 8-bit=70 +// +20 if bpp matches preferredBpp +// +10 if exact resolution match, -10 if oversize +// -1 (rejected) if mode lacks LFB, is text-mode, is below requested +// resolution, or uses an unsupported bpp (e.g. 24-bit) +// +// 16-bit is preferred over 32-bit because it's twice as fast for +// span fill/copy on a 486/Pentium bus (half the bytes). 15-bit scores +// slightly below 16-bit because some VESA BIOSes report 15bpp modes +// as 16bpp with a dead high bit, causing confusion. 8-bit scores +// lowest because palette management adds complexity. +// +// 24-bit is explicitly rejected (not 8/15/16/32) because its 3-byte +// pixels can't use dword-aligned rep stosl fills without masking. +// +// The physical LFB address is temporarily stored in d->lfb as a raw +// integer cast — it will be properly mapped via DPMI in mapLfb() later. static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { __dpmi_regs r; @@ -218,15 +296,15 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ return; } - // Read mode attributes + // VBE mode attribute word at offset 0: + // bit 7 = LFB available, bit 4 = graphics mode (not text) + // Both are required — we never bank-switch and never want text modes. uint16_t attr = _farpeekw(_dos_ds, __tb + 0); - // Must have LFB support (bit 7) if (!(attr & 0x0080)) { return; } - // Must be a graphics mode (bit 4) if (!(attr & 0x0010)) { return; } @@ -282,7 +360,10 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ d->format.bitsPerPixel = bpp; d->format.bytesPerPixel = (bpp + 7) / 8; - // Read color masks from mode info + // Read the channel mask layout from the VBE mode info block. + // These offsets (31-36) define the bit position and size of each + // color channel. This is essential because the channel layout + // varies: some cards use RGB565, others BGR565, etc. if (bpp >= 15) { int32_t redSize = _farpeekb(_dos_ds, __tb + 31); int32_t redPos = _farpeekb(_dos_ds, __tb + 32); @@ -310,6 +391,33 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ // ============================================================ // mapLfb // ============================================================ +// +// Maps the video card's physical LFB address into the DPMI linear +// address space, then converts it to a near pointer for direct C +// access. +// +// The mapping process has three steps: +// 1. __dpmi_physical_address_mapping() — asks the DPMI host to +// create a linear address mapping for the physical framebuffer. +// This is necessary because DPMI runs in protected mode with +// paging; physical addresses aren't directly accessible. +// 2. __dpmi_lock_linear_region() — pins the mapped pages so they +// can't be swapped out. The LFB is memory-mapped I/O to the +// video card; paging it would be catastrophic. +// 3. __djgpp_nearptr_enable() — disables DJGPP's default segment +// limit checking so we can use plain C pointers to access the +// LFB address. Without this, all LFB access would require far +// pointer calls (_farpokeb etc.), which are much slower because +// each one involves a segment register load. +// +// Why near pointers: the performance difference is dramatic. +// platformFlushRect() copies thousands of dwords per frame using +// rep movsl — this only works with near pointers. Far pointer access +// would add ~10 cycles per byte and make 60fps impossible on a 486. +// +// The final pointer calculation adds __djgpp_conventional_base, which +// is the offset DJGPP applies to convert linear addresses to near +// pointer addresses (compensating for the DS segment base). static int32_t mapLfb(DisplayT *d, uint32_t physAddr) { __dpmi_meminfo info; @@ -323,18 +431,17 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr) { return -1; } - // Lock the region to prevent paging __dpmi_meminfo lockInfo; lockInfo.address = info.address; lockInfo.size = fbSize; __dpmi_lock_linear_region(&lockInfo); - // Enable near pointers for direct access if (__djgpp_nearptr_enable() == 0) { fprintf(stderr, "VBE: Failed to enable near pointers\n"); return -1; } + // Convert linear address to near pointer by adding the DS base offset d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base); return 0; @@ -357,6 +464,26 @@ char platformAltScanToChar(int32_t scancode) { // ============================================================ // platformFlushRect // ============================================================ +// +// Copies a dirty rectangle from the system RAM backbuffer to the LFB. +// This is the critical path for display updates — the compositor calls +// it once per dirty rect per frame. +// +// Two code paths: +// 1. Full-width: if the rect spans the entire scanline (rowBytes == +// pitch), collapse all rows into a single large rep movsl. This +// avoids per-row loop overhead and is the common case for full- +// screen redraws. +// 2. Partial-width: copy each scanline individually with rep movsl, +// advancing src/dst by pitch (not rowBytes) between rows. +// +// rep movsl is used instead of memcpy() because on 486/Pentium, GCC's +// memcpy may not generate the optimal dword-aligned string move, and +// the DJGPP C library's memcpy isn't always tuned for large copies. +// The explicit asm guarantees exactly the instruction sequence we want. +// +// __builtin_expect hints tell GCC to generate branch-free fast paths +// for the common cases (non-zero w/h, no trailing bytes). void platformFlushRect(const DisplayT *d, const RectT *r) { int32_t bpp = d->format.bytesPerPixel; @@ -421,7 +548,9 @@ void platformFlushRect(const DisplayT *d, const RectT *r) { // ============================================================ void platformInit(void) { - // Disable Ctrl+C/Break termination + // Disable Ctrl+C/Break so the user can't accidentally kill the + // GUI while in graphics mode (which would leave the display in + // an unusable state without restoring text mode first). signal(SIGINT, SIG_IGN); } @@ -429,6 +558,13 @@ void platformInit(void) { // ============================================================ // platformKeyboardGetModifiers // ============================================================ +// +// Returns the current modifier key state via INT 16h function 12h +// (enhanced get extended shift flags). The low byte contains: +// bit 0 = right shift, bit 1 = left shift +// bit 2 = ctrl, bit 3 = alt +// The widget system uses these bits for keyboard accelerators +// (Alt+key) and text editing shortcuts (Ctrl+C/V/X). int32_t platformKeyboardGetModifiers(void) { __dpmi_regs r; @@ -444,29 +580,43 @@ int32_t platformKeyboardGetModifiers(void) { // ============================================================ // platformKeyboardRead // ============================================================ +// +// Non-blocking keyboard read using enhanced INT 16h functions. +// +// Uses the "enhanced" functions (10h/11h) rather than the original +// (00h/01h) because the originals can't distinguish between grey +// and numpad arrow keys, and they don't report F11/F12. The enhanced +// functions have been standard since AT-class machines (1984). +// +// The two-step peek-then-read is necessary because function 10h +// (read key) blocks until a key is available — there's no non-blocking +// read in the BIOS API. Function 11h (check key) peeks without +// consuming, letting us poll without blocking the event loop. bool platformKeyboardRead(PlatformKeyEventT *evt) { __dpmi_regs r; - // Check if key is available (INT 16h, enhanced function 11h) + // Peek: function 11h sets ZF if buffer is empty r.x.ax = 0x1100; __dpmi_int(0x16, &r); - // Zero flag set = no key available + // Test the Zero Flag (bit 6 of the flags register) if (r.x.flags & 0x40) { return false; } - // Read the key (INT 16h, enhanced function 10h) + // Consume: function 10h removes the key from the BIOS buffer. + // AH = scan code, AL = ASCII character (0 for extended keys). r.x.ax = 0x1000; __dpmi_int(0x16, &r); evt->scancode = (r.x.ax >> 8) & 0xFF; evt->ascii = r.x.ax & 0xFF; - // Enhanced INT 16h returns ascii=0xE0 for grey/extended keys - // (arrows, Home, End, Insert, Delete, etc. on 101-key keyboards). - // Normalize to 0 so all extended key checks work uniformly. + // Enhanced INT 16h uses 0xE0 as the ASCII byte for grey/extended + // keys (arrows, Home, End, Insert, Delete on 101-key keyboards). + // Normalize to 0 so the rest of the codebase can use a single + // "ascii == 0 means extended key, check scancode" convention. if (evt->ascii == 0xE0) { evt->ascii = 0; } @@ -478,30 +628,42 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) { // ============================================================ // platformMouseInit // ============================================================ +// +// Initializes the INT 33h mouse driver. The mouse driver is a TSR +// (or emulated by the DOS environment) that tracks position and +// buttons independently of the application. +// +// We must set the movement range to match our VESA resolution, +// because the default range may be 640x200 (CGA text mode). +// Without this, mouse coordinates would be wrong or clipped. +// +// The hardware cursor is never shown — DVX composites its own +// software cursor on top of the backbuffer. We only use INT 33h +// for position/button state via polling (function 03h). void platformMouseInit(int32_t screenW, int32_t screenH) { __dpmi_regs r; - // Reset mouse driver + // Function 00h: reset driver, detect mouse hardware memset(&r, 0, sizeof(r)); r.x.ax = 0x0000; __dpmi_int(0x33, &r); - // Set horizontal range + // Function 07h: set horizontal min/max range memset(&r, 0, sizeof(r)); r.x.ax = 0x0007; r.x.cx = 0; r.x.dx = screenW - 1; __dpmi_int(0x33, &r); - // Set vertical range + // Function 08h: set vertical min/max range memset(&r, 0, sizeof(r)); r.x.ax = 0x0008; r.x.cx = 0; r.x.dx = screenH - 1; __dpmi_int(0x33, &r); - // Position cursor at center + // Function 04h: warp cursor to center of screen memset(&r, 0, sizeof(r)); r.x.ax = 0x0004; r.x.cx = screenW / 2; @@ -513,6 +675,16 @@ void platformMouseInit(int32_t screenW, int32_t screenH) { // ============================================================ // platformMousePoll // ============================================================ +// +// Reads current mouse state via INT 33h function 03h. +// Returns: CX=X position, DX=Y position, BX=button state +// (bit 0 = left, bit 1 = right, bit 2 = middle). +// +// Polling is used instead of a callback/event model because the +// DVX event loop already runs at frame rate. Installing a real-mode +// callback for mouse events would add DPMI mode-switch overhead +// on every mickeyed movement, which is wasteful when we only sample +// once per frame anyway. void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { __dpmi_regs r; @@ -530,9 +702,23 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { // ============================================================ // platformSpanCopy8 // ============================================================ +// +// Copies 'count' 8-bit pixels from src to dst using dword-aligned +// rep movsl for the bulk transfer. +// +// All span operations (Copy8/16/32, Fill8/16/32) follow the same +// pattern: align to a dword boundary, do the bulk as rep movsl or +// rep stosl, then handle the remainder. This pattern exists because +// on 486/Pentium, misaligned dword moves incur a 3-cycle penalty per +// access. Aligning first ensures the critical rep loop runs at full +// bus speed. +// +// rep movsl moves 4 bytes per iteration with hardware loop decrement, +// which is faster than a C for-loop — the CPU string move pipeline +// optimizes sequential memory access patterns. void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) { - // Align to 4 bytes + // Align dst to a dword boundary with byte copies while (((uintptr_t)dst & 3) && count > 0) { *dst++ = *src++; count--; @@ -560,9 +746,15 @@ void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) { // ============================================================ // platformSpanCopy16 // ============================================================ +// +// Copies 'count' 16-bit pixels. Since each pixel is 2 bytes, we +// only need to check bit 1 of the address for dword alignment +// (bit 0 is always clear for 16-bit aligned data). A single +// leading pixel copy brings us to a dword boundary, then rep movsl +// copies pixel pairs as dwords. void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) { - // Handle odd leading pixel for dword alignment + // Copy one pixel to reach dword alignment if needed if (((uintptr_t)dst & 2) && count > 0) { *(uint16_t *)dst = *(const uint16_t *)src; dst += 2; @@ -591,6 +783,9 @@ void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) { // ============================================================ // platformSpanCopy32 // ============================================================ +// +// 32-bit pixels are inherently dword-aligned, so no alignment +// preamble is needed — straight to rep movsl. void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) { __asm__ __volatile__ ( @@ -605,6 +800,12 @@ void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) { // ============================================================ // platformSpanFill8 // ============================================================ +// +// Fills 'count' 8-bit pixels with a single color value. +// The 8-bit value is replicated into all four bytes of a dword so +// that rep stosl writes 4 identical pixels per iteration. This is +// 4x faster than byte-at-a-time for large fills (window backgrounds, +// screen clears). void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) { uint8_t c = (uint8_t)color; @@ -639,6 +840,10 @@ void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) { // ============================================================ // platformSpanFill16 // ============================================================ +// +// Fills 'count' 16-bit pixels. Two pixels are packed into a dword +// (low half = first pixel, high half = second pixel) so rep stosl +// writes 2 pixels per iteration. void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) { uint16_t c = (uint16_t)color; @@ -673,6 +878,10 @@ void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) { // ============================================================ // platformSpanFill32 // ============================================================ +// +// 32-bit fill is the simplest case — each pixel is already a dword, +// so rep stosl writes exactly one pixel per iteration with no +// alignment or packing concerns. void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) { __asm__ __volatile__ ( @@ -687,6 +896,19 @@ void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) { // ============================================================ // platformValidateFilename — DOS 8.3 filename validation // ============================================================ +// +// Validates that a filename conforms to DOS 8.3 conventions: +// - Base name: 1-8 chars, extension: 0-3 chars, one dot max +// - No spaces or special characters that DOS can't handle +// - Not a reserved device name (CON, PRN, AUX, NUL, COMn, LPTn) +// +// The reserved name check compares the base name only (before the +// dot), case-insensitive, because DOS treats "CON.TXT" the same +// as the CON device — the extension is ignored for device names. +// +// Returns NULL on success, or a human-readable error string on failure. +// On non-DOS platforms, this function would be replaced with one that +// validates for that platform's filesystem rules. const char *platformValidateFilename(const char *name) { static const char *reserved[] = { @@ -764,6 +986,20 @@ const char *platformValidateFilename(const char *name) { // ============================================================ // platformVideoInit // ============================================================ +// +// Complete video initialization sequence: +// 1. findBestMode() — enumerate VESA modes and pick the best match +// 2. setVesaMode() — actually switch to the chosen mode with LFB +// 3. mapLfb() — DPMI-map the physical framebuffer into linear memory +// 4. Allocate system RAM backbuffer (same size as LFB) +// 5. Set up 8-bit palette if needed +// 6. Initialize clip rect to full display +// +// The backbuffer is allocated in system RAM rather than drawing +// directly to the LFB because: (a) reads from the LFB are extremely +// slow on ISA/VLB/PCI (uncached MMIO), so any compositing that reads +// pixels would crawl; (b) the dirty rect system only flushes changed +// regions, so most of the LFB is never touched per frame. int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { uint16_t bestMode; @@ -833,9 +1069,20 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i // ============================================================ // platformVideoSetPalette // ============================================================ +// +// Programs the VGA DAC palette registers via direct port I/O. +// Port 0x3C8 = write index, port 0x3C9 = data (auto-increments). +// +// The VGA DAC expects 6-bit values (0-63) but our palette stores +// 8-bit values (0-255), hence the >> 2 shift. This is standard +// VGA behavior dating back to the original IBM VGA in 1987. +// +// Direct port I/O is used instead of VBE function 09h (set palette) +// because the VGA DAC ports are faster (no BIOS call overhead) and +// universally compatible — even VBE 3.0 cards still have the standard +// VGA DAC at ports 0x3C8/0x3C9. void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count) { - // Set VGA DAC registers directly via port I/O outportb(0x3C8, (uint8_t)firstEntry); for (int32_t i = 0; i < count; i++) { @@ -850,9 +1097,14 @@ void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t cou // ============================================================ // platformVideoShutdown // ============================================================ +// +// Tears down the graphics mode and restores standard 80x25 text +// mode (BIOS mode 3) so the user gets their DOS prompt back. +// Also frees the backbuffer, palette, and disables near pointers +// (re-enables DJGPP's segment limit checking for safety). void platformVideoShutdown(DisplayT *d) { - // Restore text mode (mode 3) + // INT 10h function 00h, mode 03h = 80x25 color text __dpmi_regs r; memset(&r, 0, sizeof(r)); r.x.ax = 0x0003; @@ -877,6 +1129,12 @@ void platformVideoShutdown(DisplayT *d) { // ============================================================ // platformYield // ============================================================ +// +// Cooperative yield to the DPMI host. In a multitasking DOS +// environment (Windows 3.x DOS box, OS/2 VDM, or DESQview), +// this gives other tasks a chance to run. Under a single-tasking +// DPMI server (CWSDPMI) it's essentially a no-op, but it doesn't +// hurt. Called once per event loop iteration when idle. void platformYield(void) { __dpmi_yield(); @@ -886,6 +1144,10 @@ void platformYield(void) { // ============================================================ // setVesaMode // ============================================================ +// +// Sets a VBE video mode via function 0x4F02. Bit 14 of the mode +// number tells the BIOS to enable the Linear Frame Buffer instead +// of the default banked mode. This is the only mode we support. static int32_t setVesaMode(uint16_t mode) { __dpmi_regs r; diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index 01deee4..103202a 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -1,4 +1,39 @@ // widgetAnsiTerm.c — ANSI BBS terminal emulator widget +// +// Implements a VT100/ANSI-compatible terminal emulator widget designed for +// connecting to BBS systems over the serial link. The terminal uses a +// traditional text-mode cell buffer (character + attribute byte pairs) that +// mirrors the layout of CGA/VGA text mode memory. This representation was +// chosen because: +// 1. It maps directly to the BBS/ANSI art paradigm (CP437 character set, +// 16-color CGA palette, blink attribute) +// 2. Cell-based storage is extremely compact — 2 bytes per cell means an +// 80x25 screen is only 4000 bytes, fitting in L1 cache on a 486 +// 3. Dirty-row tracking via a 32-bit bitmask allows sub-millisecond +// incremental repaints without scanning the entire buffer +// +// The ANSI parser is a 3-state machine (NORMAL → ESC → CSI) that handles +// the subset of sequences commonly used by DOS BBS software: cursor movement, +// screen/line erase, scrolling regions, SGR colors, and a few DEC private modes. +// Full VT100 conformance is explicitly NOT a goal — only sequences actually +// emitted by real BBS systems are implemented. +// +// Scrollback is implemented as a circular buffer of row snapshots. Only +// full-screen scroll operations push lines into scrollback; scroll-region +// operations (used by split-screen chat, status bars) do not, matching +// the behavior users expect from DOS terminal programs. +// +// The widget supports two paint paths: +// - Full paint (widgetAnsiTermPaint): used during normal widget repaints +// - Fast repaint (wgtAnsiTermRepaint): bypasses the widget pipeline +// entirely, rendering dirty rows directly into the window's content +// buffer. This is critical for serial communication where ACK turnaround +// time matters — the fewer milliseconds between receiving data and +// displaying it, the higher the effective throughput. +// +// Communication is abstracted through read/write function pointers, allowing +// the terminal to work with raw serial ports, the secLink encrypted channel, +// or any other byte-oriented transport. #include "widgetInternal.h" @@ -12,6 +47,9 @@ #define ANSI_SB_W 14 #define ANSI_DEFAULT_SCROLLBACK 500 +// Three-state ANSI parser: NORMAL processes printable chars and C0 controls, +// ESC waits for the CSI introducer '[' (or standalone ESC sequences like ESC D/M/c), +// CSI accumulates numeric parameters until a final byte (0x40-0x7E) dispatches. #define PARSE_NORMAL 0 #define PARSE_ESC 1 #define PARSE_CSI 2 @@ -19,7 +57,9 @@ // Default attribute: light gray on black #define ANSI_DEFAULT_ATTR 0x07 -// Attribute byte: bit 7 = blink, bits 6-4 = bg (0-7), bits 3-0 = fg (0-15) +// Attribute byte layout matches CGA text mode exactly so that ANSI art +// designed for DOS renders correctly without any translation. +// Bit 7 = blink, bits 6-4 = bg (0-7), bits 3-0 = fg (0-15) #define ATTR_BLINK_BIT 0x80 #define ATTR_BG_MASK 0x70 #define ATTR_FG_MASK 0x0F @@ -51,7 +91,13 @@ static const uint8_t sCgaPalette[16][3] = { {255, 255, 255}, // 15: bright white }; -// ANSI SGR color index to CGA color index +// ANSI SGR color index to CGA color index. +// ANSI and CGA use different orderings for the base 8 colors. ANSI follows +// the RGB bit-field convention (0=black, 1=red, 2=green, 3=yellow, 4=blue, +// 5=magenta, 6=cyan, 7=white) while CGA uses (0=black, 1=blue, 2=green, +// 3=cyan, 4=red, 5=magenta, 6=brown, 7=light gray). This table maps from +// ANSI SGR color numbers (30-37 minus 30) to CGA palette indices. The +// high bit (bright) is handled separately via the bold attribute. static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 }; @@ -89,6 +135,10 @@ static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t // ============================================================ // // Copy a screen row into the scrollback circular buffer. +// The circular buffer avoids any need for memmove when the buffer fills — +// the head index simply wraps around, overwriting the oldest entry. This +// is O(1) per row regardless of scrollback size, which matters when BBS +// software rapidly dumps text (e.g. file listings, ANSI art). static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { if (!w->as.ansiTerm->scrollback || w->as.ansiTerm->scrollbackMax <= 0) { @@ -118,6 +168,11 @@ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { // Build the packed 16-color palette and cache it in the widget. // Only recomputed when paletteValid is false (first use or // after a display format change). +// +// Caching avoids calling packColor() 16 times per repaint. Since packColor +// involves shifting and masking per the display's pixel format, and the +// terminal repaints rows frequently during data reception, this saves +// significant overhead on a 486 where each function call costs ~30 cycles. static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { if (w->as.ansiTerm->paletteValid) { @@ -137,6 +192,8 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { // ============================================================ static void ansiTermClearSelection(WidgetT *w) { + // Dirty all rows when clearing a selection so the selection highlight + // is erased on the next repaint if (ansiTermHasSelection(w)) { w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; } @@ -166,7 +223,11 @@ static void ansiTermCopySelection(WidgetT *w) { int32_t cols = w->as.ansiTerm->cols; - // Build text from selected cells (strip trailing spaces per line) + // Build text from selected cells (strip trailing spaces per line). + // Trailing spaces are stripped because BBS text mode fills the entire + // row with spaces — without stripping, pasting would include many + // unwanted trailing blanks. Fixed 4KB buffer is sufficient for typical + // terminal selections (80 cols * 50 rows = 4000 chars max). char buf[4096]; int32_t pos = 0; @@ -208,6 +269,10 @@ static void ansiTermCopySelection(WidgetT *w) { // ============================================================ // // Mark rows dirty that are touched by a cell range [startCell, startCell+count). +// Uses a 32-bit bitmask — one bit per row — which limits tracking to the first +// 32 rows. This is fine because standard terminal sizes are 24-25 rows, and +// bitmask operations are single-cycle on the target CPU. The bitmask approach +// is much cheaper than maintaining a dirty rect list for per-row tracking. static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { int32_t cols = w->as.ansiTerm->cols; @@ -235,6 +300,10 @@ static void ansiTermDirtyRow(WidgetT *w, int32_t row) { // ansiTermDeleteLines // ============================================================ +// Delete lines within the scroll region by shifting rows up with memmove. +// The vacated lines at the bottom are filled with the current attribute. +// This is the CSI 'M' (DL) handler. Used by BBS software for scroll-region +// tricks (e.g., split-screen chat windows). static void ansiTermDeleteLines(WidgetT *w, int32_t count) { int32_t cols = w->as.ansiTerm->cols; int32_t bot = w->as.ansiTerm->scrollBot; @@ -274,11 +343,19 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) { // ansiTermDispatchCsi // ============================================================ +// Central CSI dispatcher. After the parser accumulates parameters in the CSI +// state, the final byte triggers dispatch here. Only sequences commonly used +// by BBS software are implemented — exotic VT220+ sequences are silently ignored. +// DEC private modes (ESC[?...) are handled separately since they use a different +// parameter namespace than standard ECMA-48 sequences. static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { int32_t *p = w->as.ansiTerm->params; int32_t n = w->as.ansiTerm->paramCount; // DEC private modes (ESC[?...) + // Mode 6: origin mode (cursor addressing relative to scroll region) + // Mode 7: auto-wrap at right margin + // Mode 25: cursor visibility (DECTCEM) if (w->as.ansiTerm->csiPrivate) { int32_t mode = (n >= 1) ? p[0] : 0; @@ -395,7 +472,10 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'c': // DA - device attributes { if (w->as.ansiTerm->commWrite) { - // Respond as VT100 with advanced video option + // Respond as VT100 with advanced video option (AVO). + // Many BBS door games query DA to detect terminal capabilities. + // Claiming VT100+AVO is the safest response — it tells the remote + // side we support ANSI color without implying VT220+ features. const uint8_t reply[] = "\033[?1;2c"; w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, reply, 7); } @@ -529,6 +609,9 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'Z': // CBT - back tab { + // Move cursor back to the previous tab stop (every 8 columns). + // Uses integer math: ((col-1)/8)*8 rounds down to the nearest + // multiple of 8 strictly before the current position. int32_t col = w->as.ansiTerm->cursorCol; if (col > 0) { @@ -549,12 +632,16 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { if (w->as.ansiTerm->commWrite) { if (mode == 6) { - // CPR — cursor position report: ESC[row;colR (1-based) + // CPR — cursor position report: ESC[row;colR (1-based). + // BBS software uses this for screen-size detection and + // to synchronize cursor positioning in door games. char reply[16]; int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(w->as.ansiTerm->cursorRow + 1), (long)(w->as.ansiTerm->cursorCol + 1)); w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len); } else if (mode == 255) { - // Screen size report + // Custom extension: screen size report (non-standard). + // Allows the remote side to query our terminal dimensions + // without relying on NAWS or other Telnet extensions. char reply[16]; int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm->rows, (long)w->as.ansiTerm->cols); w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len); @@ -566,6 +653,10 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'r': // DECSTBM - set scrolling region { + // Defines the top and bottom rows of the scroll region. Lines + // outside this region are not affected by scroll operations. This + // is heavily used by BBS split-screen chat (status line at top, + // chat scrolling below) and door game scoreboards. int32_t rows = w->as.ansiTerm->rows; int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t bot = (n >= 2 && p[1]) ? p[1] - 1 : rows - 1; @@ -613,6 +704,13 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { // ansiTermEraseDisplay // ============================================================ +// Erase Display (ED): mode 0 = cursor to end, 1 = start to cursor, 2 = all. +// Mode 2 pushes all visible lines to scrollback before clearing, preserving +// content that was on screen — this is what users expect when a BBS sends +// a clear-screen sequence (they can scroll back to see previous content). +// The wasAtBottom check ensures auto-scroll tracking: if the user was +// already viewing the latest content, they stay at the bottom after new +// scrollback lines are added. static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { int32_t cols = w->as.ansiTerm->cols; int32_t rows = w->as.ansiTerm->rows; @@ -669,6 +767,10 @@ static void ansiTermEraseLine(WidgetT *w, int32_t mode) { // ============================================================ // // Fill a range of cells with space + current attribute. +// Uses the current attribute (not default) so that erasing respects the +// currently active background color — this is correct per ECMA-48 and +// matches what BBS software expects (e.g., colored backgrounds that +// persist after a line erase). static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { uint8_t *cells = w->as.ansiTerm->cells; @@ -701,6 +803,11 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { // combined scrollback+screen view. // lineIndex < scrollbackCount → scrollback line // lineIndex >= scrollbackCount → screen line +// +// The unified line index simplifies paint and selection code — they don't +// need to know whether a given line is in scrollback or on screen. The +// circular buffer index computation uses modular arithmetic to map from +// logical scrollback line number to physical buffer position. static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { int32_t cols = w->as.ansiTerm->cols; @@ -742,6 +849,9 @@ static bool ansiTermHasSelection(const WidgetT *w) { // ansiTermInsertLines // ============================================================ +// Insert blank lines at the cursor position within the scroll region by +// shifting rows down with memmove. This is the CSI 'L' (IL) handler, +// the inverse of ansiTermDeleteLines. static void ansiTermInsertLines(WidgetT *w, int32_t count) { int32_t cols = w->as.ansiTerm->cols; int32_t bot = w->as.ansiTerm->scrollBot; @@ -799,6 +909,16 @@ static void ansiTermNewline(WidgetT *w) { // ============================================================ // // Feed one byte through the ANSI parser state machine. +// This is the hot path for data reception — called once per byte received +// from the serial link. The state machine is kept as simple as possible +// (no tables, no indirect calls) because this runs on every incoming byte +// and branch prediction on a 486/Pentium benefits from straightforward +// if/switch chains. +// +// In PARSE_NORMAL, C0 control characters (CR, LF, BS, TAB, FF, BEL, ESC) +// are handled first. All other bytes — including CP437 graphic characters +// in the 0x01-0x1F range that aren't C0 controls, plus 0x80-0xFF — are +// treated as printable and placed at the cursor via ansiTermPutChar. static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { switch (w->as.ansiTerm->parseState) { @@ -814,6 +934,8 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { w->as.ansiTerm->cursorCol--; } } else if (ch == '\t') { + // Advance to next 8-column tab stop using bit-mask trick: + // (col+8) & ~7 rounds up to the next multiple of 8 w->as.ansiTerm->cursorCol = (w->as.ansiTerm->cursorCol + 8) & ~7; if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) { @@ -867,6 +989,11 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { break; case PARSE_CSI: + // CSI parameter accumulation. Parameters are separated by ';'. + // Digits are accumulated into the current parameter slot using + // decimal shift (p*10 + digit). The '?' prefix marks DEC private + // mode sequences which have separate semantics from standard CSI. + // Final bytes (0x40-0x7E) terminate the sequence and dispatch. if (ch == '?') { w->as.ansiTerm->csiPrivate = true; } else if (ch >= '0' && ch <= '9') { @@ -901,6 +1028,17 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { // ============================================================ // // Handle SGR (Select Graphic Rendition) escape sequence. +// Maps ANSI color codes to the CGA attribute byte. The attribute byte +// is packed as (bg<<4)|fg, matching CGA text mode. Bold (code 1) sets +// the high bit of the foreground (giving bright colors), while blink +// (code 5) sets bit 3 of the background nibble (the CGA blink/bright-bg bit). +// +// Multiple SGR codes in a single sequence are processed left-to-right, +// each modifying the running attribute. This handles sequences like +// ESC[1;33;44m (bold yellow on blue) correctly. +// +// Codes 90-97/100-107 (bright colors) are non-standard but widely used +// by BBS software as an alternative to bold+color for explicit bright colors. static void ansiTermProcessSgr(WidgetT *w) { if (w->as.ansiTerm->paramCount == 0) { @@ -977,7 +1115,9 @@ static void ansiTermPasteToComm(WidgetT *w) { return; } - // Transmit clipboard contents, converting \n to \r for the terminal + // Transmit clipboard contents, converting \n to \r for the terminal. + // Terminals expect CR as the line-ending character; the remote system + // will echo back CR+LF if needed. for (int32_t i = 0; i < clipLen; i++) { uint8_t ch = (uint8_t)clip[i]; @@ -1056,6 +1196,11 @@ static void ansiTermScrollDown(WidgetT *w) { // // Scroll the screen up by one line. The top line is pushed into // the scrollback buffer before being discarded. +// +// Scrollback capture only occurs for full-screen scrolls (top=0, +// bot=last row). When a sub-region is scrolling (e.g., a BBS +// split-screen chat window), those lines are NOT added to scrollback +// because they represent transient UI content, not conversation history. static void ansiTermScrollUp(WidgetT *w) { int32_t cols = w->as.ansiTerm->cols; @@ -1121,6 +1266,11 @@ static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t // wgtAnsiTerm // ============================================================ +// Create a new ANSI terminal widget. The cell buffer is allocated as a +// flat array of (char, attr) pairs — rows * cols * 2 bytes. The separate +// heap allocation for AnsiTermDataT (via calloc) keeps the WidgetT union +// small; terminal state is substantial (~100+ bytes of fields plus the +// cell and scrollback buffers) so it's pointed to rather than inlined. WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { if (!parent) { return NULL; @@ -1281,6 +1431,18 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { // wgtAnsiTermPoll // ============================================================ +// Poll the terminal for incoming data and update timers. This should be called +// from the application's main loop at a reasonable frequency. It handles three +// things: +// 1. Text blink timer (500ms) — toggles visibility of cells with the blink +// attribute. Only rows containing blink cells are dirtied, avoiding +// unnecessary repaints of static content. +// 2. Cursor blink timer (250ms) — faster than text blink to feel responsive. +// Only the cursor's row is dirtied. +// 3. Comm read — pulls up to 256 bytes from the transport and feeds them +// through the ANSI parser. The 256-byte chunk size balances between +// responsiveness (smaller = more frequent repaints) and throughput +// (larger = fewer function call overhead per byte). int32_t wgtAnsiTermPoll(WidgetT *w) { VALIDATE_WIDGET(w, WidgetAnsiTermE, 0); @@ -1346,6 +1508,17 @@ int32_t wgtAnsiTermPoll(WidgetT *w) { // no relayout, no other widgets). This keeps ACK turnaround fast // for the serial link. +// Fast repaint: renders dirty rows directly into the window's content buffer, +// completely bypassing the normal widget paint pipeline (widgetOnPaint → +// widgetPaintOne → full tree walk). Returns the number of rows repainted +// and optionally reports the vertical extent via outY/outH so the caller +// can issue a minimal compositor dirty rect. +// +// This exists because the normal paint path clears the entire content area +// and repaints all widgets, which is far too expensive to do on every +// incoming serial byte. With fast repaint, the path from data reception +// to LFB flush is: poll → write → dirtyRows → repaint → compositor dirty, +// keeping the round-trip under 1ms on a Pentium. int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { if (!w || w->type != WidgetAnsiTermE || !w->window) { return 0; @@ -1382,7 +1555,10 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { return 0; } - // Set up display context pointing at the content buffer + // Set up a temporary DisplayT pointing at the window's content buffer + // instead of the backbuffer. This lets us reuse all the drawing + // primitives (drawTermRow, etc.) while rendering directly into the + // window's private content bitmap. DisplayT cd = ctx->display; cd.backBuf = win->contentBuf; cd.width = win->contentW; @@ -1449,6 +1625,10 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { // wgtAnsiTermSetComm // ============================================================ +// Attach communication callbacks to the terminal. The read/write function +// pointers are transport-agnostic — the terminal doesn't care whether +// data comes from a raw UART, the secLink encrypted channel, or a +// socket-based proxy. The ctx pointer is passed through opaquely. void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { VALIDATE_WIDGET_VOID(w, WidgetAnsiTermE); @@ -1462,6 +1642,11 @@ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t // wgtAnsiTermWrite // ============================================================ +// Write raw bytes into the terminal for parsing and display. Any active +// selection is cleared first since the screen content is changing and the +// selection coordinates would become stale. Each byte is fed through the +// ANSI parser individually — the parser maintains state between calls so +// multi-byte sequences split across writes are handled correctly. void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) { return; @@ -1491,6 +1676,10 @@ void widgetAnsiTermDestroy(WidgetT *w) { // widgetAnsiTermCalcMinSize // ============================================================ +// Min size = exact pixel dimensions needed for the grid plus border and +// scrollbar. The terminal is not designed to be resizable — the grid +// dimensions (cols x rows) are fixed at creation time, matching the BBS +// convention of 80x25 or similar fixed screen sizes. void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = w->as.ansiTerm->cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; w->calcMinH = w->as.ansiTerm->rows * font->charHeight + ANSI_BORDER * 2; @@ -1502,7 +1691,11 @@ void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ // // Overlay selection highlighting for a single terminal row. -// Inverts fg/bg for selected cells. +// Selected cells are redrawn with fg/bg swapped (fg becomes bg and vice versa), +// which is the traditional terminal selection highlight method. This avoids +// needing a separate "selection color" and works regardless of the underlying +// cell colors. The swap is done in palette-index space (not pixel space) so +// it's just a few index lookups, keeping the per-cell cost minimal. static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY) { if (!ansiTermHasSelection(w)) { @@ -1552,6 +1745,14 @@ static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, co // Translate keyboard input to ANSI escape sequences and send // via the comm interface. Does nothing if commWrite is NULL. +// Key handling for the terminal. Keyboard input is translated to ANSI +// escape sequences and transmitted via the comm interface. The key codes +// use the BIOS INT 16h convention: extended keys have bit 0x100 set, with +// the scan code in the low byte. This matches the dvxApp keyboard layer. +// +// Ctrl+C has dual behavior: copy selection if one exists, otherwise send +// the ^C control character to the remote (for breaking running programs). +// Ctrl+V pastes clipboard contents to the terminal as keystrokes. void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { // Ctrl+C: copy if selection exists, otherwise send ^C if (key == 0x03 && (mod & KEY_MOD_CTRL)) { @@ -1660,6 +1861,12 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { // // Handle mouse clicks: scrollbar interaction and focus. +// Mouse handling: the text area (left of the scrollbar) supports click-to-select +// with single-click (anchor), double-click (word), and triple-click (line). +// The scrollbar area uses direct hit testing on up/down arrow buttons and +// page-up/page-down regions, with proportional thumb positioning. +// Selection drag is handled externally by the widget event dispatcher via +// sDragTextSelect — on mouse-down we set the anchor and enable drag mode. void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { AppContextT *actx = (AppContextT *)root->userData; const BitmapFontT *font = &actx->font; @@ -1803,6 +2010,18 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) // widgetAnsiTermPaint // ============================================================ +// Full paint: renders the complete terminal widget including border, all text +// rows, selection overlay, and scrollbar. This is called through the normal +// widget paint pipeline (e.g., on window expose or full invalidation). +// For incremental updates during data reception, wgtAnsiTermRepaint is used +// instead — it's much faster since it only repaints dirty rows and skips +// the border/scrollbar. +// +// The terminal renders its own scrollbar rather than using the shared +// widgetDrawScrollbarV because the scrollbar's total/visible/position +// semantics are different — the terminal scrollbar represents scrollback +// lines (historical content above the screen), not a viewport over a +// virtual content area. void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { // Draw sunken bevel border BevelStyleT bevel; diff --git a/dvx/widgets/widgetBox.c b/dvx/widgets/widgetBox.c index 06f46df..bb16fd7 100644 --- a/dvx/widgets/widgetBox.c +++ b/dvx/widgets/widgetBox.c @@ -1,4 +1,22 @@ // widgetBox.c — VBox, HBox, and Frame container widgets +// +// VBox and HBox are the primary layout containers. They have no visual +// representation of their own — they exist purely to arrange children +// vertically or horizontally. The actual layout algorithm lives in +// widgetLayout.c (widgetCalcMinSizeBox / widgetLayoutBox) which handles +// weight-based space distribution, spacing, padding, and alignment. +// +// VBox and HBox are distinguished by a flag (WCLASS_HORIZ_CONTAINER) in +// the class table rather than having separate code. This keeps the layout +// engine unified — the same algorithm works in both orientations by +// swapping which axis is "major" vs "minor". +// +// Frame is a labeled grouping box with a Motif-style beveled border. +// It acts as a VBox for layout purposes (children stack vertically inside +// the frame's padded interior). The title text sits centered vertically +// on the top border line, with a small background-filled gap to visually +// "break" the border behind the title — this is the classic Win3.1/Motif +// group box appearance. #include "widgetInternal.h" @@ -7,6 +25,17 @@ // widgetFramePaint // ============================================================ +// Paint the frame border and optional title. The border is offset down by +// half the font height so the title text can sit centered on the top edge. +// This creates the illusion of the title "interrupting" the border — a +// background-colored rectangle is drawn behind the title to erase the +// border pixels, then the title is drawn on top. +// +// Three border styles are supported: +// FrameFlatE — single-pixel solid color outline +// FrameInE — Motif "groove" (inset bevel: shadow-then-highlight) +// FrameOutE — Motif "ridge" (outset bevel: highlight-then-shadow) +// The groove/ridge are each two nested 1px bevels with swapped colors. void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { int32_t fb = widgetFrameBorderWidth(w); int32_t boxY = w->y + font->charHeight / 2; @@ -69,6 +98,9 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap // wgtFrame // ============================================================ +// Create a Frame container. The title string supports accelerator keys +// (prefixed with '&') which are parsed by accelParse. The title pointer +// is stored directly (not copied) — the caller must ensure it remains valid. WidgetT *wgtFrame(WidgetT *parent, const char *title) { WidgetT *w = widgetAlloc(parent, WidgetFrameE); diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index 0864a5c..bf4ef69 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -1,4 +1,23 @@ // widgetButton.c — Button widget +// +// Standard push button with text label, Motif-style 2px beveled border, +// and press animation. The button uses a two-phase press model: +// - Mouse press: sets pressed=true and stores the widget in sPressedButton. +// The event dispatcher tracks mouse movement — if the mouse leaves the +// button bounds, pressed is cleared (visual feedback), and if it re-enters, +// pressed is re-set. The onClick callback fires only on mouse-up while +// still inside the button. This gives the user a chance to cancel by +// dragging away, matching standard GUI behavior. +// - Keyboard press (Space/Enter): sets pressed=true and stores in +// sKeyPressedBtn. The onClick fires on key-up. This uses a different +// global than mouse press because key and mouse can't happen simultaneously. +// +// The press animation shifts text and focus rect by BUTTON_PRESS_OFFSET (1px) +// down and right, and swaps the bevel highlight/shadow colors to create the +// illusion of the button being pushed "into" the screen. +// +// Text supports accelerator keys via '&' prefix (e.g., "&OK" underlines 'O' +// and Alt+O activates the button). #include "widgetInternal.h" @@ -43,6 +62,9 @@ void widgetButtonSetText(WidgetT *w, const char *text) { // widgetButtonCalcMinSize // ============================================================ +// Min size includes horizontal and vertical padding around the text. The text +// width is computed by textWidthAccel which excludes the '&' accelerator prefix +// character from the measurement. void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = textWidthAccel(font, w->as.button.text) + BUTTON_PAD_H * 2; w->calcMinH = font->charHeight + BUTTON_PAD_V * 2; @@ -82,6 +104,12 @@ void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { // widgetButtonPaint // ============================================================ +// Paint: draws the beveled border, centered text, and optional focus rect. +// When pressed, the bevel colors swap (highlight↔shadow) creating the sunken +// appearance, and the text shifts by BUTTON_PRESS_OFFSET pixels. Disabled +// buttons use the "embossed" text technique (highlight color at +1,+1, then +// shadow color at 0,0) to create a chiseled/etched look — this is the +// standard Windows 3.1 disabled control appearance. void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; diff --git a/dvx/widgets/widgetCanvas.c b/dvx/widgets/widgetCanvas.c index 70094f7..9e04985 100644 --- a/dvx/widgets/widgetCanvas.c +++ b/dvx/widgets/widgetCanvas.c @@ -1,4 +1,25 @@ // widgetCanvas.c — Drawable canvas widget (freehand draw, PNG save/load) +// +// The canvas widget provides a pixel buffer in the display's native pixel +// format that applications can draw into directly. It stores pixels in +// display format (not always RGB) to avoid per-pixel conversion on every +// repaint — the paint function just does a straight rectCopy blit from +// the canvas buffer to the display. This is critical on a 486 where +// per-pixel format conversion during repaint would be prohibitively slow. +// +// The tradeoff is that load/save operations must convert between RGB and +// the display format, but those are one-time costs at I/O time rather +// than per-frame costs. +// +// Drawing operations (dot, line, rect, circle) operate directly on the +// canvas buffer using canvasPutPixel for pixel-level writes. The dot +// primitive draws a filled circle using the pen size, and line uses +// Bresenham's algorithm placing dots along the path. This gives smooth +// freehand drawing with variable pen widths. +// +// Canvas coordinates are independent of widget position — (0,0) is the +// top-left of the canvas content, not the widget. Mouse events translate +// widget-space coordinates to canvas-space by subtracting the border offset. #include "widgetInternal.h" #include "../thirdparty/stb_image.h" @@ -17,11 +38,14 @@ static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uin // ============================================================ -// canvasPutPixel +// canvasGetPixel / canvasPutPixel // ============================================================ // -// Write a single pixel of the given color at dst, respecting the display's -// bytes-per-pixel depth. +// Read/write a single pixel at the given address, respecting the display's +// bytes-per-pixel depth (1=8-bit palette, 2=16-bit hicolor, 4=32-bit truecolor). +// These are inline because they're called per-pixel in tight loops (circle fill, +// line draw) — the function call overhead would dominate on a 486. The bpp +// branch is predictable since it doesn't change within a single draw operation. static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) { if (bpp == 1) { @@ -71,7 +95,11 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) { return; } - // Filled circle via per-row horizontal span + // Filled circle via per-row horizontal span. For each row (dy offset from + // center), compute the horizontal extent using the circle equation + // dx^2 + dy^2 <= r^2. The horizontal half-span is floor(sqrt(r^2 - dy^2)). + // This approach is faster than checking each pixel individually because + // the inner loop just fills a horizontal run — no per-pixel distance check. int32_t r2 = rad * rad; for (int32_t dy = -rad; dy <= rad; dy++) { @@ -86,7 +114,11 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) { int32_t rem = r2 - dy2; int32_t hspan = 0; - // Integer sqrt via Newton's method + // Integer sqrt via Newton's method (Babylonian method). 8 iterations + // is more than enough to converge for any radius that fits in int32_t. + // Using integer sqrt avoids pulling in the FPU which may not be + // present on 486SX systems, and avoids the float-to-int conversion + // overhead even on systems with an FPU. if (rem > 0) { hspan = rad; @@ -127,6 +159,10 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) { // ============================================================ // // Bresenham line from (x0,y0) to (x1,y1), placing dots along the path. +// Each point on the line gets a full pen dot (canvasDrawDot), which means +// lines with large pen sizes are smooth rather than aliased. Bresenham was +// chosen over DDA because it's pure integer arithmetic — no floating point +// needed, which matters on 486SX (no FPU). static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { int32_t dx = x1 - x0; @@ -166,6 +202,10 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32 // ============================================================ // // Reverse of packColor — extract RGB from a display-format pixel. +// Only used during PNG save (wgtCanvasSave) to convert the canvas's +// native-format pixels back to RGB for stb_image_write. The bit-shift +// approach works for 15/16/24/32-bit modes; 8-bit paletted mode uses +// a direct palette table lookup instead. static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b) { if (d->format.bitsPerPixel == 8) { @@ -192,6 +232,12 @@ static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uin // wgtCanvas // ============================================================ +// Create a canvas widget with the specified dimensions. The canvas buffer +// is allocated in the display's native pixel format by walking up the widget +// tree to find the AppContextT (which holds the display format info). This +// tree-walk pattern is necessary because the widget doesn't have direct access +// to the display — only the root widget's userData points to the AppContextT. +// The buffer is initialized to white using spanFill for performance. WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { if (!parent || w <= 0 || h <= 0) { return NULL; @@ -470,6 +516,10 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t } AppContextT *ctx = (AppContextT *)root->userData; + // Use the optimized spanFill (which uses rep stosl on x86) when available, + // falling back to per-pixel writes if the AppContextT can't be reached. + // The spanFill path is ~4x faster for 32-bit modes because it writes + // 4 bytes per iteration instead of going through the bpp switch. if (ctx) { for (int32_t py = y0; py < y1; py++) { ctx->blitOps.spanFill(data + py * pitch + x0 * bpp, color, fillW); @@ -508,6 +558,12 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) { // wgtCanvasLoad // ============================================================ +// Load an image file into the canvas, replacing the current content. +// Uses stb_image for decoding (supports BMP, PNG, JPEG, GIF, etc.). +// The loaded RGB pixels are converted to the display's native pixel format +// during load so that subsequent repaints are just a memcpy. The old buffer +// is freed and replaced with the new one — canvas dimensions change to match +// the loaded image. int32_t wgtCanvasLoad(WidgetT *w, const char *path) { if (!w || w->type != WidgetCanvasE || !path) { return -1; @@ -573,6 +629,9 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path) { // wgtCanvasSave // ============================================================ +// Save the canvas content to a PNG file. Since the canvas stores pixels in +// the display's native format (which varies per video mode), we must convert +// back to RGB before writing. This is the inverse of the load conversion. int32_t wgtCanvasSave(WidgetT *w, const char *path) { if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) { return -1; @@ -687,6 +746,10 @@ void widgetCanvasDestroy(WidgetT *w) { // widgetCanvasCalcMinSize // ============================================================ +// The canvas requests exactly its pixel dimensions plus the sunken bevel +// border. The font parameter is unused since the canvas has no text content. +// The canvas is not designed to scale — it reports its exact size as the +// minimum, and the layout engine should respect that. void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2; @@ -698,6 +761,11 @@ void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetCanvasOnMouse // ============================================================ +// Mouse handler: translates widget-space coordinates to canvas-space (subtracting +// border offset) and invokes the application's mouse callback. The sDrawingCanvas +// global tracks whether this is a drag (mouse was already down on this canvas) +// vs a new click, so the callback can distinguish between starting a new stroke +// and continuing one. void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { (void)root; @@ -727,6 +795,10 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { // widgetCanvasPaint // ============================================================ +// Paint: draws a sunken bevel border then blits the canvas buffer. Because +// the canvas stores pixels in the display's native format, rectCopy is a +// straight memcpy per scanline — no per-pixel conversion needed. This makes +// repaint essentially free relative to the display bandwidth. void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { (void)font; diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index 9f8396d..1ab7dfb 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -1,4 +1,16 @@ // widgetCheckbox.c — Checkbox widget +// +// Classic checkbox: a small box with a sunken bevel (1px) on the left, a text +// label to the right. The check mark is drawn as two diagonal lines forming an +// "X" pattern rather than a traditional checkmark glyph — this is simpler to +// render with drawHLine primitives and matches the DV/X aesthetic. +// +// State management is simple: a boolean 'checked' flag toggles on each click +// or Space/Enter keypress. The onChange callback fires after each toggle so +// the application can respond immediately. +// +// Focus is shown via a dotted rectangle around the label text (not the box), +// matching the Win3.1 convention where the focus indicator wraps the label. #include "widgetInternal.h" @@ -43,6 +55,9 @@ void widgetCheckboxSetText(WidgetT *w, const char *text) { // widgetCheckboxCalcMinSize // ============================================================ +// Min width = box + gap + text. Min height = whichever is taller (the box or +// the font). This ensures the box and text are always vertically centered +// relative to each other regardless of font size. void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + textWidthAccel(font, w->as.checkbox.text); @@ -103,7 +118,11 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit bevel.width = 1; drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel); - // Draw check mark if checked + // Draw check mark if checked. The check is an X pattern drawn as + // two diagonal lines of single pixels. Using 1-pixel drawHLine calls + // instead of a real line drawing algorithm avoids Bresenham overhead + // for what's always a small fixed-size glyph (6x6 pixels). The 3px + // inset from the box edge keeps the mark visually centered. if (w->as.checkbox.checked) { int32_t cx = w->x + 3; int32_t cy = boxY + 3; diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c index d9e2588..23db391 100644 --- a/dvx/widgets/widgetClass.c +++ b/dvx/widgets/widgetClass.c @@ -1,10 +1,49 @@ // widgetClass.c — Widget class vtable definitions +// +// This file implements a C vtable pattern for the widget type system. +// Each widget type has a static const WidgetClassT struct that defines +// its behavior — paint, layout, mouse handling, keyboard handling, etc. +// A master table (widgetClassTable[]) maps WidgetTypeE enum values to +// their class definitions, enabling O(1) dispatch. +// +// Why a vtable approach instead of switch statements: +// 1. Adding a new widget type is purely additive — define a new class +// struct and add one entry to the table. No existing switch +// statements need modification, reducing the risk of forgetting +// a case. +// 2. NULL function pointers indicate "no behavior" naturally. A box +// container has no paint function because the framework paints +// its children directly. A label has no onKey handler because +// it's not interactive. This is cleaner than empty case blocks. +// 3. The flags field combines multiple boolean properties into a +// single bitmask, avoiding per-type if-chains in hot paths +// like hit testing and layout. +// +// Class flags: +// WCLASS_FOCUSABLE — widget can receive keyboard focus (Tab order) +// WCLASS_BOX_CONTAINER — uses the generic box layout engine (VBox/HBox) +// WCLASS_HORIZ_CONTAINER — lays out children horizontally (HBox variant) +// WCLASS_PAINTS_CHILDREN — widget handles its own child painting (TabControl, +// TreeView, ScrollPane, Splitter). The default paint +// walker skips children for these widgets. +// WCLASS_NO_HIT_RECURSE — hit testing stops at this widget instead of +// recursing into children. Used by widgets that +// manage their own internal click regions (TreeView, +// ScrollPane, ListView, Splitter). #include "widgetInternal.h" // ============================================================ // Per-type class definitions // ============================================================ +// +// Containers (VBox, HBox, RadioGroup, TabPage, StatusBar, Toolbar) +// typically have NULL for most function pointers because their +// behavior is handled generically by the box layout engine and +// the default child-walking paint logic. +// +// Leaf widgets (Button, Label, TextInput, etc.) override paint, +// calcMinSize, and usually onMouse/onKey for interactivity. static const WidgetClassT sClassVBox = { .flags = WCLASS_BOX_CONTAINER, @@ -97,6 +136,9 @@ static const WidgetClassT sClassRadio = { .setText = widgetRadioSetText }; +// TextInput and TextArea have destroy functions because they dynamically +// allocate their text buffer and undo buffer. Most other widgets store +// all data inline in the WidgetT union and need no cleanup. static const WidgetClassT sClassTextInput = { .flags = WCLASS_FOCUSABLE, .paint = widgetTextInputPaint, @@ -175,6 +217,9 @@ static const WidgetClassT sClassFrame = { .setText = NULL }; +// Dropdown and ComboBox use paintOverlay to draw their popup lists +// on top of the entire widget tree. This is rendered in a separate +// pass after the main paint, so the popup can overlap sibling widgets. static const WidgetClassT sClassDropdown = { .flags = WCLASS_FOCUSABLE, .paint = widgetDropdownPaint, @@ -227,6 +272,9 @@ static const WidgetClassT sClassSlider = { .setText = NULL }; +// TabControl has both PAINTS_CHILDREN and a custom layout function +// because it needs to show only the active tab page's children and +// position them inside the tab content area (below the tab strip). static const WidgetClassT sClassTabControl = { .flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN, .paint = widgetTabControlPaint, @@ -279,6 +327,13 @@ static const WidgetClassT sClassToolbar = { .setText = NULL }; +// TreeView uses all three special flags: +// PAINTS_CHILDREN — it renders tree items itself (with indentation, +// expand/collapse buttons, and selection highlighting) +// NO_HIT_RECURSE — mouse clicks go to the TreeView widget, which +// figures out which tree item was clicked based on scroll position +// and item Y coordinates, rather than letting the hit tester +// recurse into child TreeItem widgets static const WidgetClassT sClassTreeView = { .flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE, .paint = widgetTreeViewPaint, @@ -292,6 +347,10 @@ static const WidgetClassT sClassTreeView = { .setText = NULL }; +// TreeItem has no paint/mouse/key handlers — it's a data-only node. +// The TreeView parent widget handles all rendering and interaction. +// TreeItem exists as a WidgetT so it can participate in the tree +// structure (parent/child/sibling links) for hierarchical data. static const WidgetClassT sClassTreeItem = { .flags = 0, .paint = NULL, @@ -370,6 +429,9 @@ static const WidgetClassT sClassAnsiTerm = { .setText = NULL }; +// ScrollPane, Splitter: PAINTS_CHILDREN because they clip/position +// children in a custom way; NO_HIT_RECURSE because they manage their +// own scrollbar/divider hit regions. static const WidgetClassT sClassScrollPane = { .flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE, .paint = widgetScrollPanePaint, @@ -412,6 +474,18 @@ static const WidgetClassT sClassSpinner = { // ============================================================ // Class table — indexed by WidgetTypeE // ============================================================ +// +// This array is the central dispatch table for the widget system. +// Indexed by the WidgetTypeE enum, it provides O(1) lookup of any +// widget type's class definition. Every WidgetT stores a pointer +// to its class (w->wclass) set at allocation time, so per-widget +// dispatch doesn't even need to index this table — it's a direct +// pointer dereference through the vtable. +// +// Using C99 designated initializers ensures the array slots match +// the enum values even if the enum is reordered. If a new enum +// value is added without a table entry, it will be NULL, which +// callers check for before dispatching. const WidgetClassT *widgetClassTable[] = { [WidgetVBoxE] = &sClassVBox, diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 29a268d..67778e4 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -1,4 +1,24 @@ // widgetComboBox.c — ComboBox widget (editable text + dropdown list) +// +// Combines a single-line text input with a dropdown list. The text area +// supports full editing (cursor movement, selection, undo, clipboard) via +// the shared widgetTextEditOnKey helper, while the dropdown button opens +// a popup list overlay. +// +// This is a "combo" box in the Windows sense: the user can either type a +// value or select from the list. When an item is selected from the list, +// its text is copied into the edit buffer. The edit buffer is independently +// allocated (malloc'd) so the user can modify the text after selecting. +// +// The popup list is painted as an overlay (widgetComboBoxPaintPopup) that +// renders on top of all other widgets. Popup visibility is coordinated +// through the sOpenPopup global — only one popup can be open at a time. +// The sClosedPopup mechanism prevents click-to-close from immediately +// reopening the popup when the close click lands on the dropdown button. +// +// Text selection supports single-click (cursor placement + drag start), +// double-click (word select), and triple-click (select all). Drag-select +// is tracked via the sDragTextSelect global. #include "widgetInternal.h" @@ -7,6 +27,11 @@ // wgtComboBox // ============================================================ +// Create a combo box. maxLen controls the edit buffer size (0 = default 256). +// Two buffers are allocated: the edit buffer and an undo buffer (for Ctrl+Z +// single-level undo). Selection indices start at -1 (nothing selected). +// Default weight=100 makes combo boxes expand to fill available space in +// a layout container, which is the typical desired behavior. WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) { WidgetT *w = widgetAlloc(parent, WidgetComboBoxE); @@ -56,7 +81,9 @@ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { w->as.comboBox.items = items; w->as.comboBox.itemCount = count; - // Cache max item strlen to avoid recomputing in calcMinSize + // Cache max item string length so calcMinSize doesn't need to re-scan + // the entire item array on every layout pass. Items are stored as + // external pointers (not copied) — the caller owns the string data. int32_t maxLen = 0; for (int32_t i = 0; i < count; i++) { @@ -73,6 +100,8 @@ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { w->as.comboBox.selectedIdx = -1; } + // wgtInvalidate (not wgtInvalidatePaint) triggers a full relayout because + // changing items may change the widget's minimum width wgtInvalidate(w); } @@ -141,6 +170,12 @@ const char *widgetComboBoxGetText(const WidgetT *w) { // widgetComboBoxOnKey // ============================================================ +// Key handling has two modes: when the popup is open, Up/Down navigate the list +// and Enter confirms the selection. When closed, keys go to the text editor +// (via widgetTextEditOnKey) except Down-arrow which opens the popup. This split +// behavior is necessary because the same widget must serve as both a text input +// and a list selector depending on popup state. +// Key codes: 0x48|0x100 = Up, 0x50|0x100 = Down (BIOS scan codes with extended bit). void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { if (w->as.comboBox.open) { if (key == (0x48 | 0x100)) { @@ -304,6 +339,12 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { // widgetComboBoxPaint // ============================================================ +// Paint: two regions side-by-side — a sunken text area (left) and a raised +// dropdown button (right). The text area renders the edit buffer with optional +// selection highlighting (up to 3 text runs: pre-selection, selection, +// post-selection). The dropdown button has a small triangular arrow glyph +// drawn as horizontal lines of decreasing width. When the popup is open, +// the button bevel is inverted (sunken) to show it's active. void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; @@ -338,7 +379,11 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit selHi = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selEnd : w->as.comboBox.selStart; } - // Draw up to 3 runs: before selection, selection, after selection + // Draw up to 3 text runs: before selection (normal colors), selection + // (highlight colors), after selection (normal colors). This avoids + // drawing the entire line twice (once normal, once selected) which + // would be wasteful on slow hardware. The visible selection range + // is clamped to the scrolled viewport. int32_t visSelLo = selLo - off; int32_t visSelHi = selHi - off; diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 75aa6e3..f6bd906 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -1,31 +1,67 @@ // widgetCore.c — Core widget infrastructure (alloc, tree ops, helpers) +// +// This file provides the foundation for the widget tree: allocation, +// parent-child linking, focus management, hit testing, and shared +// utility functions used across multiple widget types. +// +// Widgets form a tree using intrusive linked lists (firstChild/lastChild/ +// nextSibling pointers inside each WidgetT). This is a singly-linked +// child list with a tail pointer for O(1) append. The tree is owned +// by its root, which is attached to a WindowT. Destroying the root +// recursively destroys all descendants. +// +// Memory allocation is plain malloc/free rather than an arena or pool. +// The widget count per window is typically small (tens to low hundreds), +// so the allocation overhead is negligible on target hardware. An arena +// approach was considered but rejected because widgets can be individually +// created and destroyed at runtime (dialog dynamics, tree item insertion), +// which doesn't map cleanly to an arena pattern. #include "widgetInternal.h" // ============================================================ // Global state for drag and popup tracking // ============================================================ +// +// These module-level pointers track ongoing UI interactions that span +// multiple mouse events (drags, popups, button presses). They are global +// rather than per-window because the DOS GUI is single-threaded and only +// one interaction can be active at a time. +// +// Each pointer is set when an interaction begins (e.g. mouse-down on a +// slider) and cleared when it ends (mouse-up). The event dispatcher in +// widgetEvent.c checks these before normal hit testing — active drags +// take priority over everything else. +// +// All of these must be NULLed when the pointed-to widget is destroyed, +// otherwise dangling pointers would cause crashes. widgetDestroyChildren() +// and wgtDestroy() handle this cleanup. bool sDebugLayout = false; -WidgetT *sFocusedWidget = NULL; -WidgetT *sOpenPopup = NULL; -WidgetT *sPressedButton = NULL; -WidgetT *sDragSlider = NULL; -WidgetT *sDrawingCanvas = NULL; -WidgetT *sDragTextSelect = NULL; -int32_t sDragOffset = 0; -WidgetT *sResizeListView = NULL; -int32_t sResizeCol = -1; -int32_t sResizeStartX = 0; -int32_t sResizeOrigW = 0; -WidgetT *sDragSplitter = NULL; -int32_t sDragSplitStart = 0; -WidgetT *sDragReorder = NULL; +WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk) +WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list +WidgetT *sPressedButton = NULL; // button being held down (tracks mouse in/out) +WidgetT *sDragSlider = NULL; // slider being dragged +WidgetT *sDrawingCanvas = NULL; // canvas receiving paint strokes +WidgetT *sDragTextSelect = NULL; // text widget in drag-select mode +int32_t sDragOffset = 0; // pixel offset from drag start to thumb center +WidgetT *sResizeListView = NULL; // ListView undergoing column resize +int32_t sResizeCol = -1; // which column is being resized +int32_t sResizeStartX = 0; // mouse X at resize start +int32_t sResizeOrigW = 0; // column width at resize start +WidgetT *sDragSplitter = NULL; // splitter being dragged +int32_t sDragSplitStart = 0; // mouse offset from splitter edge at drag start +WidgetT *sDragReorder = NULL; // list/tree widget in drag-reorder mode // ============================================================ // widgetAddChild // ============================================================ +// +// Appends a child to the end of the parent's child list. O(1) +// thanks to the lastChild tail pointer. The child list is singly- +// linked (nextSibling), which saves 4 bytes per widget vs doubly- +// linked and is sufficient because child removal is infrequent. void widgetAddChild(WidgetT *parent, WidgetT *child) { child->parent = parent; @@ -44,6 +80,18 @@ void widgetAddChild(WidgetT *parent, WidgetT *child) { // ============================================================ // widgetAlloc // ============================================================ +// +// Allocates and zero-initializes a new widget, links it to its +// class vtable via widgetClassTable[], and optionally adds it as +// a child of the given parent. +// +// The memset to 0 is intentional — it establishes sane defaults +// for all fields: NULL pointers, zero coordinates, no focus, +// no accel key, etc. Only visible and enabled default to true. +// +// The window pointer is inherited from the parent so that any +// widget in the tree can find its owning window without walking +// to the root. WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) { WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT)); @@ -104,6 +152,17 @@ int32_t widgetCountVisibleChildren(const WidgetT *w) { // ============================================================ // widgetDestroyChildren // ============================================================ +// +// Recursively destroys all descendants of a widget. Processes +// children depth-first (destroy grandchildren before the child +// itself) so that per-widget destroy callbacks see a consistent +// tree state. +// +// Critically, this function clears all global state pointers that +// reference destroyed widgets. Without this, any pending drag or +// focus state would become a dangling pointer. Each global is +// checked individually rather than cleared unconditionally to +// avoid disrupting unrelated ongoing interactions. void widgetDestroyChildren(WidgetT *w) { WidgetT *child = w->firstChild; @@ -155,7 +214,17 @@ void widgetDestroyChildren(WidgetT *w) { // widgetDropdownPopupRect // ============================================================ // -// Calculate the rectangle for a dropdown/combobox popup list. +// Calculates the screen rectangle for a dropdown/combobox popup list. +// Shared between Dropdown and ComboBox since they have identical +// popup positioning logic. +// +// The popup tries to open below the widget first. If there isn't +// enough room (popup would extend past the content area bottom), +// it flips to open above instead. This ensures the popup is always +// visible, even for dropdowns near the bottom of a window. +// +// Popup height is capped at DROPDOWN_MAX_VISIBLE items to prevent +// huge popups from dominating the screen. void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) { int32_t itemCount = 0; @@ -196,6 +265,15 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten // ============================================================ // widgetFindByAccel // ============================================================ +// +// Finds a widget with the given Alt+key accelerator. Recurses the +// tree depth-first, respecting visibility and enabled state. +// +// Special case for TabPage widgets: even if the tab page itself is +// not visible (inactive tab), its accelKey is still checked. This +// allows Alt+key to switch to a different tab. However, children +// of invisible tab pages are NOT searched — their accelerators +// should not be active when the tab is hidden. WidgetT *widgetFindByAccel(WidgetT *root, char key) { if (!root || !root->enabled) { @@ -232,9 +310,15 @@ WidgetT *widgetFindByAccel(WidgetT *root, char key) { // widgetFindNextFocusable // ============================================================ // -// Depth-first walk of the widget tree. Returns the first focusable -// widget found after 'after'. If 'after' is NULL, returns the first -// focusable widget. Wraps around to the beginning if needed. +// Implements Tab-order navigation: finds the next focusable widget +// after 'after' in depth-first tree order. The two-pass approach +// (search from 'after' to end, then wrap to start) ensures circular +// tabbing — Tab on the last focusable widget wraps to the first. +// +// The pastAfter flag tracks whether we've passed the 'after' widget +// during traversal. Once past it, the next focusable widget is the +// answer. This avoids collecting all focusable widgets into an array +// just to find the next one — the common case returns quickly. static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) { if (!w->visible || !w->enabled) { @@ -280,8 +364,17 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) { // widgetFindPrevFocusable // ============================================================ // -// Depth-first walk collecting all visible+enabled focusable widgets, -// then returns the one before 'before'. Wraps around if needed. +// Shift+Tab navigation: finds the previous focusable widget. +// Unlike findNextFocusable which can short-circuit during traversal, +// finding the PREVIOUS widget requires knowing the full order. +// So this collects all focusable widgets into an array, finds the +// target's index, and returns index-1 (with wraparound). +// +// The explicit stack-based DFS (rather than recursion) is used here +// because we need to push children in reverse order to get the same +// left-to-right depth-first ordering as the recursive version. +// Fixed-size arrays (128 widgets, 64 stack depth) are adequate for +// any reasonable dialog layout and avoid dynamic allocation. WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) { WidgetT *list[128]; @@ -362,6 +455,22 @@ int32_t widgetFrameBorderWidth(const WidgetT *w) { // ============================================================ // widgetHitTest // ============================================================ +// +// Recursive hit testing: finds the deepest (most specific) widget +// under the given coordinates. Returns the widget itself if no +// child is hit, or NULL if the point is outside this widget. +// +// Children are iterated front-to-back (first to last in the linked +// list), but the LAST match wins. This gives later siblings higher +// Z-order, which matches the painting order (later children paint +// on top of earlier ones). This is important for overlapping widgets, +// though in practice the layout engine rarely produces overlap. +// +// Widgets with WCLASS_NO_HIT_RECURSE stop the recursion — the parent +// widget handles all mouse events for its children. This is used by +// TreeView, ScrollPane, ListView, and Splitter, which need to manage +// their own internal regions (scrollbars, column headers, tree +// expand buttons) that don't correspond to child widgets. WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) { if (!w->visible) { @@ -427,8 +536,16 @@ bool widgetIsHorizContainer(WidgetTypeE type) { // widgetNavigateIndex // ============================================================ // -// Shared Up/Down/Home/End/PgUp/PgDn handler for list-like widgets. -// Returns the new index, or -1 if the key is not a navigation key. +// Shared keyboard navigation for list-like widgets (ListBox, Dropdown, +// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic +// so each widget doesn't have to reimplement index clamping. +// +// Key values use the 0x100 flag to mark extended scan codes (arrow +// keys, Home, End, etc.) — this is the DVX convention for passing +// scan codes through the same int32_t channel as ASCII values. +// +// Returns -1 for unrecognized keys so callers can check whether the +// key was consumed. int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) { if (key == (0x50 | 0x100)) { @@ -516,6 +633,15 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f // ============================================================ // widgetScrollbarThumb // ============================================================ +// +// Calculates thumb position and size for a scrollbar track. +// Used by both the WM-level scrollbars and widget-internal scrollbars +// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior. +// +// The thumb size is proportional to visibleSize/totalSize — a larger +// visible area means a larger thumb, giving visual feedback about how +// much content is scrollable. SB_MIN_THUMB prevents the thumb from +// becoming too small to grab with a mouse. void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) { *thumbSize = (trackLen * visibleSize) / totalSize; @@ -541,6 +667,11 @@ void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSi // ============================================================ // widgetRemoveChild // ============================================================ +// +// Unlinks a child from its parent's child list. O(n) in the number +// of children because the singly-linked list requires walking to +// find the predecessor. This is acceptable because child removal +// is infrequent (widget destruction, tree item reordering). void widgetRemoveChild(WidgetT *parent, WidgetT *child) { WidgetT *prev = NULL; diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index b7cbc51..f821b22 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -1,4 +1,22 @@ // widgetDropdown.c — Dropdown (select) widget +// +// A non-editable dropdown list (HTML