Much new documentation added as comments to code.
This commit is contained in:
parent
14bc2027df
commit
786353fa08
76 changed files with 6217 additions and 572 deletions
|
|
@ -1,7 +1,22 @@
|
||||||
// clock.c — Clock DXE application (main-loop with tsYield)
|
// clock.c — Clock DXE application (main-loop with tsYield)
|
||||||
//
|
//
|
||||||
// Demonstrates a main-loop app: runs its own loop calling tsYield(),
|
// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback-
|
||||||
// updates a clock display every second, and invalidates its window.
|
// 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 "dvxApp.h"
|
||||||
#include "dvxWidget.h"
|
#include "dvxWidget.h"
|
||||||
|
|
@ -45,6 +60,9 @@ static void updateTime(void);
|
||||||
// App descriptor
|
// 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 = {
|
AppDescriptorT appDescriptor = {
|
||||||
.name = "Clock",
|
.name = "Clock",
|
||||||
.hasMainLoop = true,
|
.hasMainLoop = true,
|
||||||
|
|
@ -56,12 +74,23 @@ AppDescriptorT appDescriptor = {
|
||||||
// Callbacks (fire in task 0 during dvxUpdate)
|
// 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) {
|
static void onClose(WindowT *win) {
|
||||||
(void)win;
|
(void)win;
|
||||||
sState.quit = true;
|
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) {
|
static void onPaint(WindowT *win, RectT *dirty) {
|
||||||
(void)dirty;
|
(void)dirty;
|
||||||
|
|
||||||
|
|
@ -70,7 +99,9 @@ static void onPaint(WindowT *win, RectT *dirty) {
|
||||||
const BitmapFontT *font = dvxGetFont(ac);
|
const BitmapFontT *font = dvxGetFont(ac);
|
||||||
const ColorSchemeT *colors = dvxGetColors(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);
|
DisplayT cd = *dvxGetDisplay(ac);
|
||||||
cd.backBuf = win->contentBuf;
|
cd.backBuf = win->contentBuf;
|
||||||
cd.width = win->contentW;
|
cd.width = win->contentW;
|
||||||
|
|
@ -121,6 +152,10 @@ static void updateTime(void) {
|
||||||
// Shutdown hook (optional DXE export)
|
// 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) {
|
void appShutdown(void) {
|
||||||
sState.quit = true;
|
sState.quit = true;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +164,8 @@ void appShutdown(void) {
|
||||||
// Entry point (runs in its own task)
|
// 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) {
|
int32_t appMain(DxeAppContextT *ctx) {
|
||||||
sCtx = ctx;
|
sCtx = ctx;
|
||||||
AppContextT *ac = ctx->shellCtx;
|
AppContextT *ac = ctx->shellCtx;
|
||||||
|
|
@ -136,11 +173,13 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
memset(&sState, 0, sizeof(sState));
|
memset(&sState, 0, sizeof(sState));
|
||||||
updateTime();
|
updateTime();
|
||||||
|
|
||||||
|
// Position in the upper-right corner, out of the way of other windows
|
||||||
int32_t winW = 200;
|
int32_t winW = 200;
|
||||||
int32_t winH = 100;
|
int32_t winH = 100;
|
||||||
int32_t winX = ac->display.width - winW - 40;
|
int32_t winX = ac->display.width - winW - 40;
|
||||||
int32_t winY = 40;
|
int32_t winY = 40;
|
||||||
|
|
||||||
|
// resizable=false: clock has a fixed size, no resize handle
|
||||||
sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false);
|
sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false);
|
||||||
|
|
||||||
if (!sWin) {
|
if (!sWin) {
|
||||||
|
|
@ -155,7 +194,12 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
onPaint(sWin, &full);
|
onPaint(sWin, &full);
|
||||||
dvxInvalidateWindow(ac, sWin);
|
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) {
|
while (!sState.quit) {
|
||||||
time_t now = time(NULL);
|
time_t now = time(NULL);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
// dvxdemo.c — DVX GUI demonstration app (DXE version)
|
// dvxdemo.c — DVX GUI demonstration app (DXE version)
|
||||||
//
|
//
|
||||||
// Callback-only app that opens several windows showcasing the DVX
|
// Callback-only DXE app (hasMainLoop = false) that opens several windows
|
||||||
// widget system, paint callbacks, menus, accelerators, etc.
|
// 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 "dvxApp.h"
|
||||||
#include "dvxDialog.h"
|
#include "dvxDialog.h"
|
||||||
|
|
@ -79,6 +90,9 @@ static DxeAppContextT *sDxeCtx = NULL;
|
||||||
// App descriptor
|
// 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 = {
|
AppDescriptorT appDescriptor = {
|
||||||
.name = "DVX Demo",
|
.name = "DVX Demo",
|
||||||
.hasMainLoop = false,
|
.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) {
|
static void onOkClick(WidgetT *w) {
|
||||||
WidgetT *root = 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) {
|
static void onPaintColor(WindowT *win, RectT *dirtyArea) {
|
||||||
(void)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) {
|
static void onPaintPattern(WindowT *win, RectT *dirtyArea) {
|
||||||
(void)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) {
|
static void onPaintText(WindowT *win, RectT *dirtyArea) {
|
||||||
(void)dirtyArea;
|
(void)dirtyArea;
|
||||||
|
|
||||||
|
|
@ -398,9 +427,14 @@ static void onToolbarClick(WidgetT *w) {
|
||||||
// setupControlsWindow — advanced widgets with tabs
|
// 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 *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"};
|
||||||
static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"};
|
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) {
|
static void setupControlsWindow(void) {
|
||||||
WindowT *win = dvxCreateWindow(sAc, "Advanced Widgets", 380, 50, 360, 440, true);
|
WindowT *win = dvxCreateWindow(sAc, "Advanced Widgets", 380, 50, 360, 440, true);
|
||||||
|
|
||||||
|
|
@ -597,9 +631,11 @@ static void setupControlsWindow(void) {
|
||||||
wgtCanvasSetMouseCallback(cv, onCanvasDraw);
|
wgtCanvasSetMouseCallback(cv, onCanvasDraw);
|
||||||
|
|
||||||
// --- Tab 8: Splitter ---
|
// --- 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");
|
WidgetT *page8s = wgtTabPage(tabs, "S&plit");
|
||||||
|
|
||||||
// Outer horizontal splitter: explorer on top, detail on bottom
|
|
||||||
WidgetT *hSplit = wgtSplitter(page8s, false);
|
WidgetT *hSplit = wgtSplitter(page8s, false);
|
||||||
hSplit->weight = 100;
|
hSplit->weight = 100;
|
||||||
wgtSplitterSetPos(hSplit, 120);
|
wgtSplitterSetPos(hSplit, 120);
|
||||||
|
|
@ -637,9 +673,9 @@ static void setupControlsWindow(void) {
|
||||||
wgtLabel(detailFrame, "Select a file above to preview.");
|
wgtLabel(detailFrame, "Select a file above to preview.");
|
||||||
|
|
||||||
// --- Tab 9: Disabled ---
|
// --- 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");
|
WidgetT *page9d = wgtTabPage(tabs, "&Disabled");
|
||||||
|
|
||||||
// Left column: enabled widgets Right column: disabled widgets
|
|
||||||
WidgetT *disRow = wgtHBox(page9d);
|
WidgetT *disRow = wgtHBox(page9d);
|
||||||
disRow->weight = 100;
|
disRow->weight = 100;
|
||||||
|
|
||||||
|
|
@ -736,8 +772,11 @@ static void setupControlsWindow(void) {
|
||||||
// setupMainWindow — info window + paint demos
|
// 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) {
|
static void setupMainWindow(void) {
|
||||||
// Window 1: Text information window with menu bar
|
|
||||||
WindowT *win1 = dvxCreateWindow(sAc, "DVX Information", 50, 40, 340, 350, true);
|
WindowT *win1 = dvxCreateWindow(sAc, "DVX Information", 50, 40, 340, 350, true);
|
||||||
|
|
||||||
if (win1) {
|
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();
|
AccelTableT *accel = dvxCreateAccelTable();
|
||||||
dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW);
|
dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW);
|
||||||
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
|
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
|
||||||
|
|
@ -818,7 +859,8 @@ static void setupMainWindow(void) {
|
||||||
dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT);
|
dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT);
|
||||||
win1->accelTable = accel;
|
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();
|
MenuT *winCtx = wmCreateMenu();
|
||||||
wmAddMenuItem(winCtx, "Cu&t", CMD_CTX_CUT);
|
wmAddMenuItem(winCtx, "Cu&t", CMD_CTX_CUT);
|
||||||
wmAddMenuItem(winCtx, "&Copy", CMD_CTX_COPY);
|
wmAddMenuItem(winCtx, "&Copy", CMD_CTX_COPY);
|
||||||
|
|
@ -827,6 +869,10 @@ static void setupMainWindow(void) {
|
||||||
wmAddMenuItem(winCtx, "&Properties...", CMD_CTX_PROPS);
|
wmAddMenuItem(winCtx, "&Properties...", CMD_CTX_PROPS);
|
||||||
win1->contextMenu = winCtx;
|
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);
|
wmUpdateContentRect(win1);
|
||||||
wmReallocContentBuf(win1, &sAc->display);
|
wmReallocContentBuf(win1, &sAc->display);
|
||||||
|
|
||||||
|
|
@ -867,6 +913,10 @@ static void setupMainWindow(void) {
|
||||||
// setupTerminalWindow — ANSI terminal widget demo
|
// 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) {
|
static void setupTerminalWindow(void) {
|
||||||
WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", 60, 60, 660, 420, true);
|
WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", 60, 60, 660, 420, true);
|
||||||
|
|
||||||
|
|
@ -946,6 +996,10 @@ static void setupTerminalWindow(void) {
|
||||||
// setupWidgetDemo — form with accelerators
|
// 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) {
|
static void setupWidgetDemo(void) {
|
||||||
WindowT *win = dvxCreateWindow(sAc, "Widget Demo", 80, 200, 280, 360, true);
|
WindowT *win = dvxCreateWindow(sAc, "Widget Demo", 80, 200, 280, 360, true);
|
||||||
|
|
||||||
|
|
@ -1037,6 +1091,9 @@ static void setupWidgetDemo(void) {
|
||||||
// Entry point
|
// 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) {
|
int32_t appMain(DxeAppContextT *ctx) {
|
||||||
sDxeCtx = ctx;
|
sDxeCtx = ctx;
|
||||||
sAc = ctx->shellCtx;
|
sAc = ctx->shellCtx;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
// notepad.c — Simple text editor DXE application (callback-only)
|
// 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 "dvxApp.h"
|
||||||
#include "dvxDialog.h"
|
#include "dvxDialog.h"
|
||||||
|
|
@ -18,6 +26,9 @@
|
||||||
// Constants
|
// 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 TEXT_BUF_SIZE 32768
|
||||||
|
|
||||||
#define CMD_NEW 100
|
#define CMD_NEW 100
|
||||||
|
|
@ -38,6 +49,7 @@ static DxeAppContextT *sCtx = NULL;
|
||||||
static WindowT *sWin = NULL;
|
static WindowT *sWin = NULL;
|
||||||
static WidgetT *sTextArea = NULL;
|
static WidgetT *sTextArea = NULL;
|
||||||
static char sFilePath[260] = "";
|
static char sFilePath[260] = "";
|
||||||
|
// Hash of text content at last save/open, used for cheap dirty detection
|
||||||
static uint32_t sCleanHash = 0;
|
static uint32_t sCleanHash = 0;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -60,6 +72,8 @@ static void onMenu(WindowT *win, int32_t menuId);
|
||||||
// App descriptor
|
// 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 = {
|
AppDescriptorT appDescriptor = {
|
||||||
.name = "Notepad",
|
.name = "Notepad",
|
||||||
.hasMainLoop = false,
|
.hasMainLoop = false,
|
||||||
|
|
@ -71,6 +85,10 @@ AppDescriptorT appDescriptor = {
|
||||||
// Dirty tracking
|
// 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) {
|
static uint32_t hashText(const char *text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -151,6 +169,8 @@ static void doOpen(void) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open in binary mode to avoid DJGPP's CR/LF translation; the TextArea
|
||||||
|
// widget handles line endings internally.
|
||||||
FILE *f = fopen(path, "rb");
|
FILE *f = fopen(path, "rb");
|
||||||
|
|
||||||
if (!f) {
|
if (!f) {
|
||||||
|
|
@ -158,6 +178,9 @@ static void doOpen(void) {
|
||||||
return;
|
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);
|
fseek(f, 0, SEEK_END);
|
||||||
long size = ftell(f);
|
long size = ftell(f);
|
||||||
fseek(f, 0, SEEK_SET);
|
fseek(f, 0, SEEK_SET);
|
||||||
|
|
@ -234,6 +257,9 @@ static void doSaveAs(void) {
|
||||||
// Callbacks
|
// 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) {
|
static void onClose(WindowT *win) {
|
||||||
if (!askSaveChanges()) {
|
if (!askSaveChanges()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -287,6 +313,8 @@ static void onMenu(WindowT *win, int32_t menuId) {
|
||||||
// Entry point
|
// 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) {
|
int32_t appMain(DxeAppContextT *ctx) {
|
||||||
sCtx = ctx;
|
sCtx = ctx;
|
||||||
AppContextT *ac = ctx->shellCtx;
|
AppContextT *ac = ctx->shellCtx;
|
||||||
|
|
@ -326,7 +354,10 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
wmAddMenuSeparator(editMenu);
|
wmAddMenuSeparator(editMenu);
|
||||||
wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELALL);
|
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);
|
WidgetT *root = wgtInitWindow(ac, sWin);
|
||||||
|
|
||||||
sTextArea = wgtTextArea(root, TEXT_BUF_SIZE);
|
sTextArea = wgtTextArea(root, TEXT_BUF_SIZE);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,21 @@
|
||||||
//
|
//
|
||||||
// Displays a grid of available apps from the apps/ directory.
|
// Displays a grid of available apps from the apps/ directory.
|
||||||
// Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc).
|
// 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 "dvxApp.h"
|
||||||
#include "dvxDialog.h"
|
#include "dvxDialog.h"
|
||||||
|
|
@ -23,8 +36,11 @@
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// 64 entries is generous; limited by screen real estate before this cap
|
||||||
#define MAX_APP_FILES 64
|
#define MAX_APP_FILES 64
|
||||||
|
// DOS 8.3 paths are short, but long names under DJGPP can reach ~260
|
||||||
#define MAX_PATH_LEN 260
|
#define MAX_PATH_LEN 260
|
||||||
|
// Grid layout for app buttons: 4 columns, rows created dynamically
|
||||||
#define PM_GRID_COLS 4
|
#define PM_GRID_COLS 4
|
||||||
#define PM_BTN_W 100
|
#define PM_BTN_W 100
|
||||||
#define PM_BTN_H 24
|
#define PM_BTN_H 24
|
||||||
|
|
@ -44,11 +60,15 @@
|
||||||
// Module state
|
// Module state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Each discovered .app file in the apps/ directory tree
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[SHELL_APP_NAME_MAX]; // display name (filename without .app)
|
char name[SHELL_APP_NAME_MAX]; // display name (filename without .app)
|
||||||
char path[MAX_PATH_LEN]; // full path
|
char path[MAX_PATH_LEN]; // full path
|
||||||
} AppEntryT;
|
} 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 DxeAppContextT *sCtx = NULL;
|
||||||
static AppContextT *sAc = NULL;
|
static AppContextT *sAc = NULL;
|
||||||
static int32_t sMyAppId = 0;
|
static int32_t sMyAppId = 0;
|
||||||
|
|
@ -86,6 +106,10 @@ static void refreshTaskList(void);
|
||||||
// App descriptor
|
// 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 = {
|
AppDescriptorT appDescriptor = {
|
||||||
.name = "Program Manager",
|
.name = "Program Manager",
|
||||||
.hasMainLoop = false,
|
.hasMainLoop = false,
|
||||||
|
|
@ -97,6 +121,9 @@ AppDescriptorT appDescriptor = {
|
||||||
// Static functions (alphabetical)
|
// 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) {
|
static void buildPmWindow(void) {
|
||||||
int32_t screenW = sAc->display.width;
|
int32_t screenW = sAc->display.width;
|
||||||
int32_t screenH = sAc->display.height;
|
int32_t screenH = sAc->display.height;
|
||||||
|
|
@ -135,10 +162,12 @@ static void buildPmWindow(void) {
|
||||||
wmAddMenuSeparator(helpMenu);
|
wmAddMenuSeparator(helpMenu);
|
||||||
wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR);
|
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);
|
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");
|
WidgetT *appFrame = wgtFrame(root, "Applications");
|
||||||
appFrame->weight = 100;
|
appFrame->weight = 100;
|
||||||
|
|
||||||
|
|
@ -146,7 +175,9 @@ static void buildPmWindow(void) {
|
||||||
WidgetT *lbl = wgtLabel(appFrame, "(No applications found in apps/ directory)");
|
WidgetT *lbl = wgtLabel(appFrame, "(No applications found in apps/ directory)");
|
||||||
(void)lbl;
|
(void)lbl;
|
||||||
} else {
|
} 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;
|
int32_t row = 0;
|
||||||
WidgetT *hbox = NULL;
|
WidgetT *hbox = NULL;
|
||||||
|
|
||||||
|
|
@ -168,19 +199,26 @@ static void buildPmWindow(void) {
|
||||||
(void)row;
|
(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);
|
WidgetT *statusBar = wgtStatusBar(root);
|
||||||
sStatusLabel = wgtLabel(statusBar, "");
|
sStatusLabel = wgtLabel(statusBar, "");
|
||||||
sStatusLabel->weight = 100;
|
sStatusLabel->weight = 100;
|
||||||
updateStatusText();
|
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);
|
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) {
|
static void buildTaskManager(void) {
|
||||||
if (sTmWindow) {
|
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++) {
|
for (int32_t i = 0; i < sAc->stack.count; i++) {
|
||||||
if (sAc->stack.windows[i] == sTmWindow) {
|
if (sAc->stack.windows[i] == sTmWindow) {
|
||||||
wmRaiseWindow(&sAc->stack, &sAc->dirty, i);
|
wmRaiseWindow(&sAc->stack, &sAc->dirty, i);
|
||||||
|
|
@ -208,7 +246,8 @@ static void buildTaskManager(void) {
|
||||||
|
|
||||||
WidgetT *root = wgtInitWindow(sAc, sTmWindow);
|
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];
|
ListViewColT tmCols[3];
|
||||||
tmCols[0].title = "Name";
|
tmCols[0].title = "Name";
|
||||||
tmCols[0].width = wgtPercent(50);
|
tmCols[0].width = wgtPercent(50);
|
||||||
|
|
@ -225,7 +264,7 @@ static void buildTaskManager(void) {
|
||||||
sTmListView->prefH = wgtPixels(160);
|
sTmListView->prefH = wgtPixels(160);
|
||||||
wgtListViewSetColumns(sTmListView, tmCols, 3);
|
wgtListViewSetColumns(sTmListView, tmCols, 3);
|
||||||
|
|
||||||
// Button row
|
// Button row right-aligned (AlignEndE) to follow Windows UI convention
|
||||||
WidgetT *btnRow = wgtHBox(root);
|
WidgetT *btnRow = wgtHBox(root);
|
||||||
btnRow->align = AlignEndE;
|
btnRow->align = AlignEndE;
|
||||||
btnRow->spacing = wgtPixels(8);
|
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) {
|
static void desktopUpdate(void) {
|
||||||
updateStatusText();
|
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) {
|
static void onAppButtonClick(WidgetT *w) {
|
||||||
AppEntryT *entry = (AppEntryT *)w->userData;
|
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) {
|
static void onPmClose(WindowT *win) {
|
||||||
(void)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);
|
int32_t result = dvxMessageBox(sAc, "Exit Shell", "Are you sure you want to exit DVX Shell?", MB_YESNO | MB_ICONQUESTION);
|
||||||
|
|
||||||
if (result == ID_YES) {
|
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) {
|
static void onTmClose(WindowT *win) {
|
||||||
sTmListView = NULL;
|
sTmListView = NULL;
|
||||||
sTmWindow = NULL;
|
sTmWindow = NULL;
|
||||||
|
|
@ -357,7 +404,10 @@ static void onTmEndTask(WidgetT *w) {
|
||||||
return;
|
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;
|
int32_t idx = 0;
|
||||||
|
|
||||||
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
|
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
|
||||||
|
|
@ -394,7 +444,9 @@ static void onTmSwitchTo(WidgetT *w) {
|
||||||
return;
|
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;
|
int32_t idx = 0;
|
||||||
|
|
||||||
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
|
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) {
|
static void refreshTaskList(void) {
|
||||||
if (!sTmListView) {
|
if (!sTmListView) {
|
||||||
return;
|
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 const char *cells[SHELL_MAX_APPS * 3];
|
||||||
static char typeStrs[SHELL_MAX_APPS][12];
|
static char typeStrs[SHELL_MAX_APPS][12];
|
||||||
int32_t rowCount = 0;
|
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) {
|
static void scanAppsDir(void) {
|
||||||
sAppCount = 0;
|
sAppCount = 0;
|
||||||
scanAppsDirRecurse("apps");
|
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) {
|
static void scanAppsDirRecurse(const char *dirPath) {
|
||||||
DIR *dir = opendir(dirPath);
|
DIR *dir = opendir(dirPath);
|
||||||
|
|
||||||
|
|
@ -555,7 +617,8 @@ static void updateStatusText(void) {
|
||||||
|
|
||||||
static char buf[64];
|
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;
|
int32_t count = shellRunningAppCount() - 1;
|
||||||
|
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
|
|
@ -577,6 +640,10 @@ static void updateStatusText(void) {
|
||||||
// Entry point
|
// 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) {
|
int32_t appMain(DxeAppContextT *ctx) {
|
||||||
sCtx = ctx;
|
sCtx = ctx;
|
||||||
sAc = ctx->shellCtx;
|
sAc = ctx->shellCtx;
|
||||||
|
|
@ -585,7 +652,8 @@ int32_t appMain(DxeAppContextT *ctx) {
|
||||||
scanAppsDir();
|
scanAppsDir();
|
||||||
buildPmWindow();
|
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);
|
shellRegisterDesktopUpdate(desktopUpdate);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
469
dvx/dvxApp.c
469
dvx/dvxApp.c
|
|
@ -1,4 +1,32 @@
|
||||||
// dvx_app.c — Layer 5: Application API for DVX GUI
|
// 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 "dvxApp.h"
|
||||||
#include "dvxWidget.h"
|
#include "dvxWidget.h"
|
||||||
|
|
@ -14,9 +42,19 @@
|
||||||
|
|
||||||
#include "thirdparty/stb_image_write.h"
|
#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)
|
#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
|
#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 KB_MOVE_STEP 8
|
||||||
|
|
||||||
#define MENU_CHECK_WIDTH 14
|
#define MENU_CHECK_WIDTH 14
|
||||||
#define SUBMENU_ARROW_WIDTH 12
|
#define SUBMENU_ARROW_WIDTH 12
|
||||||
#define SUBMENU_ARROW_HALF 3 // half-size of submenu arrow glyph
|
#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 updateCursorShape(AppContextT *ctx);
|
||||||
static void updateTooltip(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;
|
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) {
|
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);
|
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) {
|
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
|
||||||
int32_t maxW = 0;
|
int32_t maxW = 0;
|
||||||
bool hasSub = false;
|
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);
|
*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;
|
*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
|
// 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) {
|
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers) {
|
||||||
if (!win->accelTable || !win->onMenu) {
|
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);
|
int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT);
|
||||||
AccelTableT *table = win->accelTable;
|
AccelTableT *table = win->accelTable;
|
||||||
|
|
||||||
// Match against pre-normalized keys (Item 6)
|
|
||||||
for (int32_t i = 0; i < table->count; i++) {
|
for (int32_t i = 0; i < table->count; i++) {
|
||||||
AccelEntryT *e = &table->entries[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
|
// 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) {
|
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) {
|
||||||
MenuItemT *item = &menu->items[itemIdx];
|
MenuItemT *item = &menu->items[itemIdx];
|
||||||
|
|
@ -210,22 +289,18 @@ static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) {
|
||||||
if (item->type == MenuItemCheckE) {
|
if (item->type == MenuItemCheckE) {
|
||||||
item->checked = !item->checked;
|
item->checked = !item->checked;
|
||||||
} else if (item->type == MenuItemRadioE) {
|
} 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;
|
int32_t groupStart = itemIdx;
|
||||||
|
|
||||||
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
|
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
|
||||||
groupStart--;
|
groupStart--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search forward to find group end
|
|
||||||
int32_t groupEnd = itemIdx;
|
int32_t groupEnd = itemIdx;
|
||||||
|
|
||||||
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
|
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
|
||||||
groupEnd++;
|
groupEnd++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncheck all in group, check the clicked one
|
|
||||||
for (int32_t i = groupStart; i <= groupEnd; i++) {
|
for (int32_t i = groupStart; i <= groupEnd; i++) {
|
||||||
menu->items[i].checked = (i == itemIdx);
|
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
|
// 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) {
|
static void closeAllPopups(AppContextT *ctx) {
|
||||||
if (!ctx->popup.active) {
|
if (!ctx->popup.active) {
|
||||||
|
|
@ -260,6 +342,12 @@ static void closeAllPopups(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// closePopupLevel — close one submenu level (or deactivate if top)
|
// 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) {
|
static void closePopupLevel(AppContextT *ctx) {
|
||||||
if (!ctx->popup.active) {
|
if (!ctx->popup.active) {
|
||||||
|
|
@ -303,6 +391,25 @@ static void closeSysMenu(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// compositeAndFlush
|
// 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) {
|
static void compositeAndFlush(AppContextT *ctx) {
|
||||||
DisplayT *d = &ctx->display;
|
DisplayT *d = &ctx->display;
|
||||||
|
|
@ -310,9 +417,11 @@ static void compositeAndFlush(AppContextT *ctx) {
|
||||||
DirtyListT *dl = &ctx->dirty;
|
DirtyListT *dl = &ctx->dirty;
|
||||||
WindowStackT *ws = &ctx->stack;
|
WindowStackT *ws = &ctx->stack;
|
||||||
|
|
||||||
|
// Merge overlapping dirty rects to reduce flush count
|
||||||
dirtyListMerge(dl);
|
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 visibleIdx[MAX_WINDOWS];
|
||||||
int32_t visibleCount = 0;
|
int32_t visibleCount = 0;
|
||||||
|
|
||||||
|
|
@ -444,11 +553,16 @@ static void compositeAndFlush(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dirtyCursorArea
|
// 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) {
|
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);
|
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
|
// 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) {
|
static bool dispatchAccelKey(AppContextT *ctx, char key) {
|
||||||
if (ctx->stack.focusedIdx < 0) {
|
if (ctx->stack.focusedIdx < 0) {
|
||||||
|
|
@ -464,7 +587,7 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
|
||||||
|
|
||||||
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
||||||
|
|
||||||
// Check menu bar first
|
// Menu bar accelerators take priority over widget accelerators
|
||||||
if (win->menuBar) {
|
if (win->menuBar) {
|
||||||
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
|
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
|
||||||
if (win->menuBar->menus[i].accelKey == key) {
|
if (win->menuBar->menus[i].accelKey == key) {
|
||||||
|
|
@ -613,6 +736,20 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dispatchEvents
|
// 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) {
|
static void dispatchEvents(AppContextT *ctx) {
|
||||||
int32_t mx = ctx->mouseX;
|
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) {
|
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 &&
|
bool inCurrent = (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
|
||||||
my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH);
|
my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH);
|
||||||
|
|
||||||
if (inCurrent) {
|
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 relY = my - ctx->popup.popupY - POPUP_BEVEL_WIDTH;
|
||||||
int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0;
|
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);
|
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;
|
int32_t menuId = item->id;
|
||||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||||
closeAllPopups(ctx);
|
closeAllPopups(ctx);
|
||||||
|
|
@ -862,7 +1008,12 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
handleMouseButton(ctx, mx, my, buttons);
|
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)) {
|
if ((buttons & 2) && !(prevBtn & 2)) {
|
||||||
int32_t hitPart;
|
int32_t hitPart;
|
||||||
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
||||||
|
|
@ -870,7 +1021,6 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
if (hitIdx >= 0 && hitPart == 0) {
|
if (hitIdx >= 0 && hitPart == 0) {
|
||||||
WindowT *win = ctx->stack.windows[hitIdx];
|
WindowT *win = ctx->stack.windows[hitIdx];
|
||||||
|
|
||||||
// Raise and focus if not already
|
|
||||||
if (hitIdx != ctx->stack.focusedIdx) {
|
if (hitIdx != ctx->stack.focusedIdx) {
|
||||||
wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
|
wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
|
||||||
hitIdx = ctx->stack.count - 1;
|
hitIdx = ctx->stack.count - 1;
|
||||||
|
|
@ -878,7 +1028,6 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
win = ctx->stack.windows[hitIdx];
|
win = ctx->stack.windows[hitIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check widget context menu first
|
|
||||||
MenuT *ctxMenu = NULL;
|
MenuT *ctxMenu = NULL;
|
||||||
|
|
||||||
if (win->widgetRoot) {
|
if (win->widgetRoot) {
|
||||||
|
|
@ -886,7 +1035,6 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
int32_t relY = my - win->y - win->contentY;
|
int32_t relY = my - win->y - win->contentY;
|
||||||
WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY);
|
WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY);
|
||||||
|
|
||||||
// Walk up the tree to find a context menu
|
|
||||||
while (hit && !hit->contextMenu) {
|
while (hit && !hit->contextMenu) {
|
||||||
hit = hit->parent;
|
hit = hit->parent;
|
||||||
}
|
}
|
||||||
|
|
@ -896,7 +1044,6 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to window context menu
|
|
||||||
if (!ctxMenu) {
|
if (!ctxMenu) {
|
||||||
ctxMenu = win->contextMenu;
|
ctxMenu = win->contextMenu;
|
||||||
}
|
}
|
||||||
|
|
@ -937,6 +1084,12 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawCursorAt
|
// 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) {
|
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) {
|
||||||
const CursorT *cur = &ctx->cursors[ctx->cursorId];
|
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)
|
// 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) {
|
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 };
|
RectT popRect = { px, py, pw, ph };
|
||||||
|
|
@ -1103,13 +1267,18 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w,
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxAddAccel
|
// 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) {
|
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
|
||||||
if (!table || table->count >= MAX_ACCEL_ENTRIES) {
|
if (!table || table->count >= MAX_ACCEL_ENTRIES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-normalize key for fast matching (Item 6)
|
|
||||||
int32_t normKey = key;
|
int32_t normKey = key;
|
||||||
|
|
||||||
if (normKey >= 'a' && normKey <= 'z') {
|
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) {
|
void dvxClipboardCopy(const char *text, int32_t len) {
|
||||||
clipboardCopy(text, len);
|
clipboardCopy(text, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// dvxClipboardGet
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
const char *dvxClipboardGet(int32_t *outLen) {
|
const char *dvxClipboardGet(int32_t *outLen) {
|
||||||
return clipboardGet(outLen);
|
return clipboardGet(outLen);
|
||||||
}
|
}
|
||||||
|
|
@ -1147,8 +1318,12 @@ const char *dvxClipboardGet(int32_t *outLen) {
|
||||||
// dvxCascadeWindows
|
// dvxCascadeWindows
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Arrange all visible, non-minimized windows in a staggered
|
// Arranges windows in the classic cascade pattern: each window is the same
|
||||||
// diagonal pattern from the top-left corner.
|
// 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) {
|
void dvxCascadeWindows(AppContextT *ctx) {
|
||||||
int32_t screenW = ctx->display.width;
|
int32_t screenW = ctx->display.width;
|
||||||
|
|
@ -1157,7 +1332,6 @@ void dvxCascadeWindows(AppContextT *ctx) {
|
||||||
int32_t offsetY = 0;
|
int32_t offsetY = 0;
|
||||||
int32_t step = CHROME_TITLE_HEIGHT + CHROME_BORDER_WIDTH;
|
int32_t step = CHROME_TITLE_HEIGHT + CHROME_BORDER_WIDTH;
|
||||||
|
|
||||||
// Default cascade size: 2/3 of screen
|
|
||||||
int32_t winW = screenW * 2 / 3;
|
int32_t winW = screenW * 2 / 3;
|
||||||
int32_t winH = screenH * 2 / 3;
|
int32_t winH = screenH * 2 / 3;
|
||||||
|
|
||||||
|
|
@ -1218,6 +1392,11 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxFitWindow
|
// 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) {
|
void dvxFitWindow(AppContextT *ctx, WindowT *win) {
|
||||||
if (!ctx || !win || !win->widgetRoot) {
|
if (!ctx || !win || !win->widgetRoot) {
|
||||||
|
|
@ -1304,42 +1483,44 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxInit
|
// 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) {
|
int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
|
||||||
memset(ctx, 0, sizeof(*ctx));
|
memset(ctx, 0, sizeof(*ctx));
|
||||||
|
|
||||||
// Platform-specific initialization (signal handling, etc.)
|
|
||||||
platformInit();
|
platformInit();
|
||||||
|
|
||||||
// Initialize video
|
|
||||||
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
|
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
|
||||||
return -1;
|
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);
|
drawInit(&ctx->blitOps, &ctx->display);
|
||||||
|
|
||||||
// Initialize window stack
|
|
||||||
wmInit(&ctx->stack);
|
wmInit(&ctx->stack);
|
||||||
|
|
||||||
// Initialize dirty list
|
|
||||||
dirtyListInit(&ctx->dirty);
|
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;
|
ctx->font = dvxFont8x16;
|
||||||
|
|
||||||
// Set up cursors
|
|
||||||
memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors));
|
memcpy(ctx->cursors, dvxCursors, sizeof(dvxCursors));
|
||||||
ctx->cursorId = CURSOR_ARROW;
|
ctx->cursorId = CURSOR_ARROW;
|
||||||
|
|
||||||
// Initialize colors
|
|
||||||
initColorScheme(ctx);
|
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->cursorFg = packColor(&ctx->display, 255, 255, 255);
|
||||||
ctx->cursorBg = packColor(&ctx->display, 0, 0, 0);
|
ctx->cursorBg = packColor(&ctx->display, 0, 0, 0);
|
||||||
|
|
||||||
// Initialize mouse
|
|
||||||
platformMouseInit(ctx->display.width, ctx->display.height);
|
platformMouseInit(ctx->display.width, ctx->display.height);
|
||||||
ctx->mouseX = ctx->display.width / 2;
|
ctx->mouseX = ctx->display.width / 2;
|
||||||
ctx->mouseY = ctx->display.height / 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->lastIconClickTime = 0;
|
||||||
ctx->lastCloseClickId = -1;
|
ctx->lastCloseClickId = -1;
|
||||||
ctx->lastCloseClickTime = 0;
|
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;
|
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);
|
dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -1424,6 +1610,23 @@ void dvxRun(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxUpdate
|
// 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) {
|
bool dvxUpdate(AppContextT *ctx) {
|
||||||
if (!ctx->running) {
|
if (!ctx->running) {
|
||||||
|
|
@ -1436,7 +1639,6 @@ bool dvxUpdate(AppContextT *ctx) {
|
||||||
updateTooltip(ctx);
|
updateTooltip(ctx);
|
||||||
pollAnsiTermWidgets(ctx);
|
pollAnsiTermWidgets(ctx);
|
||||||
|
|
||||||
// Periodically refresh one minimized window thumbnail (staggered)
|
|
||||||
ctx->frameCount++;
|
ctx->frameCount++;
|
||||||
|
|
||||||
if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) {
|
if (ctx->frameCount % ICON_REFRESH_INTERVAL == 0) {
|
||||||
|
|
@ -1451,7 +1653,10 @@ bool dvxUpdate(AppContextT *ctx) {
|
||||||
platformYield();
|
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) {
|
||||||
if (sKeyPressedBtn->type == WidgetImageButtonE) {
|
if (sKeyPressedBtn->type == WidgetImageButtonE) {
|
||||||
sKeyPressedBtn->as.imageButton.pressed = false;
|
sKeyPressedBtn->as.imageButton.pressed = false;
|
||||||
|
|
@ -1479,7 +1684,11 @@ bool dvxUpdate(AppContextT *ctx) {
|
||||||
// dvxScreenshot
|
// 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) {
|
int32_t dvxScreenshot(AppContextT *ctx, const char *path) {
|
||||||
DisplayT *d = &ctx->display;
|
DisplayT *d = &ctx->display;
|
||||||
|
|
@ -1533,8 +1742,11 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) {
|
||||||
// dvxWindowScreenshot
|
// dvxWindowScreenshot
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Save a window's content buffer to a PNG file. This captures the
|
// Save a window's content buffer to a PNG file. Because each window has
|
||||||
// full content area regardless of whether the window is occluded.
|
// 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) {
|
int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) {
|
||||||
if (!win || !win->contentBuf) {
|
if (!win || !win->contentBuf) {
|
||||||
|
|
@ -1559,9 +1771,15 @@ int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) {
|
||||||
// dvxTileWindows
|
// dvxTileWindows
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Tile all visible, non-minimized windows in a grid pattern.
|
// Tile windows in a grid. The grid dimensions are chosen so columns =
|
||||||
// Columns = ceil(sqrt(count)), rows = ceil(count / cols).
|
// ceil(sqrt(n)), which produces a roughly square grid. This is better than
|
||||||
// Last row may have fewer windows, which are widened to fill.
|
// 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) {
|
void dvxTileWindows(AppContextT *ctx) {
|
||||||
int32_t screenW = ctx->display.width;
|
int32_t screenW = ctx->display.width;
|
||||||
|
|
@ -1629,8 +1847,10 @@ void dvxTileWindows(AppContextT *ctx) {
|
||||||
// dvxTileWindowsH
|
// dvxTileWindowsH
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Tile all visible, non-minimized windows horizontally:
|
// Horizontal tiling: windows side by side left to right, each the full
|
||||||
// side by side left to right, each window gets full screen height.
|
// 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) {
|
void dvxTileWindowsH(AppContextT *ctx) {
|
||||||
int32_t screenW = ctx->display.width;
|
int32_t screenW = ctx->display.width;
|
||||||
|
|
@ -1677,8 +1897,8 @@ void dvxTileWindowsH(AppContextT *ctx) {
|
||||||
// dvxTileWindowsV
|
// dvxTileWindowsV
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Tile all visible, non-minimized windows vertically:
|
// Vertical tiling: windows stacked top to bottom, each the full screen
|
||||||
// stacked top to bottom, each window gets full screen width.
|
// width. The complement of dvxTileWindowsH.
|
||||||
|
|
||||||
void dvxTileWindowsV(AppContextT *ctx) {
|
void dvxTileWindowsV(AppContextT *ctx) {
|
||||||
int32_t screenW = ctx->display.width;
|
int32_t screenW = ctx->display.width;
|
||||||
|
|
@ -1724,6 +1944,12 @@ void dvxTileWindowsV(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// executeSysMenuCmd
|
// 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) {
|
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
|
||||||
WindowT *win = findWindowById(ctx, ctx->sysMenu.windowId);
|
WindowT *win = findWindowById(ctx, ctx->sysMenu.windowId);
|
||||||
|
|
@ -1799,6 +2025,22 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// handleMouseButton
|
// 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) {
|
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons) {
|
||||||
// Modal window gating: only the modal window receives clicks
|
// 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
|
// 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) {
|
static void initColorScheme(AppContextT *ctx) {
|
||||||
DisplayT *d = &ctx->display;
|
DisplayT *d = &ctx->display;
|
||||||
|
|
@ -1977,6 +2226,13 @@ static void initColorScheme(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// openContextMenu — open a context menu at a screen position
|
// 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) {
|
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
|
||||||
if (!menu || menu->itemCount <= 0) {
|
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
|
// 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) {
|
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
|
||||||
if (!win->menuBar || menuIdx < 0 || menuIdx >= win->menuBar->menuCount) {
|
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
|
// 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) {
|
static void openSubMenu(AppContextT *ctx) {
|
||||||
if (!ctx->popup.active || !ctx->popup.menu) {
|
if (!ctx->popup.active || !ctx->popup.menu) {
|
||||||
|
|
@ -2096,9 +2363,17 @@ static void openSubMenu(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// openSysMenu
|
// 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) {
|
static void openSysMenu(AppContextT *ctx, WindowT *win) {
|
||||||
// Close any existing popup menus first
|
|
||||||
closeAllPopups(ctx);
|
closeAllPopups(ctx);
|
||||||
|
|
||||||
if (ctx->sysMenu.active) {
|
if (ctx->sysMenu.active) {
|
||||||
|
|
@ -2108,8 +2383,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) {
|
||||||
|
|
||||||
ctx->sysMenu.itemCount = 0;
|
ctx->sysMenu.itemCount = 0;
|
||||||
ctx->sysMenu.windowId = win->id;
|
ctx->sysMenu.windowId = win->id;
|
||||||
|
|
||||||
// Restore — enabled only when maximized
|
|
||||||
SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
|
SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
|
||||||
strncpy(item->label, "&Restore", MAX_MENU_LABEL - 1);
|
strncpy(item->label, "&Restore", MAX_MENU_LABEL - 1);
|
||||||
item->cmd = SysMenuRestoreE;
|
item->cmd = SysMenuRestoreE;
|
||||||
|
|
@ -2194,6 +2467,15 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// pollAnsiTermWidgets — poll and repaint all ANSI term widgets
|
// 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) {
|
static void pollAnsiTermWidgets(AppContextT *ctx) {
|
||||||
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
||||||
|
|
@ -2240,30 +2522,51 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// pollKeyboard
|
// 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) {
|
static void pollKeyboard(AppContextT *ctx) {
|
||||||
// Read modifier state once per poll
|
|
||||||
int32_t shiftFlags = platformKeyboardGetModifiers();
|
int32_t shiftFlags = platformKeyboardGetModifiers();
|
||||||
ctx->keyModifiers = shiftFlags;
|
ctx->keyModifiers = shiftFlags;
|
||||||
bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift
|
bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift
|
||||||
|
|
||||||
// Process buffered keys
|
|
||||||
PlatformKeyEventT evt;
|
PlatformKeyEventT evt;
|
||||||
|
|
||||||
while (platformKeyboardRead(&evt)) {
|
while (platformKeyboardRead(&evt)) {
|
||||||
int32_t scancode = evt.scancode;
|
int32_t scancode = evt.scancode;
|
||||||
int32_t ascii = evt.ascii;
|
int32_t ascii = evt.ascii;
|
||||||
|
|
||||||
// Alt+Tab / Shift+Alt+Tab — cycle windows
|
// Alt+Tab / Shift+Alt+Tab — cycle windows.
|
||||||
// Alt+Tab: scancode=0xA5, ascii=0x00
|
// 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 (ascii == 0 && scancode == 0xA5) {
|
||||||
if (ctx->stack.count > 1) {
|
if (ctx->stack.count > 1) {
|
||||||
if (shiftHeld) {
|
if (shiftHeld) {
|
||||||
// Shift+Alt+Tab — focus the bottom window, raise it
|
|
||||||
wmRaiseWindow(&ctx->stack, &ctx->dirty, 0);
|
wmRaiseWindow(&ctx->stack, &ctx->dirty, 0);
|
||||||
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
|
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
|
||||||
} else {
|
} 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];
|
WindowT *top = ctx->stack.windows[ctx->stack.count - 1];
|
||||||
dirtyListAdd(&ctx->dirty, top->x, top->y, top->w, top->h);
|
dirtyListAdd(&ctx->dirty, top->x, top->y, top->w, top->h);
|
||||||
|
|
||||||
|
|
@ -2903,9 +3206,14 @@ static void pollMouse(AppContextT *ctx) {
|
||||||
// refreshMinimizedIcons
|
// refreshMinimizedIcons
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Dirty the next minimized window icon whose content has changed
|
// Minimized windows show a thumbnail of their content. When the content
|
||||||
// since the last refresh. Only considers windows without custom
|
// changes (e.g., a terminal receives output while minimized), the icon
|
||||||
// iconData. Called every ICON_REFRESH_INTERVAL frames to stagger.
|
// 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) {
|
static void refreshMinimizedIcons(AppContextT *ctx) {
|
||||||
WindowStackT *ws = &ctx->stack;
|
WindowStackT *ws = &ctx->stack;
|
||||||
|
|
@ -2944,6 +3252,12 @@ static void refreshMinimizedIcons(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// repositionWindow — move/resize a window, dirty old & new, fire callbacks
|
// 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) {
|
static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||||
// Dirty old position
|
// Dirty old position
|
||||||
|
|
@ -2980,6 +3294,19 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// updateCursorShape
|
// 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) {
|
static void updateCursorShape(AppContextT *ctx) {
|
||||||
int32_t newCursor = CURSOR_ARROW;
|
int32_t newCursor = CURSOR_ARROW;
|
||||||
|
|
@ -3099,6 +3426,16 @@ static void updateCursorShape(AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// updateTooltip — show/hide tooltip based on hover state
|
// 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) {
|
static void updateTooltip(AppContextT *ctx) {
|
||||||
clock_t now = clock();
|
clock_t now = clock();
|
||||||
|
|
|
||||||
107
dvx/dvxApp.h
107
dvx/dvxApp.h
|
|
@ -1,4 +1,15 @@
|
||||||
// dvx_app.h — Layer 5: Application API for DVX GUI
|
// 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
|
#ifndef DVX_APP_H
|
||||||
#define DVX_APP_H
|
#define DVX_APP_H
|
||||||
|
|
||||||
|
|
@ -13,6 +24,13 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Application context
|
// 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 {
|
typedef struct AppContextT {
|
||||||
DisplayT display;
|
DisplayT display;
|
||||||
|
|
@ -33,57 +51,88 @@ typedef struct AppContextT {
|
||||||
int32_t mouseY;
|
int32_t mouseY;
|
||||||
int32_t mouseButtons;
|
int32_t mouseButtons;
|
||||||
int32_t keyModifiers; // current BIOS shift state (KEY_MOD_xxx)
|
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 prevMouseX;
|
||||||
int32_t prevMouseY;
|
int32_t prevMouseY;
|
||||||
int32_t prevMouseButtons;
|
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;
|
clock_t lastIconClickTime;
|
||||||
int32_t lastIconClickId; // window ID of last-clicked minimized icon (-1 = none)
|
int32_t lastIconClickId; // window ID of last-clicked minimized icon (-1 = none)
|
||||||
clock_t lastCloseClickTime;
|
clock_t lastCloseClickTime;
|
||||||
int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none)
|
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 iconRefreshIdx; // next minimized icon to refresh (staggered)
|
||||||
int32_t frameCount; // frame counter for periodic tasks
|
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 (*idleCallback)(void *ctx); // called instead of yield when non-NULL
|
||||||
void *idleCtx;
|
void *idleCtx;
|
||||||
WindowT *modalWindow; // if non-NULL, only this window receives input
|
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
|
clock_t tooltipHoverStart; // when mouse stopped moving
|
||||||
const char *tooltipText; // text to show (NULL = hidden)
|
const char *tooltipText; // text to show (NULL = hidden)
|
||||||
int32_t tooltipX; // screen position
|
int32_t tooltipX; // screen position
|
||||||
int32_t tooltipY;
|
int32_t tooltipY;
|
||||||
int32_t tooltipW; // size (pre-computed)
|
int32_t tooltipW; // size (pre-computed)
|
||||||
int32_t tooltipH;
|
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
|
uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight
|
||||||
} AppContextT;
|
} 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);
|
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);
|
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);
|
void dvxRun(AppContextT *ctx);
|
||||||
|
|
||||||
// Process one iteration of the event loop.
|
// Process exactly one frame of the event loop. Provided for applications
|
||||||
// Returns true if the GUI is still running, false if it wants to exit.
|
// 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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win);
|
||||||
|
|
||||||
// Minimize a window (show as icon at bottom of screen)
|
// 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
|
// Request exit from main loop
|
||||||
void dvxQuit(AppContextT *ctx);
|
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);
|
int32_t dvxScreenshot(AppContextT *ctx, const char *path);
|
||||||
|
|
||||||
// Set window title
|
// 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)
|
// 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);
|
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);
|
AccelTableT *dvxCreateAccelTable(void);
|
||||||
|
|
||||||
// Free an accelerator table
|
// Free an accelerator table and its entries.
|
||||||
void dvxFreeAccelTable(AccelTableT *table);
|
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);
|
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);
|
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);
|
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);
|
void dvxTileWindowsH(AppContextT *ctx);
|
||||||
|
|
||||||
// Tile windows vertically (stacked top to bottom, full width)
|
// Vertical tile: stacked, full width, equal height.
|
||||||
void dvxTileWindowsV(AppContextT *ctx);
|
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);
|
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);
|
const char *dvxClipboardGet(int32_t *outLen);
|
||||||
|
|
||||||
#endif // DVX_APP_H
|
#endif // DVX_APP_H
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,33 @@
|
||||||
// dvx_comp.c — Layer 3: Dirty rectangle compositor for DVX GUI (optimized)
|
// 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 "dvxComp.h"
|
||||||
#include "platform/dvxPlatform.h"
|
#include "platform/dvxPlatform.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
// 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
|
#define DIRTY_MERGE_GAP 4
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -18,16 +41,33 @@ static inline void rectUnion(const RectT *a, const RectT *b, RectT *result);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dirtyListAdd
|
// dirtyListAdd
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Appends a dirty rect to the list. Uses a fixed-size array (MAX_DIRTY_RECTS
|
||||||
|
// = 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) {
|
void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||||
|
// Branch hint: degenerate rects are rare — callers usually validate first
|
||||||
if (__builtin_expect(w <= 0 || h <= 0, 0)) {
|
if (__builtin_expect(w <= 0 || h <= 0, 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overflow path: try merging, then fall back to a single bounding rect
|
||||||
if (__builtin_expect(dl->count >= MAX_DIRTY_RECTS, 0)) {
|
if (__builtin_expect(dl->count >= MAX_DIRTY_RECTS, 0)) {
|
||||||
dirtyListMerge(dl);
|
dirtyListMerge(dl);
|
||||||
|
|
||||||
if (dl->count >= MAX_DIRTY_RECTS) {
|
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];
|
RectT merged = dl->rects[0];
|
||||||
|
|
||||||
for (int32_t i = 1; i < dl->count; i++) {
|
for (int32_t i = 1; i < dl->count; i++) {
|
||||||
|
|
@ -72,16 +112,31 @@ void dirtyListInit(DirtyListT *dl) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dirtyListMerge
|
// 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) {
|
void dirtyListMerge(DirtyListT *dl) {
|
||||||
if (dl->count <= 1) {
|
if (dl->count <= 1) {
|
||||||
return;
|
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++) {
|
for (int32_t i = 0; i < dl->count; i++) {
|
||||||
int32_t restarts = 0;
|
int32_t restarts = 0;
|
||||||
bool merged = true;
|
bool merged = true;
|
||||||
|
|
@ -92,6 +147,7 @@ void dirtyListMerge(DirtyListT *dl) {
|
||||||
for (int32_t j = i + 1; j < dl->count; j++) {
|
for (int32_t j = i + 1; j < dl->count; j++) {
|
||||||
if (rectsOverlapOrAdjacent(&dl->rects[i], &dl->rects[j], DIRTY_MERGE_GAP)) {
|
if (rectsOverlapOrAdjacent(&dl->rects[i], &dl->rects[j], DIRTY_MERGE_GAP)) {
|
||||||
rectUnion(&dl->rects[i], &dl->rects[j], &dl->rects[i]);
|
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->rects[j] = dl->rects[dl->count - 1];
|
||||||
dl->count--;
|
dl->count--;
|
||||||
j--;
|
j--;
|
||||||
|
|
@ -108,6 +164,16 @@ void dirtyListMerge(DirtyListT *dl) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// flushRect
|
// 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) {
|
void flushRect(DisplayT *d, const RectT *r) {
|
||||||
platformFlushRect(d, r);
|
platformFlushRect(d, r);
|
||||||
|
|
@ -117,6 +183,12 @@ void flushRect(DisplayT *d, const RectT *r) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// rectIntersect
|
// 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) {
|
bool rectIntersect(const RectT *a, const RectT *b, RectT *result) {
|
||||||
int32_t ix1 = a->x > b->x ? a->x : b->x;
|
int32_t ix1 = a->x > b->x ? a->x : b->x;
|
||||||
|
|
@ -149,6 +221,15 @@ bool rectIsEmpty(const RectT *r) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// rectsOverlapOrAdjacent
|
// 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) {
|
static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_t gap) {
|
||||||
if (a->x + a->w + gap < b->x) { return false; }
|
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
|
// 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) {
|
static inline void rectUnion(const RectT *a, const RectT *b, RectT *result) {
|
||||||
int32_t x1 = a->x < b->x ? a->x : b->x;
|
int32_t x1 = a->x < b->x ? a->x : b->x;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,56 @@
|
||||||
// dvx_comp.h — Layer 3: Dirty rectangle compositor for DVX GUI
|
// 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
|
#ifndef DVX_COMP_H
|
||||||
#define DVX_COMP_H
|
#define DVX_COMP_H
|
||||||
|
|
||||||
#include "dvxTypes.h"
|
#include "dvxTypes.h"
|
||||||
|
|
||||||
// Initialize the dirty list
|
// Zero the dirty rect count. Called at the start of each frame.
|
||||||
void dirtyListInit(DirtyListT *dl);
|
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);
|
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);
|
void dirtyListMerge(DirtyListT *dl);
|
||||||
|
|
||||||
// Clear the dirty list
|
// Reset the dirty list to empty.
|
||||||
void dirtyListClear(DirtyListT *dl);
|
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);
|
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);
|
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);
|
bool rectIsEmpty(const RectT *r);
|
||||||
|
|
||||||
#endif // DVX_COMP_H
|
#endif // DVX_COMP_H
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,19 @@
|
||||||
// dvxCursor.h — Embedded mouse cursor bitmaps for DVX GUI
|
// 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
|
#ifndef DVX_CURSOR_H
|
||||||
#define 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
|
{ 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 };
|
static const CursorT dvxCursor = { 16, 16, 0, 0, cursorArrowAnd, cursorArrowXor };
|
||||||
|
|
||||||
#endif // DVX_CURSOR_H
|
#endif // DVX_CURSOR_H
|
||||||
|
|
|
||||||
194
dvx/dvxDialog.c
194
dvx/dvxDialog.c
|
|
@ -1,4 +1,24 @@
|
||||||
// dvxDialog.c — Modal dialogs for DVX GUI
|
// 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 "dvxDialog.h"
|
||||||
#include "platform/dvxPlatform.h"
|
#include "platform/dvxPlatform.h"
|
||||||
|
|
@ -17,12 +37,21 @@
|
||||||
// Constants
|
// 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_MAX_WIDTH 320
|
||||||
#define MSG_PADDING 8
|
#define MSG_PADDING 8
|
||||||
#define ICON_AREA_WIDTH 40
|
#define ICON_AREA_WIDTH 40
|
||||||
#define BUTTON_WIDTH 80
|
#define BUTTON_WIDTH 80
|
||||||
#define BUTTON_HEIGHT 24
|
#define BUTTON_HEIGHT 24
|
||||||
#define BUTTON_GAP 8
|
#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_SIZE 24 // icon glyph drawing area (pixels)
|
||||||
#define ICON_GLYPH_CENTER 12 // center of icon glyph (ICON_GLYPH_SIZE / 2)
|
#define ICON_GLYPH_CENTER 12 // center of icon glyph (ICON_GLYPH_SIZE / 2)
|
||||||
#define ICON_OUTER_R2 144 // outer circle radius squared (12*12)
|
#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)
|
// 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 {
|
typedef struct {
|
||||||
AppContextT *ctx;
|
AppContextT *ctx;
|
||||||
int32_t result;
|
int32_t result; // ID_OK, ID_CANCEL, ID_YES, ID_NO, etc.
|
||||||
bool done;
|
bool done; // set true to break the modal loop
|
||||||
const char *message;
|
const char *message;
|
||||||
int32_t iconType;
|
int32_t iconType;
|
||||||
int32_t textX;
|
int32_t textX; // pre-computed text origin (accounts for icon)
|
||||||
int32_t textY;
|
int32_t textY;
|
||||||
int32_t textMaxW;
|
int32_t textMaxW; // max text width for word wrapping
|
||||||
int32_t msgAreaH;
|
int32_t msgAreaH; // height of the message+icon area above the buttons
|
||||||
} MsgBoxStateT;
|
} MsgBoxStateT;
|
||||||
|
|
||||||
static MsgBoxStateT sMsgBox;
|
static MsgBoxStateT sMsgBox;
|
||||||
|
|
@ -77,6 +112,26 @@ static MsgBoxStateT sMsgBox;
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawIconGlyph — draw a simple icon shape
|
// 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) {
|
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color) {
|
||||||
if (iconType == MB_ICONINFO) {
|
if (iconType == MB_ICONINFO) {
|
||||||
|
|
@ -155,6 +210,20 @@ static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxMessageBox
|
// 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 dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
|
||||||
int32_t btnFlags = flags & 0x000F;
|
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 };
|
RectT fullRect = { 0, 0, win->contentW, win->contentH };
|
||||||
win->onPaint(win, &fullRect);
|
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;
|
WindowT *prevModal = ctx->modalWindow;
|
||||||
ctx->modalWindow = win;
|
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) {
|
while (!sMsgBox.done && ctx->running) {
|
||||||
dvxUpdate(ctx);
|
dvxUpdate(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up — restore previous modal
|
|
||||||
ctx->modalWindow = prevModal;
|
ctx->modalWindow = prevModal;
|
||||||
dvxDestroyWindow(ctx, win);
|
dvxDestroyWindow(ctx, win);
|
||||||
|
|
||||||
|
|
@ -351,6 +424,15 @@ static void onMsgBoxClose(WindowT *win) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// onMsgBoxPaint — custom paint: background + text/icon + widgets
|
// 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) {
|
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
|
||||||
(void)dirtyArea;
|
(void)dirtyArea;
|
||||||
|
|
@ -358,7 +440,9 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
|
||||||
MsgBoxStateT *state = (MsgBoxStateT *)win->userData;
|
MsgBoxStateT *state = (MsgBoxStateT *)win->userData;
|
||||||
AppContextT *ctx = state->ctx;
|
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;
|
DisplayT cd = ctx->display;
|
||||||
cd.lfb = win->contentBuf;
|
cd.lfb = win->contentBuf;
|
||||||
cd.backBuf = win->contentBuf;
|
cd.backBuf = win->contentBuf;
|
||||||
|
|
@ -406,6 +490,13 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wordWrapDraw — draw word-wrapped text
|
// 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) {
|
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) {
|
||||||
int32_t charW = font->charWidth;
|
int32_t 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
|
// 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) {
|
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) {
|
||||||
int32_t charW = font->charWidth;
|
int32_t charW = font->charWidth;
|
||||||
|
|
@ -517,24 +614,31 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// File dialog
|
// 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_ENTRIES 512
|
||||||
#define FD_MAX_PATH 260
|
#define FD_MAX_PATH 260
|
||||||
#define FD_NAME_LEN 64
|
#define FD_NAME_LEN 64
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
AppContextT *ctx;
|
AppContextT *ctx;
|
||||||
bool done;
|
bool done; // set true to break modal loop
|
||||||
bool accepted;
|
bool accepted; // true if user clicked OK/Open/Save
|
||||||
int32_t flags;
|
int32_t flags; // FD_SAVE, etc.
|
||||||
char curDir[FD_MAX_PATH];
|
char curDir[FD_MAX_PATH];
|
||||||
const FileFilterT *filters;
|
const FileFilterT *filters; // caller-provided filter list
|
||||||
int32_t filterCount;
|
int32_t filterCount;
|
||||||
int32_t activeFilter;
|
int32_t activeFilter; // index into filters[]
|
||||||
char *entryNames[FD_MAX_ENTRIES];
|
char *entryNames[FD_MAX_ENTRIES]; // heap-allocated, freed by fdFreeEntries
|
||||||
bool entryIsDir[FD_MAX_ENTRIES];
|
bool entryIsDir[FD_MAX_ENTRIES];
|
||||||
int32_t entryCount;
|
int32_t entryCount;
|
||||||
const char *listItems[FD_MAX_ENTRIES];
|
const char *listItems[FD_MAX_ENTRIES]; // pointers into entryNames, for listbox API
|
||||||
WidgetT *fileList;
|
WidgetT *fileList;
|
||||||
WidgetT *pathInput;
|
WidgetT *pathInput;
|
||||||
WidgetT *nameInput;
|
WidgetT *nameInput;
|
||||||
|
|
@ -548,6 +652,11 @@ static FileDialogStateT sFd;
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// fdFilterMatch — check if filename matches a glob pattern
|
// 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) {
|
static bool fdFilterMatch(const char *name, const char *pattern) {
|
||||||
if (!pattern || pattern[0] == '\0') {
|
if (!pattern || pattern[0] == '\0') {
|
||||||
|
|
@ -603,6 +712,12 @@ static void fdFreeEntries(void) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// fdEntryCompare — sort: dirs first, then alphabetical
|
// 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) {
|
static int fdEntryCompare(const void *a, const void *b) {
|
||||||
int32_t ia = *(const int32_t *)a;
|
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
|
// 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) {
|
static void fdLoadDir(void) {
|
||||||
fdFreeEntries();
|
fdFreeEntries();
|
||||||
|
|
@ -716,9 +838,14 @@ static void fdLoadDir(void) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// fdNavigate — change to a new directory
|
// 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) {
|
static void fdNavigate(const char *path) {
|
||||||
// Resolve relative paths
|
|
||||||
char resolved[FD_MAX_PATH];
|
char resolved[FD_MAX_PATH];
|
||||||
|
|
||||||
if (path[0] == '/' || path[0] == '\\' ||
|
if (path[0] == '/' || path[0] == '\\' ||
|
||||||
|
|
@ -773,6 +900,12 @@ static bool fdValidateFilename(const char *name) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// fdAcceptFile — confirm and accept the selected filename
|
// 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) {
|
static bool fdAcceptFile(const char *name) {
|
||||||
if (!fdValidateFilename(name)) {
|
if (!fdValidateFilename(name)) {
|
||||||
|
|
@ -829,6 +962,11 @@ static void fdOnListClick(WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// fdOnListDblClick — file list double-click
|
// 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) {
|
static void fdOnListDblClick(WidgetT *w) {
|
||||||
int32_t sel = wgtListBoxGetSelected(w);
|
int32_t sel = wgtListBoxGetSelected(w);
|
||||||
|
|
@ -838,7 +976,6 @@ static void fdOnListDblClick(WidgetT *w) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sFd.entryIsDir[sel]) {
|
if (sFd.entryIsDir[sel]) {
|
||||||
// Double-click on directory — navigate into it
|
|
||||||
const char *display = sFd.entryNames[sel];
|
const char *display = sFd.entryNames[sel];
|
||||||
char dirName[FD_NAME_LEN];
|
char dirName[FD_NAME_LEN];
|
||||||
|
|
||||||
|
|
@ -868,11 +1005,16 @@ static void fdOnListDblClick(WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// fdOnOk — OK button clicked
|
// 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) {
|
static void fdOnOk(WidgetT *w) {
|
||||||
(void)w;
|
(void)w;
|
||||||
|
|
||||||
// Check if a directory is selected in the list
|
|
||||||
int32_t sel = wgtListBoxGetSelected(sFd.fileList);
|
int32_t sel = wgtListBoxGetSelected(sFd.fileList);
|
||||||
|
|
||||||
if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) {
|
if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) {
|
||||||
|
|
@ -969,6 +1111,18 @@ static void fdOnPathSubmit(WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxFileDialog
|
// 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) {
|
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));
|
memset(&sFd, 0, sizeof(sFd));
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
// dvxDialog.h — Modal dialogs for DVX GUI
|
// 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
|
#ifndef DVX_DIALOG_H
|
||||||
#define DVX_DIALOG_H
|
#define DVX_DIALOG_H
|
||||||
|
|
||||||
#include "dvxApp.h"
|
#include "dvxApp.h"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Message box button flags
|
// Message box button flags (low nibble)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define MB_OK 0x0000
|
#define MB_OK 0x0000
|
||||||
|
|
@ -15,7 +24,7 @@
|
||||||
#define MB_RETRYCANCEL 0x0004
|
#define MB_RETRYCANCEL 0x0004
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Message box icon flags
|
// Message box icon flags (high nibble, OR with button flags)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define MB_ICONINFO 0x0010
|
#define MB_ICONINFO 0x0010
|
||||||
|
|
@ -33,8 +42,10 @@
|
||||||
#define ID_NO 4
|
#define ID_NO 4
|
||||||
#define ID_RETRY 5
|
#define ID_RETRY 5
|
||||||
|
|
||||||
// Show a modal message box and return which button was pressed.
|
// Display a modal message box with the specified button and icon combination.
|
||||||
// flags = MB_xxx button flag | MB_ICONxxx icon flag
|
// 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);
|
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
|
// 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 {
|
typedef struct {
|
||||||
const char *label; // e.g. "Text Files (*.txt)"
|
const char *label; // e.g. "Text Files (*.txt)"
|
||||||
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
|
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
|
||||||
} FileFilterT;
|
} FileFilterT;
|
||||||
|
|
||||||
// Show a modal file open/save dialog. Returns true if the user selected
|
// Display a modal file open/save dialog. The dialog shows a directory
|
||||||
// a file, false if cancelled. The selected path is written to outPath
|
// listing with navigation (parent directory, drive letters on DOS), a
|
||||||
// (buffer must be at least outPathSize bytes).
|
// filename text input, and an optional filter dropdown. Blocks the caller
|
||||||
// initialDir may be NULL (defaults to current directory).
|
// via dvxUpdate() loop. Returns true if the user selected a file (path
|
||||||
// filters/filterCount may be NULL/0 for no filter dropdown.
|
// 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);
|
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
|
#endif // DVX_DIALOG_H
|
||||||
|
|
|
||||||
325
dvx/dvxDraw.c
325
dvx/dvxDraw.c
|
|
@ -1,4 +1,53 @@
|
||||||
// dvx_draw.c — Layer 2: Drawing primitives for DVX GUI (optimized)
|
// 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 "dvxDraw.h"
|
||||||
#include "platform/dvxPlatform.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 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);
|
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 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};
|
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
|
// 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) {
|
char accelParse(const char *text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|
@ -68,6 +130,19 @@ char accelParse(const char *text) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// clipRect
|
// 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) {
|
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;
|
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
|
// 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) {
|
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;
|
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
|
// 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 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;
|
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 < clipX1) { colStart = clipX1 - x; }
|
||||||
if (x + cw > clipX2) { colEnd = clipX2 - x; }
|
if (x + cw > clipX2) { colEnd = clipX2 - x; }
|
||||||
|
|
||||||
// Unclipped fast path: full 8-pixel character cell with direct bit
|
// Unclipped fast path: when the character cell is fully within the
|
||||||
// tests eliminates loop overhead and sGlyphBit[] lookups (Item 4)
|
// 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);
|
bool unclipped = (colStart == 0 && colEnd == cw);
|
||||||
|
|
||||||
if (opaque) {
|
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) {
|
if (unclipped && bpp == 4) {
|
||||||
for (int32_t row = rowStart; row < rowEnd; row++) {
|
for (int32_t row = rowStart; row < rowEnd; row++) {
|
||||||
uint32_t *dst32 = (uint32_t *)(d->backBuf + (y + row) * pitch + x * 4);
|
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;
|
dst16[7] = (bits & 0x01) ? fg16 : bg16;
|
||||||
}
|
}
|
||||||
} else {
|
} 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++) {
|
for (int32_t row = rowStart; row < rowEnd; row++) {
|
||||||
int32_t py = y + row;
|
int32_t py = y + row;
|
||||||
uint8_t *dst = d->backBuf + py * pitch + (x + colStart) * bpp;
|
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 {
|
} 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) {
|
if (unclipped && bpp == 4) {
|
||||||
for (int32_t row = rowStart; row < rowEnd; row++) {
|
for (int32_t row = rowStart; row < rowEnd; row++) {
|
||||||
uint8_t bits = glyph[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.
|
// Same idea as drawTermRow but for uniform fg/bg text runs.
|
||||||
// Avoids per-character function call overhead, redundant clip
|
// Avoids per-character function call overhead, redundant clip
|
||||||
// calculation, and spanFill startup costs.
|
// 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) {
|
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) {
|
if (count <= 0) {
|
||||||
|
|
@ -492,6 +651,19 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawFocusRect
|
// 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) {
|
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;
|
int32_t bpp = ops->bytesPerPixel;
|
||||||
|
|
@ -550,6 +722,10 @@ void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawHLine
|
// 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) {
|
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);
|
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
|
// 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) {
|
void drawInit(BlitOpsT *ops, const DisplayT *d) {
|
||||||
ops->bytesPerPixel = d->format.bytesPerPixel;
|
ops->bytesPerPixel = d->format.bytesPerPixel;
|
||||||
|
|
@ -588,6 +784,22 @@ void drawInit(BlitOpsT *ops, const DisplayT *d) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawMaskedBitmap
|
// 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) {
|
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;
|
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
|
// lineData points to (ch, attr) pairs. palette is a 16-entry
|
||||||
// packed-color table. This avoids per-character function call
|
// packed-color table. This avoids per-character function call
|
||||||
// overhead, redundant clip calculation, and spanFill startup
|
// 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) {
|
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;
|
int32_t cw = font->charWidth;
|
||||||
|
|
@ -801,6 +1031,19 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawText
|
// 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) {
|
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;
|
int32_t cw = font->charWidth;
|
||||||
|
|
@ -828,6 +1071,16 @@ void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawTextAccel
|
// 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) {
|
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;
|
int32_t cw = font->charWidth;
|
||||||
|
|
@ -880,6 +1133,17 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawVLine
|
// 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 drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color) {
|
||||||
(void)ops;
|
(void)ops;
|
||||||
|
|
@ -923,6 +1187,12 @@ void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// putPixel
|
// 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) {
|
static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
|
||||||
if (bpp == 2) {
|
if (bpp == 2) {
|
||||||
|
|
@ -938,6 +1208,23 @@ static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// rectCopy
|
// 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) {
|
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;
|
int32_t bpp = ops->bytesPerPixel;
|
||||||
|
|
@ -977,6 +1264,18 @@ void rectCopy(DisplayT *d, const BlitOpsT *ops, int32_t dstX, int32_t dstY, cons
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// rectFill
|
// 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) {
|
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);
|
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
|
// 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 textWidth(const BitmapFontT *font, const char *text) {
|
||||||
int32_t w = 0;
|
int32_t w = 0;
|
||||||
|
|
@ -1014,6 +1319,12 @@ int32_t textWidth(const BitmapFontT *font, const char *text) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// textWidthAccel
|
// 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 textWidthAccel(const BitmapFontT *font, const char *text) {
|
||||||
int32_t w = 0;
|
int32_t w = 0;
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,95 @@
|
||||||
// dvx_draw.h — Layer 2: Drawing primitives for DVX GUI
|
// 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
|
#ifndef DVX_DRAW_H
|
||||||
#define DVX_DRAW_H
|
#define DVX_DRAW_H
|
||||||
|
|
||||||
#include "dvxTypes.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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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).
|
// Optimized batch text rendering for a known character count. Computes
|
||||||
// Much faster than calling drawChar per character: computes clip once,
|
// clip bounds once for the entire run, fills the background in a single
|
||||||
// fills background in bulk, then overlays glyph foreground pixels.
|
// 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);
|
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);
|
int32_t textWidth(const BitmapFontT *font, const char *text);
|
||||||
|
|
||||||
// Parse accelerator key from text with & markers (e.g. "E&xit" -> 'x')
|
// Scan text for an & prefix and return the following character as a
|
||||||
// Returns the lowercase accelerator character, or 0 if none found.
|
// lowercase accelerator key. "&File" -> 'f', "E&xit" -> 'x'.
|
||||||
|
// Used by menu and button construction to extract keyboard shortcuts.
|
||||||
char accelParse(const char *text);
|
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);
|
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);
|
int32_t textWidthAccel(const BitmapFontT *font, const char *text);
|
||||||
|
|
||||||
// Draw a 1-bit bitmap with mask (for cursors, icons)
|
// Draw a 1-bit AND/XOR masked bitmap. Used for software-rendered mouse
|
||||||
// andMask/xorData are arrays of uint16_t, one per row
|
// 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);
|
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).
|
// Render an entire row of terminal character cells (ch/attr byte pairs)
|
||||||
// Renders 'cols' cells starting at (x,y). Much faster than calling drawChar per cell.
|
// in a single pass. Each cell's foreground and background colors are
|
||||||
// cursorCol: column index to draw inverted (cursor), or -1 for no cursor.
|
// 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);
|
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);
|
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);
|
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);
|
void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color);
|
||||||
|
|
||||||
#endif // DVX_DRAW_H
|
#endif // DVX_DRAW_H
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,28 @@
|
||||||
// dvx_font.h — Embedded VGA bitmap font data (CP437) for DVX GUI
|
// 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
|
#ifndef DVX_FONT_H
|
||||||
#define DVX_FONT_H
|
#define DVX_FONT_H
|
||||||
|
|
||||||
|
|
@ -1051,6 +1075,13 @@ static const uint8_t font8x16[256 * 16] = {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Font descriptor structs
|
// 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 = {
|
static const BitmapFontT dvxFont8x14 = {
|
||||||
.charWidth = 8,
|
.charWidth = 8,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,22 @@
|
||||||
// dvxIcon.c — stb_image implementation for DVX GUI
|
// 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 push
|
||||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
// dvxImageWrite.c — stb_image_write implementation for DVX GUI
|
// 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 push
|
||||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
// dvx_palette.h — 8-bit mode palette definition for DVX GUI
|
// 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
|
#ifndef DVX_PALETTE_H
|
||||||
#define 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)
|
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 ri = (r + 25) / 51;
|
||||||
int32_t gi = (g + 25) / 51;
|
int32_t gi = (g + 25) / 51;
|
||||||
int32_t bi = (b + 25) / 51;
|
int32_t bi = (b + 25) / 51;
|
||||||
|
|
|
||||||
210
dvx/dvxTypes.h
210
dvx/dvxTypes.h
|
|
@ -1,4 +1,10 @@
|
||||||
// dvx_types.h — Shared type definitions for DVX GUI
|
// 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
|
#ifndef DVX_TYPES_H
|
||||||
#define DVX_TYPES_H
|
#define DVX_TYPES_H
|
||||||
|
|
||||||
|
|
@ -8,6 +14,14 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Pixel format descriptor
|
// 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 {
|
typedef struct {
|
||||||
int32_t bitsPerPixel; // 8, 15, 16, or 32
|
int32_t bitsPerPixel; // 8, 15, 16, or 32
|
||||||
|
|
@ -26,6 +40,25 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Display context
|
// 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 {
|
typedef struct {
|
||||||
int32_t width;
|
int32_t width;
|
||||||
|
|
@ -44,6 +77,11 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Rectangle
|
// 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 {
|
typedef struct {
|
||||||
int32_t x;
|
int32_t x;
|
||||||
|
|
@ -55,6 +93,19 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Span operation function pointers
|
// 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 (*SpanFillFnT)(uint8_t *dst, uint32_t color, int32_t count);
|
||||||
typedef void (*SpanCopyFnT)(uint8_t *dst, const uint8_t *src, int32_t count);
|
typedef void (*SpanCopyFnT)(uint8_t *dst, const uint8_t *src, int32_t count);
|
||||||
|
|
@ -69,6 +120,16 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Bevel style
|
// 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 {
|
typedef struct {
|
||||||
uint32_t highlight; // lighter color (top/left edges)
|
uint32_t highlight; // lighter color (top/left edges)
|
||||||
|
|
@ -85,6 +146,18 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Bitmap font
|
// 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
|
#define FONT_CHAR_WIDTH 8 // fixed glyph width in pixels
|
||||||
|
|
||||||
|
|
@ -99,6 +172,18 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Color scheme
|
// 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 {
|
typedef struct {
|
||||||
uint32_t desktop;
|
uint32_t desktop;
|
||||||
|
|
@ -124,6 +209,13 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Dirty rectangle list
|
// 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
|
#define MAX_DIRTY_RECTS 128
|
||||||
|
|
||||||
|
|
@ -135,6 +227,17 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Window chrome constants
|
// 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_BORDER_WIDTH 4
|
||||||
#define CHROME_TITLE_HEIGHT 20
|
#define CHROME_TITLE_HEIGHT 20
|
||||||
|
|
@ -149,6 +252,16 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Menu types
|
// 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_MENU_ITEMS 16
|
||||||
#define MAX_MENUS 8
|
#define MAX_MENUS 8
|
||||||
|
|
@ -174,6 +287,8 @@ typedef struct {
|
||||||
MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item)
|
MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item)
|
||||||
} MenuItemT;
|
} MenuItemT;
|
||||||
|
|
||||||
|
// MenuT is a named struct (not anonymous typedef) because MenuItemT
|
||||||
|
// needs a forward pointer to it for cascading submenus.
|
||||||
struct MenuT {
|
struct MenuT {
|
||||||
char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File")
|
char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File")
|
||||||
MenuItemT items[MAX_MENU_ITEMS];
|
MenuItemT items[MAX_MENU_ITEMS];
|
||||||
|
|
@ -183,6 +298,9 @@ struct MenuT {
|
||||||
char accelKey; // lowercase accelerator character, 0 if none
|
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 {
|
typedef struct {
|
||||||
MenuT menus[MAX_MENUS];
|
MenuT menus[MAX_MENUS];
|
||||||
int32_t menuCount;
|
int32_t menuCount;
|
||||||
|
|
@ -192,6 +310,14 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Scrollbar types
|
// 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 {
|
typedef enum {
|
||||||
ScrollbarVerticalE,
|
ScrollbarVerticalE,
|
||||||
|
|
@ -214,6 +340,16 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Accelerator table (global hotkeys)
|
// 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
|
#define MAX_ACCEL_ENTRIES 32
|
||||||
|
|
||||||
|
|
@ -222,7 +358,8 @@ typedef struct {
|
||||||
#define ACCEL_CTRL 0x04
|
#define ACCEL_CTRL 0x04
|
||||||
#define ACCEL_ALT 0x08
|
#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_F1 (0x3B | 0x100)
|
||||||
#define KEY_F2 (0x3C | 0x100)
|
#define KEY_F2 (0x3C | 0x100)
|
||||||
#define KEY_F3 (0x3D | 0x100)
|
#define KEY_F3 (0x3D | 0x100)
|
||||||
|
|
@ -258,6 +395,20 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Window
|
// 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_WINDOWS 64
|
||||||
#define MAX_TITLE_LEN 128
|
#define MAX_TITLE_LEN 128
|
||||||
|
|
@ -271,6 +422,8 @@ typedef struct {
|
||||||
#define MIN_WINDOW_W 80
|
#define MIN_WINDOW_W 80
|
||||||
#define MIN_WINDOW_H 60
|
#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_SIZE 64
|
||||||
#define ICON_BORDER 2
|
#define ICON_BORDER 2
|
||||||
#define ICON_TOTAL_SIZE (ICON_SIZE + 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
|
bool contentDirty; // true when contentBuf has changed since last icon refresh
|
||||||
int32_t maxW; // maximum width (-1 = screen width)
|
int32_t maxW; // maximum width (-1 = screen width)
|
||||||
int32_t maxH; // maximum height (-1 = screen height)
|
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 preMaxX; // saved position before maximize
|
||||||
int32_t preMaxY;
|
int32_t preMaxY;
|
||||||
int32_t preMaxW;
|
int32_t preMaxW;
|
||||||
|
|
@ -328,7 +483,11 @@ typedef struct WindowT {
|
||||||
// Accelerator table (NULL if none, caller owns allocation)
|
// Accelerator table (NULL if none, caller owns allocation)
|
||||||
AccelTableT *accelTable;
|
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 *userData;
|
||||||
void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
|
void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
|
||||||
void (*onKey)(struct WindowT *win, int32_t key, int32_t mod);
|
void (*onKey)(struct WindowT *win, int32_t key, int32_t mod);
|
||||||
|
|
@ -342,6 +501,19 @@ typedef struct WindowT {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Window stack
|
// 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 {
|
typedef struct {
|
||||||
WindowT *windows[MAX_WINDOWS];
|
WindowT *windows[MAX_WINDOWS];
|
||||||
|
|
@ -360,6 +532,17 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Mouse cursor
|
// 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 {
|
typedef struct {
|
||||||
int32_t width;
|
int32_t width;
|
||||||
|
|
@ -373,6 +556,17 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Popup state for dropdown menus (with cascading submenu support)
|
// 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
|
#define MAX_SUBMENU_DEPTH 4
|
||||||
|
|
||||||
|
|
@ -405,6 +599,12 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// System menu (control menu / close box menu)
|
// 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 {
|
typedef enum {
|
||||||
SysMenuRestoreE = 1,
|
SysMenuRestoreE = 1,
|
||||||
|
|
@ -440,6 +640,12 @@ typedef struct {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Keyboard move/resize mode
|
// 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 {
|
typedef enum {
|
||||||
KbModeNoneE = 0,
|
KbModeNoneE = 0,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,43 @@
|
||||||
// Platform-independent video utilities. The actual VESA/VBE code
|
// Platform-independent video utilities. The actual VESA/VBE code
|
||||||
// now lives in dvxPlatformDos.c (or the platform file for whatever
|
// now lives in dvxPlatformDos.c (or the platform file for whatever
|
||||||
// OS we're targeting).
|
// 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 "dvxVideo.h"
|
||||||
#include "platform/dvxPlatform.h"
|
#include "platform/dvxPlatform.h"
|
||||||
|
|
@ -14,12 +51,30 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// packColor
|
// 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) {
|
uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) {
|
||||||
if (d->format.bitsPerPixel == 8) {
|
if (d->format.bitsPerPixel == 8) {
|
||||||
return dvxNearestPalEntry(d->palette, r, g, b);
|
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 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 gv = ((uint32_t)g >> (8 - d->format.greenBits)) << d->format.greenShift;
|
||||||
uint32_t bv = ((uint32_t)b >> (8 - d->format.blueBits)) << d->format.blueShift;
|
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
|
// resetClipRect
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Restores the clip rect to the full screen. Called after the
|
||||||
|
// compositor finishes flushing dirty rects, and during init.
|
||||||
|
|
||||||
void resetClipRect(DisplayT *d) {
|
void resetClipRect(DisplayT *d) {
|
||||||
d->clipX = 0;
|
d->clipX = 0;
|
||||||
|
|
@ -43,6 +101,19 @@ void resetClipRect(DisplayT *d) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// setClipRect
|
// 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) {
|
void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||||
int32_t x2 = x + w;
|
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
|
// 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) {
|
int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
|
||||||
return platformVideoInit(d, requestedW, requestedH, 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
|
// 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) {
|
void videoShutdown(DisplayT *d) {
|
||||||
platformVideoShutdown(d);
|
platformVideoShutdown(d);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,48 @@
|
||||||
// dvx_video.h — Layer 1: VESA VBE video backend for DVX GUI
|
// 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
|
#ifndef DVX_VIDEO_H
|
||||||
#define DVX_VIDEO_H
|
#define DVX_VIDEO_H
|
||||||
|
|
||||||
#include "dvxTypes.h"
|
#include "dvxTypes.h"
|
||||||
|
|
||||||
// Initialize VESA video mode and map LFB
|
// Probes VBE for a mode matching the requested resolution and depth,
|
||||||
// Returns 0 on success, -1 on failure (error message printed to stderr)
|
// 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);
|
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);
|
void videoShutdown(DisplayT *d);
|
||||||
|
|
||||||
// Pack an RGB color into the display's pixel format
|
// Pack an RGB triplet into the display's native pixel format.
|
||||||
// For 15/16/32-bit: returns packed pixel value
|
// For direct-color modes (15/16/32 bpp): returns a packed pixel value
|
||||||
// For 8-bit: returns nearest palette index
|
// 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);
|
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);
|
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);
|
void resetClipRect(DisplayT *d);
|
||||||
|
|
||||||
#endif // DVX_VIDEO_H
|
#endif // DVX_VIDEO_H
|
||||||
|
|
|
||||||
204
dvx/dvxWidget.h
204
dvx/dvxWidget.h
|
|
@ -1,4 +1,33 @@
|
||||||
// dvxWidget.h — Widget system for DVX GUI
|
// 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
|
#ifndef DVX_WIDGET_H
|
||||||
#define DVX_WIDGET_H
|
#define DVX_WIDGET_H
|
||||||
|
|
||||||
|
|
@ -14,9 +43,15 @@ struct WidgetClassT;
|
||||||
// Size specifications
|
// Size specifications
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Tagged size values encode both a unit type and a numeric value.
|
// Tagged size values encode both a unit type and a numeric value in a
|
||||||
// Use wgtPixels(), wgtChars(), or wgtPercent() to create them.
|
// single int32_t. The top 2 bits select the unit (pixels, character widths,
|
||||||
// A raw 0 means "auto" (use the widget's natural size).
|
// 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_TYPE_MASK 0xC0000000
|
||||||
#define WGT_SIZE_VAL_MASK 0x3FFFFFFF
|
#define WGT_SIZE_VAL_MASK 0x3FFFFFFF
|
||||||
|
|
@ -39,6 +74,11 @@ static inline int32_t wgtPercent(int32_t v) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Widget type enum
|
// 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 {
|
typedef enum {
|
||||||
WidgetVBoxE,
|
WidgetVBoxE,
|
||||||
|
|
@ -135,7 +175,19 @@ typedef enum {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Large widget data (separately allocated to reduce WidgetT size)
|
// 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 {
|
typedef struct {
|
||||||
uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes
|
uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes
|
||||||
int32_t cols; // columns (default 80)
|
int32_t cols; // columns (default 80)
|
||||||
|
|
@ -156,7 +208,10 @@ typedef struct {
|
||||||
// Scrolling region (0-based, inclusive)
|
// Scrolling region (0-based, inclusive)
|
||||||
int32_t scrollTop; // top row of scroll region
|
int32_t scrollTop; // top row of scroll region
|
||||||
int32_t scrollBot; // bottom 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
|
uint8_t *scrollback; // circular buffer of scrollback lines
|
||||||
int32_t scrollbackMax; // max lines in scrollback buffer
|
int32_t scrollbackMax; // max lines in scrollback buffer
|
||||||
int32_t scrollbackCount; // current number of lines stored
|
int32_t scrollbackCount; // current number of lines stored
|
||||||
|
|
@ -168,11 +223,17 @@ typedef struct {
|
||||||
// Cursor blink
|
// Cursor blink
|
||||||
bool cursorOn; // current cursor blink phase
|
bool cursorOn; // current cursor blink phase
|
||||||
clock_t cursorTime; // timestamp of last cursor toggle
|
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
|
uint32_t dirtyRows; // bitmask of rows needing repaint
|
||||||
int32_t lastCursorRow; // cursor row at last repaint
|
int32_t lastCursorRow; // cursor row at last repaint
|
||||||
int32_t lastCursorCol; // cursor col 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];
|
uint32_t packedPalette[16];
|
||||||
bool paletteValid;
|
bool paletteValid;
|
||||||
// Selection (line indices in scrollback+screen space)
|
// Selection (line indices in scrollback+screen space)
|
||||||
|
|
@ -181,12 +242,21 @@ typedef struct {
|
||||||
int32_t selEndLine;
|
int32_t selEndLine;
|
||||||
int32_t selEndCol;
|
int32_t selEndCol;
|
||||||
bool selecting;
|
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;
|
void *commCtx;
|
||||||
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);
|
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);
|
||||||
int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len);
|
int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len);
|
||||||
} AnsiTermDataT;
|
} 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 {
|
typedef struct {
|
||||||
const ListViewColT *cols;
|
const ListViewColT *cols;
|
||||||
int32_t colCount;
|
int32_t colCount;
|
||||||
|
|
@ -217,8 +287,15 @@ typedef struct {
|
||||||
|
|
||||||
typedef struct WidgetT {
|
typedef struct WidgetT {
|
||||||
WidgetTypeE type;
|
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;
|
const struct WidgetClassT *wclass;
|
||||||
char name[MAX_WIDGET_NAME];
|
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
|
uint32_t nameHash; // djb2 hash of name, 0 if unnamed
|
||||||
|
|
||||||
// Tree linkage
|
// Tree linkage
|
||||||
|
|
@ -234,17 +311,27 @@ typedef struct WidgetT {
|
||||||
int32_t w;
|
int32_t w;
|
||||||
int32_t h;
|
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 calcMinW;
|
||||||
int32_t calcMinH;
|
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 minW;
|
||||||
int32_t minH;
|
int32_t minH;
|
||||||
int32_t maxW; // 0 = no limit
|
int32_t maxW; // 0 = no limit
|
||||||
int32_t maxH;
|
int32_t maxH;
|
||||||
int32_t prefW; // preferred size, 0 = auto
|
int32_t prefW; // preferred size, 0 = auto
|
||||||
int32_t prefH;
|
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)
|
int32_t weight; // extra-space distribution (0 = fixed, 100 = normal)
|
||||||
|
|
||||||
// Container properties
|
// Container properties
|
||||||
|
|
@ -270,7 +357,12 @@ typedef struct WidgetT {
|
||||||
void (*onChange)(struct WidgetT *w);
|
void (*onChange)(struct WidgetT *w);
|
||||||
void (*onDblClick)(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 {
|
union {
|
||||||
struct {
|
struct {
|
||||||
const char *text;
|
const char *text;
|
||||||
|
|
@ -295,6 +387,11 @@ typedef struct WidgetT {
|
||||||
int32_t index;
|
int32_t index;
|
||||||
} radio;
|
} 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 {
|
struct {
|
||||||
char *buf;
|
char *buf;
|
||||||
int32_t bufSize;
|
int32_t bufSize;
|
||||||
|
|
@ -310,6 +407,11 @@ typedef struct WidgetT {
|
||||||
const char *mask; // format mask for InputMaskedE
|
const char *mask; // format mask for InputMaskedE
|
||||||
} textInput;
|
} 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 {
|
struct {
|
||||||
char *buf;
|
char *buf;
|
||||||
int32_t bufSize;
|
int32_t bufSize;
|
||||||
|
|
@ -404,6 +506,11 @@ typedef struct WidgetT {
|
||||||
const char *title;
|
const char *title;
|
||||||
} tabPage;
|
} 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 {
|
struct {
|
||||||
int32_t scrollPos;
|
int32_t scrollPos;
|
||||||
int32_t scrollPosH;
|
int32_t scrollPosH;
|
||||||
|
|
@ -488,13 +595,23 @@ typedef struct WidgetT {
|
||||||
// Window integration
|
// Window integration
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Set up a window for widgets. Returns the root container (VBox).
|
// Initialize the widget system for a window. Creates a root VBox container
|
||||||
// Automatically installs onPaint, onMouse, onKey, and onResize handlers.
|
// 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);
|
WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Container creation
|
// 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 *wgtVBox(WidgetT *parent);
|
||||||
WidgetT *wgtHBox(WidgetT *parent);
|
WidgetT *wgtHBox(WidgetT *parent);
|
||||||
|
|
@ -514,6 +631,11 @@ WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Radio buttons
|
// 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 *wgtRadioGroup(WidgetT *parent);
|
||||||
WidgetT *wgtRadio(WidgetT *parent, const char *text);
|
WidgetT *wgtRadio(WidgetT *parent, const char *text);
|
||||||
|
|
@ -521,6 +643,11 @@ WidgetT *wgtRadio(WidgetT *parent, const char *text);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Spacing and visual dividers
|
// 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 *wgtSpacer(WidgetT *parent);
|
||||||
WidgetT *wgtHSeparator(WidgetT *parent);
|
WidgetT *wgtHSeparator(WidgetT *parent);
|
||||||
|
|
@ -702,11 +829,19 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ANSI Terminal
|
// 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)
|
// Create an ANSI terminal widget (0 for cols/rows = 80x25 default)
|
||||||
WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows);
|
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);
|
void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len);
|
||||||
|
|
||||||
// Clear the terminal screen and reset cursor to home
|
// 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.
|
// Poll the comm interface for incoming data and process it. Returns bytes processed.
|
||||||
int32_t wgtAnsiTermPoll(WidgetT *w);
|
int32_t wgtAnsiTermPoll(WidgetT *w);
|
||||||
|
|
||||||
// Fast repaint: renders only dirty rows directly into the window's content
|
// Fast-path repaint for the terminal widget. Instead of going through the
|
||||||
// buffer, bypassing the full widget paint pipeline. Returns number of rows
|
// full widget paint pipeline (which would repaint the entire widget), this
|
||||||
// repainted (0 if nothing was dirty). If outY/outH are non-NULL, they receive
|
// renders only the dirty rows (tracked via the dirtyRows bitmask) directly
|
||||||
// the content-buffer-relative Y and height of the repainted region.
|
// 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);
|
int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Operations
|
// 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);
|
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);
|
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);
|
void wgtInvalidatePaint(WidgetT *w);
|
||||||
|
|
||||||
// Set/get widget text (label, button, textInput, etc.)
|
// 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)
|
// 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);
|
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);
|
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);
|
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
|
|
||||||
#endif // DVX_WIDGET_H
|
#endif // DVX_WIDGET_H
|
||||||
|
|
|
||||||
620
dvx/dvxWm.c
620
dvx/dvxWm.c
File diff suppressed because it is too large
Load diff
138
dvx/dvxWm.h
138
dvx/dvxWm.h
|
|
@ -1,106 +1,170 @@
|
||||||
// dvx_wm.h — Layer 4: Window manager for DVX GUI
|
// 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
|
#ifndef DVX_WM_H
|
||||||
#define DVX_WM_H
|
#define DVX_WM_H
|
||||||
|
|
||||||
#include "dvxTypes.h"
|
#include "dvxTypes.h"
|
||||||
|
|
||||||
// Initialize window stack
|
// Zero the window stack. Must be called before any other wm function.
|
||||||
void wmInit(WindowStackT *stack);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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)?
|
// Determine which window and which part of that window is under the given
|
||||||
// Returns stack index or -1 if no window hit
|
// screen coordinates. Iterates the stack front-to-back (highest Z first)
|
||||||
// Sets *hitPart: 0=content, 1=title, 2=close button, 3=resize edge,
|
// 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
|
// 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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
void wmResizeEnd(WindowStackT *stack);
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
void wmSetTitle(WindowT *win, DirtyListT *dl, const char *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);
|
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);
|
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);
|
void wmScrollbarEnd(WindowStackT *stack);
|
||||||
|
|
||||||
// Maximize a window (saves current geometry, expands to screen/max bounds)
|
// 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)
|
// Load an icon image for a window (converts to display pixel format)
|
||||||
int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d);
|
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);
|
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);
|
void wmFreeMenu(MenuT *menu);
|
||||||
|
|
||||||
#endif // DVX_WM_H
|
#endif // DVX_WM_H
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,22 @@
|
||||||
// All OS-specific and CPU-specific code is isolated behind this
|
// 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.
|
// 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
|
#ifndef DVX_PLATFORM_H
|
||||||
#define DVX_PLATFORM_H
|
#define DVX_PLATFORM_H
|
||||||
|
|
||||||
|
|
@ -11,6 +27,10 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Keyboard event
|
// 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 {
|
typedef struct {
|
||||||
int32_t ascii; // ASCII value, 0 for extended/function keys
|
int32_t ascii; // ASCII value, 0 for extended/function keys
|
||||||
|
|
@ -21,40 +41,68 @@ typedef struct {
|
||||||
// System lifecycle
|
// 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);
|
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);
|
void platformYield(void);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Video
|
// Video
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Initialise video mode, map framebuffer, allocate backbuffer.
|
// Probe for a suitable video mode, enable it, map the framebuffer, and
|
||||||
// Fills in all DisplayT fields on success. Returns 0/-1.
|
// 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);
|
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);
|
void platformVideoShutdown(DisplayT *d);
|
||||||
|
|
||||||
// Enumerate available graphics modes. Calls cb(w, h, bpp, userData) for
|
// Enumerate LFB-capable graphics modes. The callback is invoked for each
|
||||||
// each LFB-capable graphics mode found.
|
// 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);
|
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);
|
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Framebuffer flush
|
// 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);
|
void platformFlushRect(const DisplayT *d, const RectT *r);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Optimised memory operations (span fill / copy)
|
// 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 platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count);
|
||||||
void platformSpanFill16(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
|
// 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);
|
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);
|
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Input — Keyboard
|
// Input — Keyboard
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Return current modifier flags (BIOS shift-state format:
|
// Return the current modifier key state in BIOS shift-state format:
|
||||||
// bits 0-1 = shift, bit 2 = ctrl, bit 3 = alt).
|
// 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);
|
int32_t platformKeyboardGetModifiers(void);
|
||||||
|
|
||||||
// Read the next key from the keyboard buffer.
|
// Non-blocking read of the next key from the keyboard buffer. Returns
|
||||||
// Returns true if a key was available, false if the buffer was empty.
|
// true if a key was available. On DOS this uses INT 16h AH=11h (check)
|
||||||
// Normalises extended-key markers (e.g. 0xE0 → 0).
|
// + 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);
|
bool platformKeyboardRead(PlatformKeyEventT *evt);
|
||||||
|
|
||||||
// Map a scan code to its Alt+letter ASCII character.
|
// Translate an Alt+key scancode to its corresponding ASCII character.
|
||||||
// Returns the lowercase letter if the scan code corresponds to an
|
// When Alt is held, DOS doesn't provide the ASCII value — only the
|
||||||
// Alt+letter/digit combo, or 0 if it does not.
|
// 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);
|
char platformAltScanToChar(int32_t scancode);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// File system
|
// File system
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Validate a filename for the current platform.
|
// Validate a filename against platform-specific rules. On DOS this
|
||||||
// Returns NULL if valid, or a human-readable error string if invalid.
|
// 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);
|
const char *platformValidateFilename(const char *name);
|
||||||
|
|
||||||
#endif // DVX_PLATFORM_H
|
#endif // DVX_PLATFORM_H
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,29 @@
|
||||||
//
|
//
|
||||||
// All BIOS calls, DPMI functions, port I/O, inline assembly, and
|
// All BIOS calls, DPMI functions, port I/O, inline assembly, and
|
||||||
// DOS-specific file handling are isolated in this single file.
|
// 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 "dvxPlatform.h"
|
||||||
#include "../dvxPalette.h"
|
#include "../dvxPalette.h"
|
||||||
|
|
@ -12,6 +35,7 @@
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
|
// DJGPP-specific headers — this is the ONLY file that includes these
|
||||||
#include <dpmi.h>
|
#include <dpmi.h>
|
||||||
#include <go32.h>
|
#include <go32.h>
|
||||||
#include <pc.h>
|
#include <pc.h>
|
||||||
|
|
@ -30,6 +54,10 @@ static int32_t setVesaMode(uint16_t mode);
|
||||||
|
|
||||||
// Alt+key scan code to ASCII lookup table (indexed by BIOS scan code).
|
// 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.
|
// 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] = {
|
static const char sAltScanToAscii[256] = {
|
||||||
// Alt+letters
|
// Alt+letters
|
||||||
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
|
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
|
||||||
|
|
@ -49,6 +77,25 @@ static const char sAltScanToAscii[256] = {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// findBestMode
|
// 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) {
|
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d) {
|
||||||
__dpmi_regs r;
|
__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));
|
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 infoSeg = __tb >> 4;
|
||||||
uint32_t infoOff = __tb & 0x0F;
|
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 + 0, 'V');
|
||||||
_farpokeb(_dos_ds, __tb + 1, 'B');
|
_farpokeb(_dos_ds, __tb + 1, 'B');
|
||||||
_farpokeb(_dos_ds, __tb + 2, 'E');
|
_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;
|
r.x.di = infoOff;
|
||||||
__dpmi_int(0x10, &r);
|
__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) {
|
if (r.x.ax != 0x004F) {
|
||||||
fprintf(stderr, "VBE: Function 0x4F00 failed (AX=0x%04X)\n", r.x.ax);
|
fprintf(stderr, "VBE: Function 0x4F00 failed (AX=0x%04X)\n", r.x.ax);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify VBE signature
|
// On success the BIOS overwrites "VBE2" with "VESA" in the buffer
|
||||||
char sig[5];
|
char sig[5];
|
||||||
for (int32_t i = 0; i < 4; i++) {
|
for (int32_t i = 0; i < 4; i++) {
|
||||||
sig[i] = _farpeekb(_dos_ds, __tb + 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;
|
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);
|
uint16_t vbeVersion = _farpeekw(_dos_ds, __tb + 4);
|
||||||
if (vbeVersion < 0x0200) {
|
if (vbeVersion < 0x0200) {
|
||||||
fprintf(stderr, "VBE: Version %d.%d too old (need 2.0+)\n",
|
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;
|
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 modeListOff = _farpeekw(_dos_ds, __tb + 14);
|
||||||
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
|
uint16_t modeListSeg = _farpeekw(_dos_ds, __tb + 16);
|
||||||
uint32_t modeListAddr = ((uint32_t)modeListSeg << 4) + modeListOff;
|
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++) {
|
for (int32_t i = 0; i < 256; i++) {
|
||||||
uint16_t mode = _farpeekw(_dos_ds, modeListAddr + i * 2);
|
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
|
// 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) {
|
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
|
||||||
__dpmi_regs r;
|
__dpmi_regs r;
|
||||||
|
|
@ -218,15 +296,15 @@ static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requ
|
||||||
return;
|
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);
|
uint16_t attr = _farpeekw(_dos_ds, __tb + 0);
|
||||||
|
|
||||||
// Must have LFB support (bit 7)
|
|
||||||
if (!(attr & 0x0080)) {
|
if (!(attr & 0x0080)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be a graphics mode (bit 4)
|
|
||||||
if (!(attr & 0x0010)) {
|
if (!(attr & 0x0010)) {
|
||||||
return;
|
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.bitsPerPixel = bpp;
|
||||||
d->format.bytesPerPixel = (bpp + 7) / 8;
|
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) {
|
if (bpp >= 15) {
|
||||||
int32_t redSize = _farpeekb(_dos_ds, __tb + 31);
|
int32_t redSize = _farpeekb(_dos_ds, __tb + 31);
|
||||||
int32_t redPos = _farpeekb(_dos_ds, __tb + 32);
|
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
|
// 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) {
|
static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
|
||||||
__dpmi_meminfo info;
|
__dpmi_meminfo info;
|
||||||
|
|
@ -323,18 +431,17 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock the region to prevent paging
|
|
||||||
__dpmi_meminfo lockInfo;
|
__dpmi_meminfo lockInfo;
|
||||||
lockInfo.address = info.address;
|
lockInfo.address = info.address;
|
||||||
lockInfo.size = fbSize;
|
lockInfo.size = fbSize;
|
||||||
__dpmi_lock_linear_region(&lockInfo);
|
__dpmi_lock_linear_region(&lockInfo);
|
||||||
|
|
||||||
// Enable near pointers for direct access
|
|
||||||
if (__djgpp_nearptr_enable() == 0) {
|
if (__djgpp_nearptr_enable() == 0) {
|
||||||
fprintf(stderr, "VBE: Failed to enable near pointers\n");
|
fprintf(stderr, "VBE: Failed to enable near pointers\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert linear address to near pointer by adding the DS base offset
|
||||||
d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base);
|
d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -357,6 +464,26 @@ char platformAltScanToChar(int32_t scancode) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformFlushRect
|
// 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) {
|
void platformFlushRect(const DisplayT *d, const RectT *r) {
|
||||||
int32_t bpp = d->format.bytesPerPixel;
|
int32_t bpp = d->format.bytesPerPixel;
|
||||||
|
|
@ -421,7 +548,9 @@ void platformFlushRect(const DisplayT *d, const RectT *r) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void platformInit(void) {
|
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);
|
signal(SIGINT, SIG_IGN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +558,13 @@ void platformInit(void) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformKeyboardGetModifiers
|
// 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) {
|
int32_t platformKeyboardGetModifiers(void) {
|
||||||
__dpmi_regs r;
|
__dpmi_regs r;
|
||||||
|
|
@ -444,29 +580,43 @@ int32_t platformKeyboardGetModifiers(void) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformKeyboardRead
|
// 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) {
|
bool platformKeyboardRead(PlatformKeyEventT *evt) {
|
||||||
__dpmi_regs r;
|
__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;
|
r.x.ax = 0x1100;
|
||||||
__dpmi_int(0x16, &r);
|
__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) {
|
if (r.x.flags & 0x40) {
|
||||||
return false;
|
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;
|
r.x.ax = 0x1000;
|
||||||
__dpmi_int(0x16, &r);
|
__dpmi_int(0x16, &r);
|
||||||
|
|
||||||
evt->scancode = (r.x.ax >> 8) & 0xFF;
|
evt->scancode = (r.x.ax >> 8) & 0xFF;
|
||||||
evt->ascii = r.x.ax & 0xFF;
|
evt->ascii = r.x.ax & 0xFF;
|
||||||
|
|
||||||
// Enhanced INT 16h returns ascii=0xE0 for grey/extended keys
|
// Enhanced INT 16h uses 0xE0 as the ASCII byte for grey/extended
|
||||||
// (arrows, Home, End, Insert, Delete, etc. on 101-key keyboards).
|
// keys (arrows, Home, End, Insert, Delete on 101-key keyboards).
|
||||||
// Normalize to 0 so all extended key checks work uniformly.
|
// 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) {
|
if (evt->ascii == 0xE0) {
|
||||||
evt->ascii = 0;
|
evt->ascii = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -478,30 +628,42 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformMouseInit
|
// 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) {
|
void platformMouseInit(int32_t screenW, int32_t screenH) {
|
||||||
__dpmi_regs r;
|
__dpmi_regs r;
|
||||||
|
|
||||||
// Reset mouse driver
|
// Function 00h: reset driver, detect mouse hardware
|
||||||
memset(&r, 0, sizeof(r));
|
memset(&r, 0, sizeof(r));
|
||||||
r.x.ax = 0x0000;
|
r.x.ax = 0x0000;
|
||||||
__dpmi_int(0x33, &r);
|
__dpmi_int(0x33, &r);
|
||||||
|
|
||||||
// Set horizontal range
|
// Function 07h: set horizontal min/max range
|
||||||
memset(&r, 0, sizeof(r));
|
memset(&r, 0, sizeof(r));
|
||||||
r.x.ax = 0x0007;
|
r.x.ax = 0x0007;
|
||||||
r.x.cx = 0;
|
r.x.cx = 0;
|
||||||
r.x.dx = screenW - 1;
|
r.x.dx = screenW - 1;
|
||||||
__dpmi_int(0x33, &r);
|
__dpmi_int(0x33, &r);
|
||||||
|
|
||||||
// Set vertical range
|
// Function 08h: set vertical min/max range
|
||||||
memset(&r, 0, sizeof(r));
|
memset(&r, 0, sizeof(r));
|
||||||
r.x.ax = 0x0008;
|
r.x.ax = 0x0008;
|
||||||
r.x.cx = 0;
|
r.x.cx = 0;
|
||||||
r.x.dx = screenH - 1;
|
r.x.dx = screenH - 1;
|
||||||
__dpmi_int(0x33, &r);
|
__dpmi_int(0x33, &r);
|
||||||
|
|
||||||
// Position cursor at center
|
// Function 04h: warp cursor to center of screen
|
||||||
memset(&r, 0, sizeof(r));
|
memset(&r, 0, sizeof(r));
|
||||||
r.x.ax = 0x0004;
|
r.x.ax = 0x0004;
|
||||||
r.x.cx = screenW / 2;
|
r.x.cx = screenW / 2;
|
||||||
|
|
@ -513,6 +675,16 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformMousePoll
|
// 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) {
|
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
|
||||||
__dpmi_regs r;
|
__dpmi_regs r;
|
||||||
|
|
@ -530,9 +702,23 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformSpanCopy8
|
// 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) {
|
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) {
|
while (((uintptr_t)dst & 3) && count > 0) {
|
||||||
*dst++ = *src++;
|
*dst++ = *src++;
|
||||||
count--;
|
count--;
|
||||||
|
|
@ -560,9 +746,15 @@ void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformSpanCopy16
|
// 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) {
|
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) {
|
if (((uintptr_t)dst & 2) && count > 0) {
|
||||||
*(uint16_t *)dst = *(const uint16_t *)src;
|
*(uint16_t *)dst = *(const uint16_t *)src;
|
||||||
dst += 2;
|
dst += 2;
|
||||||
|
|
@ -591,6 +783,9 @@ void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformSpanCopy32
|
// 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) {
|
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) {
|
||||||
__asm__ __volatile__ (
|
__asm__ __volatile__ (
|
||||||
|
|
@ -605,6 +800,12 @@ void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformSpanFill8
|
// 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) {
|
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) {
|
||||||
uint8_t c = (uint8_t)color;
|
uint8_t c = (uint8_t)color;
|
||||||
|
|
@ -639,6 +840,10 @@ void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformSpanFill16
|
// 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) {
|
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) {
|
||||||
uint16_t c = (uint16_t)color;
|
uint16_t c = (uint16_t)color;
|
||||||
|
|
@ -673,6 +878,10 @@ void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformSpanFill32
|
// 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) {
|
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) {
|
||||||
__asm__ __volatile__ (
|
__asm__ __volatile__ (
|
||||||
|
|
@ -687,6 +896,19 @@ void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformValidateFilename — DOS 8.3 filename validation
|
// 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) {
|
const char *platformValidateFilename(const char *name) {
|
||||||
static const char *reserved[] = {
|
static const char *reserved[] = {
|
||||||
|
|
@ -764,6 +986,20 @@ const char *platformValidateFilename(const char *name) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformVideoInit
|
// 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) {
|
int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
|
||||||
uint16_t bestMode;
|
uint16_t bestMode;
|
||||||
|
|
@ -833,9 +1069,20 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformVideoSetPalette
|
// 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) {
|
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);
|
outportb(0x3C8, (uint8_t)firstEntry);
|
||||||
|
|
||||||
for (int32_t i = 0; i < count; i++) {
|
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
|
// 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) {
|
void platformVideoShutdown(DisplayT *d) {
|
||||||
// Restore text mode (mode 3)
|
// INT 10h function 00h, mode 03h = 80x25 color text
|
||||||
__dpmi_regs r;
|
__dpmi_regs r;
|
||||||
memset(&r, 0, sizeof(r));
|
memset(&r, 0, sizeof(r));
|
||||||
r.x.ax = 0x0003;
|
r.x.ax = 0x0003;
|
||||||
|
|
@ -877,6 +1129,12 @@ void platformVideoShutdown(DisplayT *d) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// platformYield
|
// 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) {
|
void platformYield(void) {
|
||||||
__dpmi_yield();
|
__dpmi_yield();
|
||||||
|
|
@ -886,6 +1144,10 @@ void platformYield(void) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// setVesaMode
|
// 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) {
|
static int32_t setVesaMode(uint16_t mode) {
|
||||||
__dpmi_regs r;
|
__dpmi_regs r;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,39 @@
|
||||||
// widgetAnsiTerm.c — ANSI BBS terminal emulator widget
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -12,6 +47,9 @@
|
||||||
#define ANSI_SB_W 14
|
#define ANSI_SB_W 14
|
||||||
#define ANSI_DEFAULT_SCROLLBACK 500
|
#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_NORMAL 0
|
||||||
#define PARSE_ESC 1
|
#define PARSE_ESC 1
|
||||||
#define PARSE_CSI 2
|
#define PARSE_CSI 2
|
||||||
|
|
@ -19,7 +57,9 @@
|
||||||
// Default attribute: light gray on black
|
// Default attribute: light gray on black
|
||||||
#define ANSI_DEFAULT_ATTR 0x07
|
#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_BLINK_BIT 0x80
|
||||||
#define ATTR_BG_MASK 0x70
|
#define ATTR_BG_MASK 0x70
|
||||||
#define ATTR_FG_MASK 0x0F
|
#define ATTR_FG_MASK 0x0F
|
||||||
|
|
@ -51,7 +91,13 @@ static const uint8_t sCgaPalette[16][3] = {
|
||||||
{255, 255, 255}, // 15: bright white
|
{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 };
|
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.
|
// 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) {
|
static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) {
|
||||||
if (!w->as.ansiTerm->scrollback || w->as.ansiTerm->scrollbackMax <= 0) {
|
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.
|
// Build the packed 16-color palette and cache it in the widget.
|
||||||
// Only recomputed when paletteValid is false (first use or
|
// Only recomputed when paletteValid is false (first use or
|
||||||
// after a display format change).
|
// 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) {
|
static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) {
|
||||||
if (w->as.ansiTerm->paletteValid) {
|
if (w->as.ansiTerm->paletteValid) {
|
||||||
|
|
@ -137,6 +192,8 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void ansiTermClearSelection(WidgetT *w) {
|
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)) {
|
if (ansiTermHasSelection(w)) {
|
||||||
w->as.ansiTerm->dirtyRows = 0xFFFFFFFF;
|
w->as.ansiTerm->dirtyRows = 0xFFFFFFFF;
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +223,11 @@ static void ansiTermCopySelection(WidgetT *w) {
|
||||||
|
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
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];
|
char buf[4096];
|
||||||
int32_t pos = 0;
|
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).
|
// 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) {
|
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) {
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
int32_t cols = w->as.ansiTerm->cols;
|
||||||
|
|
@ -235,6 +300,10 @@ static void ansiTermDirtyRow(WidgetT *w, int32_t row) {
|
||||||
// ansiTermDeleteLines
|
// 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) {
|
static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
int32_t cols = w->as.ansiTerm->cols;
|
||||||
int32_t bot = w->as.ansiTerm->scrollBot;
|
int32_t bot = w->as.ansiTerm->scrollBot;
|
||||||
|
|
@ -274,11 +343,19 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
|
||||||
// ansiTermDispatchCsi
|
// 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) {
|
static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
|
||||||
int32_t *p = w->as.ansiTerm->params;
|
int32_t *p = w->as.ansiTerm->params;
|
||||||
int32_t n = w->as.ansiTerm->paramCount;
|
int32_t n = w->as.ansiTerm->paramCount;
|
||||||
|
|
||||||
// DEC private modes (ESC[?...)
|
// 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) {
|
if (w->as.ansiTerm->csiPrivate) {
|
||||||
int32_t mode = (n >= 1) ? p[0] : 0;
|
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
|
case 'c': // DA - device attributes
|
||||||
{
|
{
|
||||||
if (w->as.ansiTerm->commWrite) {
|
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";
|
const uint8_t reply[] = "\033[?1;2c";
|
||||||
w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, reply, 7);
|
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
|
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;
|
int32_t col = w->as.ansiTerm->cursorCol;
|
||||||
|
|
||||||
if (col > 0) {
|
if (col > 0) {
|
||||||
|
|
@ -549,12 +632,16 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
|
||||||
|
|
||||||
if (w->as.ansiTerm->commWrite) {
|
if (w->as.ansiTerm->commWrite) {
|
||||||
if (mode == 6) {
|
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];
|
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));
|
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);
|
w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len);
|
||||||
} else if (mode == 255) {
|
} 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];
|
char reply[16];
|
||||||
int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm->rows, (long)w->as.ansiTerm->cols);
|
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);
|
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
|
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 rows = w->as.ansiTerm->rows;
|
||||||
int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0;
|
int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0;
|
||||||
int32_t bot = (n >= 2 && p[1]) ? p[1] - 1 : rows - 1;
|
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
|
// 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) {
|
static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) {
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
int32_t cols = w->as.ansiTerm->cols;
|
||||||
int32_t rows = w->as.ansiTerm->rows;
|
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.
|
// 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) {
|
static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) {
|
||||||
uint8_t *cells = w->as.ansiTerm->cells;
|
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.
|
// combined scrollback+screen view.
|
||||||
// lineIndex < scrollbackCount → scrollback line
|
// lineIndex < scrollbackCount → scrollback line
|
||||||
// lineIndex >= scrollbackCount → screen 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) {
|
static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) {
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
int32_t cols = w->as.ansiTerm->cols;
|
||||||
|
|
@ -742,6 +849,9 @@ static bool ansiTermHasSelection(const WidgetT *w) {
|
||||||
// ansiTermInsertLines
|
// 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) {
|
static void ansiTermInsertLines(WidgetT *w, int32_t count) {
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
int32_t cols = w->as.ansiTerm->cols;
|
||||||
int32_t bot = w->as.ansiTerm->scrollBot;
|
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.
|
// 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) {
|
static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
|
||||||
switch (w->as.ansiTerm->parseState) {
|
switch (w->as.ansiTerm->parseState) {
|
||||||
|
|
@ -814,6 +934,8 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
|
||||||
w->as.ansiTerm->cursorCol--;
|
w->as.ansiTerm->cursorCol--;
|
||||||
}
|
}
|
||||||
} else if (ch == '\t') {
|
} 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;
|
w->as.ansiTerm->cursorCol = (w->as.ansiTerm->cursorCol + 8) & ~7;
|
||||||
|
|
||||||
if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) {
|
if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) {
|
||||||
|
|
@ -867,6 +989,11 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PARSE_CSI:
|
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 == '?') {
|
if (ch == '?') {
|
||||||
w->as.ansiTerm->csiPrivate = true;
|
w->as.ansiTerm->csiPrivate = true;
|
||||||
} else if (ch >= '0' && ch <= '9') {
|
} 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.
|
// 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) {
|
static void ansiTermProcessSgr(WidgetT *w) {
|
||||||
if (w->as.ansiTerm->paramCount == 0) {
|
if (w->as.ansiTerm->paramCount == 0) {
|
||||||
|
|
@ -977,7 +1115,9 @@ static void ansiTermPasteToComm(WidgetT *w) {
|
||||||
return;
|
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++) {
|
for (int32_t i = 0; i < clipLen; i++) {
|
||||||
uint8_t ch = (uint8_t)clip[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
|
// Scroll the screen up by one line. The top line is pushed into
|
||||||
// the scrollback buffer before being discarded.
|
// 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) {
|
static void ansiTermScrollUp(WidgetT *w) {
|
||||||
int32_t cols = w->as.ansiTerm->cols;
|
int32_t cols = w->as.ansiTerm->cols;
|
||||||
|
|
@ -1121,6 +1266,11 @@ static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t
|
||||||
// wgtAnsiTerm
|
// 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) {
|
WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -1281,6 +1431,18 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) {
|
||||||
// wgtAnsiTermPoll
|
// 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) {
|
int32_t wgtAnsiTermPoll(WidgetT *w) {
|
||||||
VALIDATE_WIDGET(w, WidgetAnsiTermE, 0);
|
VALIDATE_WIDGET(w, WidgetAnsiTermE, 0);
|
||||||
|
|
||||||
|
|
@ -1346,6 +1508,17 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
|
||||||
// no relayout, no other widgets). This keeps ACK turnaround fast
|
// no relayout, no other widgets). This keeps ACK turnaround fast
|
||||||
// for the serial link.
|
// 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) {
|
int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
|
||||||
if (!w || w->type != WidgetAnsiTermE || !w->window) {
|
if (!w || w->type != WidgetAnsiTermE || !w->window) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -1382,7 +1555,10 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
|
||||||
return 0;
|
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;
|
DisplayT cd = ctx->display;
|
||||||
cd.backBuf = win->contentBuf;
|
cd.backBuf = win->contentBuf;
|
||||||
cd.width = win->contentW;
|
cd.width = win->contentW;
|
||||||
|
|
@ -1449,6 +1625,10 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
|
||||||
// wgtAnsiTermSetComm
|
// 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)) {
|
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);
|
VALIDATE_WIDGET_VOID(w, WidgetAnsiTermE);
|
||||||
|
|
||||||
|
|
@ -1462,6 +1642,11 @@ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t
|
||||||
// wgtAnsiTermWrite
|
// 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) {
|
void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) {
|
||||||
if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) {
|
if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1491,6 +1676,10 @@ void widgetAnsiTermDestroy(WidgetT *w) {
|
||||||
// widgetAnsiTermCalcMinSize
|
// 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) {
|
void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
w->calcMinW = w->as.ansiTerm->cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W;
|
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;
|
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.
|
// 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) {
|
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)) {
|
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
|
// Translate keyboard input to ANSI escape sequences and send
|
||||||
// via the comm interface. Does nothing if commWrite is NULL.
|
// 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) {
|
void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// Ctrl+C: copy if selection exists, otherwise send ^C
|
// Ctrl+C: copy if selection exists, otherwise send ^C
|
||||||
if (key == 0x03 && (mod & KEY_MOD_CTRL)) {
|
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.
|
// 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) {
|
void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
AppContextT *actx = (AppContextT *)root->userData;
|
AppContextT *actx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &actx->font;
|
const BitmapFontT *font = &actx->font;
|
||||||
|
|
@ -1803,6 +2010,18 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
// widgetAnsiTermPaint
|
// 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) {
|
void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
// Draw sunken bevel border
|
// Draw sunken bevel border
|
||||||
BevelStyleT bevel;
|
BevelStyleT bevel;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,22 @@
|
||||||
// widgetBox.c — VBox, HBox, and Frame container widgets
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -7,6 +25,17 @@
|
||||||
// widgetFramePaint
|
// 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) {
|
void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
int32_t fb = widgetFrameBorderWidth(w);
|
int32_t fb = widgetFrameBorderWidth(w);
|
||||||
int32_t boxY = w->y + font->charHeight / 2;
|
int32_t boxY = w->y + font->charHeight / 2;
|
||||||
|
|
@ -69,6 +98,9 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
||||||
// wgtFrame
|
// 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 *wgtFrame(WidgetT *parent, const char *title) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetFrameE);
|
WidgetT *w = widgetAlloc(parent, WidgetFrameE);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
// widgetButton.c — Button widget
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -43,6 +62,9 @@ void widgetButtonSetText(WidgetT *w, const char *text) {
|
||||||
// widgetButtonCalcMinSize
|
// 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) {
|
void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
w->calcMinW = textWidthAccel(font, w->as.button.text) + BUTTON_PAD_H * 2;
|
w->calcMinW = textWidthAccel(font, w->as.button.text) + BUTTON_PAD_H * 2;
|
||||||
w->calcMinH = font->charHeight + BUTTON_PAD_V * 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
|
// 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) {
|
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 fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,25 @@
|
||||||
// widgetCanvas.c — Drawable canvas widget (freehand draw, PNG save/load)
|
// 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 "widgetInternal.h"
|
||||||
#include "../thirdparty/stb_image.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
|
// Read/write a single pixel at the given address, respecting the display's
|
||||||
// bytes-per-pixel depth.
|
// 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) {
|
static inline uint32_t canvasGetPixel(const uint8_t *src, int32_t bpp) {
|
||||||
if (bpp == 1) {
|
if (bpp == 1) {
|
||||||
|
|
@ -71,7 +95,11 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
|
||||||
return;
|
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;
|
int32_t r2 = rad * rad;
|
||||||
|
|
||||||
for (int32_t dy = -rad; dy <= rad; dy++) {
|
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 rem = r2 - dy2;
|
||||||
int32_t hspan = 0;
|
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) {
|
if (rem > 0) {
|
||||||
hspan = rad;
|
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.
|
// 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) {
|
static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
|
||||||
int32_t dx = x1 - x0;
|
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.
|
// 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) {
|
static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b) {
|
||||||
if (d->format.bitsPerPixel == 8) {
|
if (d->format.bitsPerPixel == 8) {
|
||||||
|
|
@ -192,6 +232,12 @@ static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uin
|
||||||
// wgtCanvas
|
// 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) {
|
WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
|
||||||
if (!parent || w <= 0 || h <= 0) {
|
if (!parent || w <= 0 || h <= 0) {
|
||||||
return NULL;
|
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;
|
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) {
|
if (ctx) {
|
||||||
for (int32_t py = y0; py < y1; py++) {
|
for (int32_t py = y0; py < y1; py++) {
|
||||||
ctx->blitOps.spanFill(data + py * pitch + x0 * bpp, color, fillW);
|
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
|
// 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) {
|
int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
|
||||||
if (!w || w->type != WidgetCanvasE || !path) {
|
if (!w || w->type != WidgetCanvasE || !path) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -573,6 +629,9 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
|
||||||
// wgtCanvasSave
|
// 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) {
|
int32_t wgtCanvasSave(WidgetT *w, const char *path) {
|
||||||
if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) {
|
if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -687,6 +746,10 @@ void widgetCanvasDestroy(WidgetT *w) {
|
||||||
// widgetCanvasCalcMinSize
|
// 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 widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
(void)font;
|
(void)font;
|
||||||
w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2;
|
w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2;
|
||||||
|
|
@ -698,6 +761,11 @@ void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetCanvasOnMouse
|
// 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 widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
(void)root;
|
(void)root;
|
||||||
|
|
||||||
|
|
@ -727,6 +795,10 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetCanvasPaint
|
// 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 widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
// widgetCheckbox.c — Checkbox widget
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -43,6 +55,9 @@ void widgetCheckboxSetText(WidgetT *w, const char *text) {
|
||||||
// widgetCheckboxCalcMinSize
|
// 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) {
|
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
||||||
textWidthAccel(font, w->as.checkbox.text);
|
textWidthAccel(font, w->as.checkbox.text);
|
||||||
|
|
@ -103,7 +118,11 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
bevel.width = 1;
|
bevel.width = 1;
|
||||||
drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel);
|
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) {
|
if (w->as.checkbox.checked) {
|
||||||
int32_t cx = w->x + 3;
|
int32_t cx = w->x + 3;
|
||||||
int32_t cy = boxY + 3;
|
int32_t cy = boxY + 3;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,49 @@
|
||||||
// widgetClass.c — Widget class vtable definitions
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Per-type class definitions
|
// 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 = {
|
static const WidgetClassT sClassVBox = {
|
||||||
.flags = WCLASS_BOX_CONTAINER,
|
.flags = WCLASS_BOX_CONTAINER,
|
||||||
|
|
@ -97,6 +136,9 @@ static const WidgetClassT sClassRadio = {
|
||||||
.setText = widgetRadioSetText
|
.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 = {
|
static const WidgetClassT sClassTextInput = {
|
||||||
.flags = WCLASS_FOCUSABLE,
|
.flags = WCLASS_FOCUSABLE,
|
||||||
.paint = widgetTextInputPaint,
|
.paint = widgetTextInputPaint,
|
||||||
|
|
@ -175,6 +217,9 @@ static const WidgetClassT sClassFrame = {
|
||||||
.setText = NULL
|
.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 = {
|
static const WidgetClassT sClassDropdown = {
|
||||||
.flags = WCLASS_FOCUSABLE,
|
.flags = WCLASS_FOCUSABLE,
|
||||||
.paint = widgetDropdownPaint,
|
.paint = widgetDropdownPaint,
|
||||||
|
|
@ -227,6 +272,9 @@ static const WidgetClassT sClassSlider = {
|
||||||
.setText = NULL
|
.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 = {
|
static const WidgetClassT sClassTabControl = {
|
||||||
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN,
|
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN,
|
||||||
.paint = widgetTabControlPaint,
|
.paint = widgetTabControlPaint,
|
||||||
|
|
@ -279,6 +327,13 @@ static const WidgetClassT sClassToolbar = {
|
||||||
.setText = NULL
|
.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 = {
|
static const WidgetClassT sClassTreeView = {
|
||||||
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
||||||
.paint = widgetTreeViewPaint,
|
.paint = widgetTreeViewPaint,
|
||||||
|
|
@ -292,6 +347,10 @@ static const WidgetClassT sClassTreeView = {
|
||||||
.setText = NULL
|
.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 = {
|
static const WidgetClassT sClassTreeItem = {
|
||||||
.flags = 0,
|
.flags = 0,
|
||||||
.paint = NULL,
|
.paint = NULL,
|
||||||
|
|
@ -370,6 +429,9 @@ static const WidgetClassT sClassAnsiTerm = {
|
||||||
.setText = NULL
|
.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 = {
|
static const WidgetClassT sClassScrollPane = {
|
||||||
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
||||||
.paint = widgetScrollPanePaint,
|
.paint = widgetScrollPanePaint,
|
||||||
|
|
@ -412,6 +474,18 @@ static const WidgetClassT sClassSpinner = {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Class table — indexed by WidgetTypeE
|
// 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[] = {
|
const WidgetClassT *widgetClassTable[] = {
|
||||||
[WidgetVBoxE] = &sClassVBox,
|
[WidgetVBoxE] = &sClassVBox,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
// widgetComboBox.c — ComboBox widget (editable text + dropdown list)
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -7,6 +27,11 @@
|
||||||
// wgtComboBox
|
// 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 *wgtComboBox(WidgetT *parent, int32_t maxLen) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetComboBoxE);
|
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.items = items;
|
||||||
w->as.comboBox.itemCount = count;
|
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;
|
int32_t maxLen = 0;
|
||||||
|
|
||||||
for (int32_t i = 0; i < count; i++) {
|
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;
|
w->as.comboBox.selectedIdx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wgtInvalidate (not wgtInvalidatePaint) triggers a full relayout because
|
||||||
|
// changing items may change the widget's minimum width
|
||||||
wgtInvalidate(w);
|
wgtInvalidate(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +170,12 @@ const char *widgetComboBoxGetText(const WidgetT *w) {
|
||||||
// widgetComboBoxOnKey
|
// 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) {
|
void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
if (w->as.comboBox.open) {
|
if (w->as.comboBox.open) {
|
||||||
if (key == (0x48 | 0x100)) {
|
if (key == (0x48 | 0x100)) {
|
||||||
|
|
@ -304,6 +339,12 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetComboBoxPaint
|
// 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) {
|
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 fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
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;
|
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 visSelLo = selLo - off;
|
||||||
int32_t visSelHi = selHi - off;
|
int32_t visSelHi = selHi - off;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,67 @@
|
||||||
// widgetCore.c — Core widget infrastructure (alloc, tree ops, helpers)
|
// 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"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Global state for drag and popup tracking
|
// 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;
|
bool sDebugLayout = false;
|
||||||
WidgetT *sFocusedWidget = NULL;
|
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
|
||||||
WidgetT *sOpenPopup = NULL;
|
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
|
||||||
WidgetT *sPressedButton = NULL;
|
WidgetT *sPressedButton = NULL; // button being held down (tracks mouse in/out)
|
||||||
WidgetT *sDragSlider = NULL;
|
WidgetT *sDragSlider = NULL; // slider being dragged
|
||||||
WidgetT *sDrawingCanvas = NULL;
|
WidgetT *sDrawingCanvas = NULL; // canvas receiving paint strokes
|
||||||
WidgetT *sDragTextSelect = NULL;
|
WidgetT *sDragTextSelect = NULL; // text widget in drag-select mode
|
||||||
int32_t sDragOffset = 0;
|
int32_t sDragOffset = 0; // pixel offset from drag start to thumb center
|
||||||
WidgetT *sResizeListView = NULL;
|
WidgetT *sResizeListView = NULL; // ListView undergoing column resize
|
||||||
int32_t sResizeCol = -1;
|
int32_t sResizeCol = -1; // which column is being resized
|
||||||
int32_t sResizeStartX = 0;
|
int32_t sResizeStartX = 0; // mouse X at resize start
|
||||||
int32_t sResizeOrigW = 0;
|
int32_t sResizeOrigW = 0; // column width at resize start
|
||||||
WidgetT *sDragSplitter = NULL;
|
WidgetT *sDragSplitter = NULL; // splitter being dragged
|
||||||
int32_t sDragSplitStart = 0;
|
int32_t sDragSplitStart = 0; // mouse offset from splitter edge at drag start
|
||||||
WidgetT *sDragReorder = NULL;
|
WidgetT *sDragReorder = NULL; // list/tree widget in drag-reorder mode
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetAddChild
|
// 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) {
|
void widgetAddChild(WidgetT *parent, WidgetT *child) {
|
||||||
child->parent = parent;
|
child->parent = parent;
|
||||||
|
|
@ -44,6 +80,18 @@ void widgetAddChild(WidgetT *parent, WidgetT *child) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetAlloc
|
// 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 *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
|
||||||
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
|
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
|
||||||
|
|
@ -104,6 +152,17 @@ int32_t widgetCountVisibleChildren(const WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetDestroyChildren
|
// 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) {
|
void widgetDestroyChildren(WidgetT *w) {
|
||||||
WidgetT *child = w->firstChild;
|
WidgetT *child = w->firstChild;
|
||||||
|
|
@ -155,7 +214,17 @@ void widgetDestroyChildren(WidgetT *w) {
|
||||||
// widgetDropdownPopupRect
|
// 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) {
|
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;
|
int32_t itemCount = 0;
|
||||||
|
|
@ -196,6 +265,15 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetFindByAccel
|
// 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) {
|
WidgetT *widgetFindByAccel(WidgetT *root, char key) {
|
||||||
if (!root || !root->enabled) {
|
if (!root || !root->enabled) {
|
||||||
|
|
@ -232,9 +310,15 @@ WidgetT *widgetFindByAccel(WidgetT *root, char key) {
|
||||||
// widgetFindNextFocusable
|
// widgetFindNextFocusable
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Depth-first walk of the widget tree. Returns the first focusable
|
// Implements Tab-order navigation: finds the next focusable widget
|
||||||
// widget found after 'after'. If 'after' is NULL, returns the first
|
// after 'after' in depth-first tree order. The two-pass approach
|
||||||
// focusable widget. Wraps around to the beginning if needed.
|
// (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) {
|
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
|
||||||
if (!w->visible || !w->enabled) {
|
if (!w->visible || !w->enabled) {
|
||||||
|
|
@ -280,8 +364,17 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
|
||||||
// widgetFindPrevFocusable
|
// widgetFindPrevFocusable
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Depth-first walk collecting all visible+enabled focusable widgets,
|
// Shift+Tab navigation: finds the previous focusable widget.
|
||||||
// then returns the one before 'before'. Wraps around if needed.
|
// 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 *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
|
||||||
WidgetT *list[128];
|
WidgetT *list[128];
|
||||||
|
|
@ -362,6 +455,22 @@ int32_t widgetFrameBorderWidth(const WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetHitTest
|
// 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) {
|
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
|
||||||
if (!w->visible) {
|
if (!w->visible) {
|
||||||
|
|
@ -427,8 +536,16 @@ bool widgetIsHorizContainer(WidgetTypeE type) {
|
||||||
// widgetNavigateIndex
|
// widgetNavigateIndex
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Shared Up/Down/Home/End/PgUp/PgDn handler for list-like widgets.
|
// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
|
||||||
// Returns the new index, or -1 if the key is not a navigation key.
|
// 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) {
|
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
|
||||||
if (key == (0x50 | 0x100)) {
|
if (key == (0x50 | 0x100)) {
|
||||||
|
|
@ -516,6 +633,15 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetScrollbarThumb
|
// 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) {
|
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
|
||||||
*thumbSize = (trackLen * visibleSize) / totalSize;
|
*thumbSize = (trackLen * visibleSize) / totalSize;
|
||||||
|
|
@ -541,6 +667,11 @@ void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSi
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetRemoveChild
|
// 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) {
|
void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
|
||||||
WidgetT *prev = NULL;
|
WidgetT *prev = NULL;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,22 @@
|
||||||
// widgetDropdown.c — Dropdown (select) widget
|
// widgetDropdown.c — Dropdown (select) widget
|
||||||
|
//
|
||||||
|
// A non-editable dropdown list (HTML <select> equivalent). Unlike ComboBox,
|
||||||
|
// the user cannot type a custom value — they can only choose from the
|
||||||
|
// predefined items. This simplifies the widget considerably: no text buffer,
|
||||||
|
// no undo, no cursor, no text editing key handling.
|
||||||
|
//
|
||||||
|
// The visual layout is identical to ComboBox (sunken display area + raised
|
||||||
|
// dropdown button), but the display area shows the selected item's text
|
||||||
|
// as read-only. A focus rect is drawn inside the display area when focused
|
||||||
|
// (since there's no cursor to indicate focus).
|
||||||
|
//
|
||||||
|
// Keyboard behavior when closed: Up arrow decrements selection immediately
|
||||||
|
// (inline cycling), Down/Space/Enter opens the popup. This matches the
|
||||||
|
// Win3.1 dropdown behavior where arrow keys cycle through values without
|
||||||
|
// opening the full list.
|
||||||
|
//
|
||||||
|
// Popup overlay painting and hit testing are shared with ComboBox via
|
||||||
|
// widgetDropdownPopupRect and widgetPaintPopupList in widgetCore.c.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -108,6 +126,11 @@ void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetDropdownOnKey
|
// widgetDropdownOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Key handling: when popup is open, Up/Down navigate the hover index and
|
||||||
|
// Enter/Space confirms selection. When closed, Down/Space/Enter opens the
|
||||||
|
// popup, while Up decrements the selection inline (without opening). This
|
||||||
|
// two-mode behavior matches the standard Windows combo box UX where quick
|
||||||
|
// arrow-key cycling doesn't require the popup to be visible.
|
||||||
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
(void)mod;
|
(void)mod;
|
||||||
|
|
||||||
|
|
@ -171,6 +194,12 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetDropdownOnMouse
|
// widgetDropdownOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse click toggles the popup open/closed. The sClosedPopup guard prevents
|
||||||
|
// a re-open race condition: when a click outside the popup closes it, the
|
||||||
|
// widget event dispatcher first closes the popup (setting sClosedPopup),
|
||||||
|
// then dispatches the click to whatever widget is under the cursor. If that
|
||||||
|
// happens to be this dropdown's body, we'd immediately re-open without this
|
||||||
|
// check. The sClosedPopup reference is cleared on the next event cycle.
|
||||||
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
(void)root;
|
(void)root;
|
||||||
(void)vx;
|
(void)vx;
|
||||||
|
|
@ -225,7 +254,9 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
btnBevel.width = 2;
|
btnBevel.width = 2;
|
||||||
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
||||||
|
|
||||||
// Down arrow in button
|
// Down arrow glyph in button — a small filled triangle drawn as horizontal
|
||||||
|
// lines of decreasing width (7, 5, 3, 1 pixels). This creates a 4-pixel
|
||||||
|
// tall downward-pointing triangle centered in the button.
|
||||||
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
||||||
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
||||||
int32_t arrowY = w->y + w->h / 2 - 1;
|
int32_t arrowY = w->y + w->h / 2 - 1;
|
||||||
|
|
@ -244,6 +275,11 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
// widgetDropdownPaintPopup
|
// widgetDropdownPaintPopup
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint the popup overlay list. Delegates to widgetDropdownPopupRect (which
|
||||||
|
// computes position, handling screen-edge flip to ensure the popup stays
|
||||||
|
// on-screen) and widgetPaintPopupList (which renders the bordered, scrollable
|
||||||
|
// item list with hover highlighting). These shared helpers are also used by
|
||||||
|
// ComboBox, keeping the popup rendering consistent between both widgets.
|
||||||
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
int32_t popX;
|
int32_t popX;
|
||||||
int32_t popY;
|
int32_t popY;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,28 @@
|
||||||
// widgetEvent.c — Window event handlers for widget system
|
// widgetEvent.c — Window event handlers for widget system
|
||||||
|
//
|
||||||
|
// This file routes window-level events (mouse, keyboard, paint, resize,
|
||||||
|
// scroll) into the widget tree. It serves as the bridge between the
|
||||||
|
// window manager (dvxWm) and the widget system.
|
||||||
|
//
|
||||||
|
// Event handling follows a priority system for mouse events:
|
||||||
|
// 1. Active drag/interaction states (slider drag, button press tracking,
|
||||||
|
// text selection, canvas drawing, column resize, drag-reorder,
|
||||||
|
// splitter drag) are checked first and handled directly.
|
||||||
|
// 2. Open popups (dropdown/combobox lists) intercept clicks next.
|
||||||
|
// 3. Normal hit testing routes clicks to the target widget.
|
||||||
|
//
|
||||||
|
// This priority ordering ensures that ongoing interactions complete
|
||||||
|
// correctly even if the mouse moves outside the originating widget.
|
||||||
|
// For example, dragging a slider and moving the mouse above the slider
|
||||||
|
// still adjusts the value, because sDragSlider captures the event
|
||||||
|
// before hit testing runs.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
// Widget whose popup was just closed by click-outside — prevents
|
// Widget whose popup was just closed by click-outside — prevents
|
||||||
// immediate re-open on the same click.
|
// immediate re-open on the same click. Without this, clicking the
|
||||||
|
// dropdown button to close its popup would immediately hit-test the
|
||||||
|
// button again and re-open the popup in the same event.
|
||||||
WidgetT *sClosedPopup = NULL;
|
WidgetT *sClosedPopup = NULL;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,9 +30,26 @@ WidgetT *sClosedPopup = NULL;
|
||||||
// widgetManageScrollbars
|
// widgetManageScrollbars
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Checks whether the widget tree's minimum size exceeds the
|
// Manages automatic scrollbar addition/removal for widget-based windows.
|
||||||
// window content area. Adds or removes WM scrollbars as needed,
|
// Called on every invalidation to ensure scrollbars match the current
|
||||||
// then relayouts the widget tree at the virtual content size.
|
// widget tree's minimum size requirements.
|
||||||
|
//
|
||||||
|
// The algorithm:
|
||||||
|
// 1. Measure the full widget tree to get its minimum size.
|
||||||
|
// 2. Remove all existing scrollbars to measure the full available area.
|
||||||
|
// 3. Compare min size vs available area to decide if scrollbars are needed.
|
||||||
|
// 4. Account for scrollbar interaction: adding a vertical scrollbar
|
||||||
|
// reduces horizontal space, which may require a horizontal scrollbar
|
||||||
|
// (and vice versa). This mutual dependency is handled with a single
|
||||||
|
// extra check rather than iterating to convergence.
|
||||||
|
// 5. Preserve scroll positions across scrollbar recreation.
|
||||||
|
// 6. Layout at the virtual content size (max of available and minimum).
|
||||||
|
//
|
||||||
|
// The virtual content size concept is key: if the widget tree needs
|
||||||
|
// 800px but only 600px is available, the tree is laid out at 800px
|
||||||
|
// and the window scrolls to show the visible portion. This means
|
||||||
|
// widget positions can be negative (scrolled above the viewport)
|
||||||
|
// or extend past the window edge (scrolled below).
|
||||||
|
|
||||||
void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
|
void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
|
||||||
WidgetT *root = win->widgetRoot;
|
WidgetT *root = win->widgetRoot;
|
||||||
|
|
@ -106,6 +142,15 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetOnKey
|
// widgetOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Keyboard event dispatch. Unlike mouse events which use hit testing,
|
||||||
|
// keyboard events go directly to the focused widget (sFocusedWidget).
|
||||||
|
// The cached pointer avoids an O(n) tree walk to find the focused
|
||||||
|
// widget on every keypress.
|
||||||
|
//
|
||||||
|
// There is no keyboard event bubbling — if the focused widget doesn't
|
||||||
|
// handle a key, it's simply dropped. Accelerators (Alt+key) are
|
||||||
|
// handled at a higher level in the app event loop, not here.
|
||||||
|
|
||||||
void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
||||||
WidgetT *root = win->widgetRoot;
|
WidgetT *root = win->widgetRoot;
|
||||||
|
|
@ -136,6 +181,20 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetOnMouse
|
// widgetOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Main mouse event handler. This is the most complex event handler
|
||||||
|
// because it must manage multiple overlapping interaction states.
|
||||||
|
//
|
||||||
|
// The function is structured as a series of early-return checks:
|
||||||
|
// each active interaction (drag, press, popup) is checked in priority
|
||||||
|
// order. If the interaction handles the event, it returns immediately.
|
||||||
|
// Only if no interaction is active does the event fall through to
|
||||||
|
// normal hit testing.
|
||||||
|
//
|
||||||
|
// Coordinates (x, y) are in content-buffer space — the window manager
|
||||||
|
// has already subtracted the window chrome offset. Widget positions
|
||||||
|
// are also in content-buffer space (set during layout), so no
|
||||||
|
// coordinate transform is needed for hit testing.
|
||||||
|
|
||||||
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
||||||
WidgetT *root = win->widgetRoot;
|
WidgetT *root = win->widgetRoot;
|
||||||
|
|
@ -465,18 +524,21 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear focus from previously focused widget (O(1) instead of tree walk)
|
// Clear focus from the previously focused widget. This is done via
|
||||||
|
// the cached sFocusedWidget pointer rather than walking the tree to
|
||||||
|
// find the focused widget — an O(1) operation vs O(n).
|
||||||
if (sFocusedWidget) {
|
if (sFocusedWidget) {
|
||||||
sFocusedWidget->focused = false;
|
sFocusedWidget->focused = false;
|
||||||
sFocusedWidget = NULL;
|
sFocusedWidget = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch to per-widget mouse handler via vtable
|
// Dispatch to the hit widget's mouse handler via vtable. The handler
|
||||||
|
// is responsible for setting hit->focused=true if it wants focus.
|
||||||
if (hit->enabled && hit->wclass && hit->wclass->onMouse) {
|
if (hit->enabled && hit->wclass && hit->wclass->onMouse) {
|
||||||
hit->wclass->onMouse(hit, root, vx, vy);
|
hit->wclass->onMouse(hit, root, vx, vy);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the newly focused widget
|
// Update the cached focus pointer for O(1) access in widgetOnKey
|
||||||
if (hit->focused) {
|
if (hit->focused) {
|
||||||
sFocusedWidget = hit;
|
sFocusedWidget = hit;
|
||||||
}
|
}
|
||||||
|
|
@ -488,6 +550,25 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetOnPaint
|
// widgetOnPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Paints the entire widget tree into the window's content buffer.
|
||||||
|
// Called whenever the window needs a redraw (invalidation, scroll,
|
||||||
|
// resize).
|
||||||
|
//
|
||||||
|
// Sets up a temporary DisplayT context that points at the window's
|
||||||
|
// content buffer instead of the screen backbuffer. This means all
|
||||||
|
// draw operations (drawText, rectFill, drawBevel, etc.) write
|
||||||
|
// directly into the per-window content buffer, which the compositor
|
||||||
|
// later blits to the screen backbuffer.
|
||||||
|
//
|
||||||
|
// Scroll offset is applied by shifting the root widget's position
|
||||||
|
// to negative coordinates (-scrollX, -scrollY). This elegantly makes
|
||||||
|
// scrolling work without any special scroll handling in individual
|
||||||
|
// widgets — their positions are simply offset, and the clip rect
|
||||||
|
// on the DisplayT limits drawing to the visible area.
|
||||||
|
//
|
||||||
|
// The conditional re-layout avoids redundant layout passes when only
|
||||||
|
// the paint is needed (e.g. cursor blink, selection change).
|
||||||
|
|
||||||
void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
||||||
(void)dirtyArea;
|
(void)dirtyArea;
|
||||||
|
|
@ -544,6 +625,10 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetOnResize
|
// widgetOnResize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Called when the window is resized. Triggers a full scrollbar
|
||||||
|
// re-evaluation and relayout, since the available content area
|
||||||
|
// changed and scrollbars may need to be added or removed.
|
||||||
|
|
||||||
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) {
|
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) {
|
||||||
(void)newW;
|
(void)newW;
|
||||||
|
|
@ -567,6 +652,12 @@ void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetOnScroll
|
// widgetOnScroll
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Called by the WM when a window scrollbar value changes (user dragged
|
||||||
|
// the thumb, clicked the track, or used arrow buttons). Triggers a
|
||||||
|
// full repaint so the widget tree is redrawn at the new scroll offset.
|
||||||
|
// The actual scroll offset is read from win->vScroll/hScroll in the
|
||||||
|
// paint handler, so the orient and value parameters aren't directly used.
|
||||||
|
|
||||||
void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
||||||
(void)orient;
|
(void)orient;
|
||||||
|
|
@ -592,6 +683,20 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetReorderDrop — finalize drag-reorder on mouse release
|
// widgetReorderDrop — finalize drag-reorder on mouse release
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Completes a drag-reorder operation for ListBox, ListView, or TreeView.
|
||||||
|
// Moves the dragged item from its original position to the drop position
|
||||||
|
// by shifting intermediate elements. This is an in-place array rotation
|
||||||
|
// for ListBox/ListView (O(n) element moves) and a linked-list splice
|
||||||
|
// for TreeView.
|
||||||
|
//
|
||||||
|
// For ListBox and ListView, the item array, selection bits, and sort
|
||||||
|
// indices are all shifted together to maintain consistency. The selected
|
||||||
|
// index is updated to follow the moved item.
|
||||||
|
//
|
||||||
|
// For TreeView, the operation is a tree node re-parenting: unlink the
|
||||||
|
// dragged item from its old parent's child list, then insert it before
|
||||||
|
// or after the drop target in the target's parent's child list.
|
||||||
|
|
||||||
void widgetReorderDrop(WidgetT *w) {
|
void widgetReorderDrop(WidgetT *w) {
|
||||||
if (w->type == WidgetListBoxE) {
|
if (w->type == WidgetListBoxE) {
|
||||||
|
|
@ -801,6 +906,18 @@ void widgetReorderDrop(WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetReorderUpdate — update drop position during drag
|
// widgetReorderUpdate — update drop position during drag
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Tracks the mouse during a drag-reorder, updating the drop indicator
|
||||||
|
// position and auto-scrolling when the mouse is near the widget's edges.
|
||||||
|
//
|
||||||
|
// The drop position is computed from the mouse Y relative to item
|
||||||
|
// boundaries: if the mouse is in the top half of an item, the drop
|
||||||
|
// indicator goes before that item; if in the bottom half, it goes
|
||||||
|
// after. This gives intuitive "insert between items" behavior.
|
||||||
|
//
|
||||||
|
// Auto-scrolling happens when the mouse is within one row-height of
|
||||||
|
// the top or bottom edge, allowing the user to drag items to positions
|
||||||
|
// not currently visible.
|
||||||
|
|
||||||
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
||||||
(void)x;
|
(void)x;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
// widgetImage.c — Image widget (displays bitmap, responds to clicks)
|
// widgetImage.c — Image widget (displays bitmap, responds to clicks)
|
||||||
|
//
|
||||||
|
// Displays a bitmap image, optionally responding to clicks. The image data
|
||||||
|
// must be in the display's native pixel format (pre-converted). Two creation
|
||||||
|
// paths are provided:
|
||||||
|
// - wgtImage: from raw pixel data already in display format (takes ownership)
|
||||||
|
// - wgtImageFromFile: loads from file via stb_image and converts to display
|
||||||
|
// format during load
|
||||||
|
//
|
||||||
|
// The widget supports a simple press effect (1px offset on click) and fires
|
||||||
|
// onClick immediately on mouse-down. Unlike Button which has press/release
|
||||||
|
// tracking, Image fires instantly — this is intentional for image-based
|
||||||
|
// click targets where visual press feedback is less important than
|
||||||
|
// responsiveness.
|
||||||
|
//
|
||||||
|
// No border or bevel is drawn — the image fills its widget bounds with
|
||||||
|
// centering if the widget is larger than the image.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
#include "../thirdparty/stb_image.h"
|
#include "../thirdparty/stb_image.h"
|
||||||
|
|
@ -33,6 +49,13 @@ WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t
|
||||||
// Load an image from a file (BMP, PNG, JPEG, GIF), convert to
|
// Load an image from a file (BMP, PNG, JPEG, GIF), convert to
|
||||||
// display pixel format, and create an image widget.
|
// display pixel format, and create an image widget.
|
||||||
|
|
||||||
|
// Load an image from disk and create an Image widget. Uses stb_image for
|
||||||
|
// decoding (any format it supports: PNG, BMP, JPEG, GIF, etc.). The RGB
|
||||||
|
// pixels are converted to the display's native pixel format during load
|
||||||
|
// using packColor, then the raw RGB data is freed. The per-pixel bpp switch
|
||||||
|
// is duplicated here rather than using canvasPutPixel because this function
|
||||||
|
// is in a different compilation unit and inlining across units isn't guaranteed
|
||||||
|
// on DJGPP.
|
||||||
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
|
WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) {
|
||||||
if (!parent || !path) {
|
if (!parent || !path) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -129,6 +152,11 @@ void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetImageOnMouse
|
// widgetImageOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse click: briefly sets pressed=true (for the 1px offset effect),
|
||||||
|
// invalidates to show the press, fires onClick, then immediately clears
|
||||||
|
// pressed. The press is purely visual — there's no release tracking like
|
||||||
|
// Button has, since Image clicks are meant for instant actions (e.g.,
|
||||||
|
// clicking a logo or icon area).
|
||||||
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
(void)root;
|
(void)root;
|
||||||
(void)vx;
|
(void)vx;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
// widgetImageButton.c — Image button widget (button with image instead of text)
|
// widgetImageButton.c — Image button widget (button with image instead of text)
|
||||||
|
//
|
||||||
|
// Combines a Button's beveled border and press behavior with an Image's
|
||||||
|
// bitmap rendering. The image is centered within the button bounds and
|
||||||
|
// shifts by 1px on press, just like text in a regular button.
|
||||||
|
//
|
||||||
|
// Uses the same two-phase press model as Button: mouse press stores in
|
||||||
|
// sPressedButton, keyboard press (Space/Enter) stores in sKeyPressedBtn,
|
||||||
|
// and the event dispatcher handles release/cancel. The onClick callback
|
||||||
|
// fires on release, not press.
|
||||||
|
//
|
||||||
|
// The widget takes ownership of the image data buffer — if widget creation
|
||||||
|
// fails, the data is freed to prevent leaks.
|
||||||
|
//
|
||||||
|
// The 4px added to min size (widgetImageButtonCalcMinSize) accounts for
|
||||||
|
// the 2px bevel on each side — no extra padding is added beyond that,
|
||||||
|
// keeping image buttons compact for toolbar use.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
// widgetInternal.h — Shared internal header for widget implementation files
|
// widgetInternal.h — Shared internal header for widget implementation files
|
||||||
|
//
|
||||||
|
// This header is included only by widget implementation .c files (in the
|
||||||
|
// widgets/ directory), never by application code. It exposes the vtable
|
||||||
|
// system, shared module-level state, and internal helper functions that
|
||||||
|
// widget implementations need but the public API should not expose.
|
||||||
|
//
|
||||||
|
// The widget system is split across multiple .c files by concern:
|
||||||
|
// widgetCore.c — allocation, tree ops, focus management, shared helpers
|
||||||
|
// widgetLayout.c — measure + layout algorithms for box containers
|
||||||
|
// widgetEvent.c — mouse/keyboard dispatch, scrollbar management
|
||||||
|
// widgetOps.c — paint dispatch, overlay rendering
|
||||||
|
// widget*.c — per-type paint, event, and layout implementations
|
||||||
#ifndef WIDGET_INTERNAL_H
|
#ifndef WIDGET_INTERNAL_H
|
||||||
#define WIDGET_INTERNAL_H
|
#define WIDGET_INTERNAL_H
|
||||||
|
|
||||||
|
|
@ -15,6 +27,25 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Widget class vtable
|
// Widget class vtable
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Each widget type has a WidgetClassT entry in widgetClassTable[], indexed
|
||||||
|
// by WidgetTypeE. This is a C implementation of virtual dispatch: instead
|
||||||
|
// of a switch(type) in every operation, the code calls w->wclass->paint()
|
||||||
|
// etc. The vtable pointer is set once at widget creation and never changes.
|
||||||
|
//
|
||||||
|
// Flags encode static properties that the framework needs without calling
|
||||||
|
// into the vtable:
|
||||||
|
// FOCUSABLE — can receive keyboard focus (Tab navigation)
|
||||||
|
// BOX_CONTAINER — uses the generic VBox/HBox layout algorithm
|
||||||
|
// HORIZ_CONTAINER — lays out children horizontally (vs. default vertical)
|
||||||
|
// PAINTS_CHILDREN — the widget's paint function handles child rendering
|
||||||
|
// (e.g. TabControl only paints the active tab page)
|
||||||
|
// NO_HIT_RECURSE — hit testing stops at this widget and doesn't recurse
|
||||||
|
// into children (e.g. ListBox handles its own items)
|
||||||
|
//
|
||||||
|
// NULL function pointers are valid and mean "no-op" for that operation.
|
||||||
|
// paintOverlay is used by widgets that need to draw outside their bounds
|
||||||
|
// (dropdown popup lists), rendered in a separate pass after all widgets.
|
||||||
|
|
||||||
#define WCLASS_FOCUSABLE 0x0001
|
#define WCLASS_FOCUSABLE 0x0001
|
||||||
#define WCLASS_BOX_CONTAINER 0x0002
|
#define WCLASS_BOX_CONTAINER 0x0002
|
||||||
|
|
@ -35,11 +66,18 @@ typedef struct WidgetClassT {
|
||||||
void (*setText)(WidgetT *w, const char *text);
|
void (*setText)(WidgetT *w, const char *text);
|
||||||
} WidgetClassT;
|
} WidgetClassT;
|
||||||
|
|
||||||
|
// Global vtable array — one entry per WidgetTypeE value. Defined in
|
||||||
|
// widgetCore.c. Must stay in sync with the WidgetTypeE enum order.
|
||||||
extern const WidgetClassT *widgetClassTable[];
|
extern const WidgetClassT *widgetClassTable[];
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Validation macros
|
// Validation macros
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Defensive type-checking at the top of public widget API functions.
|
||||||
|
// Since WidgetT is a tagged union, calling a ListBox function with a
|
||||||
|
// Button widget would access the wrong union member and corrupt state.
|
||||||
|
// These macros provide a consistent early-return guard pattern.
|
||||||
|
|
||||||
#define VALIDATE_WIDGET(w, wtype, retval) \
|
#define VALIDATE_WIDGET(w, wtype, retval) \
|
||||||
do { if (!(w) || (w)->type != (wtype)) { return (retval); } } while (0)
|
do { if (!(w) || (w)->type != (wtype)) { return (retval); } } while (0)
|
||||||
|
|
@ -51,11 +89,16 @@ extern const WidgetClassT *widgetClassTable[];
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Modifier flags (BIOS INT 16h shift state bits)
|
// Modifier flags (BIOS INT 16h shift state bits). Duplicated from
|
||||||
|
// dvxTypes.h ACCEL_xxx constants because widget code references them
|
||||||
|
// frequently and the KEY_MOD_ prefix reads better in event handlers.
|
||||||
#define KEY_MOD_SHIFT 0x03
|
#define KEY_MOD_SHIFT 0x03
|
||||||
#define KEY_MOD_CTRL 0x04
|
#define KEY_MOD_CTRL 0x04
|
||||||
#define KEY_MOD_ALT 0x08
|
#define KEY_MOD_ALT 0x08
|
||||||
|
|
||||||
|
// Layout and geometry constants. These define the visual metrics of
|
||||||
|
// each widget type and are tuned for readability at 640x480 with 8px
|
||||||
|
// wide fonts. Changing these affects every instance of the widget type.
|
||||||
#define DEFAULT_SPACING 4
|
#define DEFAULT_SPACING 4
|
||||||
#define DEFAULT_PADDING 4
|
#define DEFAULT_PADDING 4
|
||||||
#define SEPARATOR_THICKNESS 2
|
#define SEPARATOR_THICKNESS 2
|
||||||
|
|
@ -82,6 +125,8 @@ extern const WidgetClassT *widgetClassTable[];
|
||||||
#define TREE_EXPAND_SIZE 9
|
#define TREE_EXPAND_SIZE 9
|
||||||
#define TREE_ICON_GAP 4
|
#define TREE_ICON_GAP 4
|
||||||
#define TREE_BORDER 2
|
#define TREE_BORDER 2
|
||||||
|
// WGT_SB_W is the widget-internal scrollbar width (slightly narrower than
|
||||||
|
// the window-level SCROLLBAR_WIDTH) to fit within widget content areas.
|
||||||
#define WGT_SB_W 14
|
#define WGT_SB_W 14
|
||||||
#define TREE_MIN_ROWS 4
|
#define TREE_MIN_ROWS 4
|
||||||
#define SB_MIN_THUMB 14
|
#define SB_MIN_THUMB 14
|
||||||
|
|
@ -100,8 +145,12 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classic Windows 3.1 embossed (etched) text for disabled widgets:
|
// Classic Windows 3.1 embossed (etched) text for disabled widgets.
|
||||||
// Draw text at +1,+1 in highlight, then at 0,0 in shadow.
|
// The illusion works by drawing the text twice: first offset +1,+1 in
|
||||||
|
// the highlight color (creating a light "shadow" behind the text), then
|
||||||
|
// at the original position in the shadow color. The result is text that
|
||||||
|
// appears chiseled into the surface — the universal visual indicator for
|
||||||
|
// "greyed out" in Motif and Windows 3.x era GUIs.
|
||||||
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
|
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
|
||||||
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
||||||
drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
||||||
|
|
@ -115,51 +164,89 @@ static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Shared state (defined in widgetCore.c)
|
// Shared state (defined in widgetCore.c)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Module-level globals for tracking active interactive operations.
|
||||||
|
// Only one of these can be active at a time (e.g. you can't drag a
|
||||||
|
// slider and resize a listview column simultaneously). Using globals
|
||||||
|
// rather than per-widget state avoids bloating every widget with fields
|
||||||
|
// that are only relevant during a single drag operation. The tradeoff
|
||||||
|
// is that these are truly global — but since the GUI is single-threaded
|
||||||
|
// and only one mouse can exist, this is safe.
|
||||||
|
|
||||||
extern bool sDebugLayout;
|
extern bool sDebugLayout;
|
||||||
extern WidgetT *sClosedPopup;
|
extern WidgetT *sClosedPopup; // popup that was just closed (prevents immediate reopen)
|
||||||
extern WidgetT *sFocusedWidget;
|
extern WidgetT *sFocusedWidget; // currently focused widget across all windows
|
||||||
extern WidgetT *sKeyPressedBtn;
|
extern WidgetT *sKeyPressedBtn; // button being held via keyboard (Space/Enter)
|
||||||
extern WidgetT *sOpenPopup;
|
extern WidgetT *sOpenPopup; // dropdown/combobox with open popup list
|
||||||
extern WidgetT *sPressedButton;
|
extern WidgetT *sPressedButton; // button/imagebutton being held via mouse
|
||||||
extern WidgetT *sDragSlider;
|
extern WidgetT *sDragSlider; // slider being dragged
|
||||||
extern WidgetT *sDrawingCanvas;
|
extern WidgetT *sDrawingCanvas; // canvas receiving drag events
|
||||||
extern WidgetT *sDragTextSelect;
|
extern WidgetT *sDragTextSelect; // text widget with active mouse selection drag
|
||||||
extern int32_t sDragOffset;
|
extern int32_t sDragOffset; // mouse offset within thumb/handle at drag start
|
||||||
extern WidgetT *sResizeListView;
|
extern WidgetT *sResizeListView; // listview whose column is being resized
|
||||||
extern int32_t sResizeCol;
|
extern int32_t sResizeCol; // which column is being resized
|
||||||
extern int32_t sResizeStartX;
|
extern int32_t sResizeStartX; // mouse X at start of column resize
|
||||||
extern int32_t sResizeOrigW;
|
extern int32_t sResizeOrigW; // original column width at start of resize
|
||||||
extern WidgetT *sDragSplitter;
|
extern WidgetT *sDragSplitter; // splitter being dragged
|
||||||
extern int32_t sDragSplitStart;
|
extern int32_t sDragSplitStart; // mouse position at start of splitter drag
|
||||||
extern WidgetT *sDragReorder;
|
extern WidgetT *sDragReorder; // listbox/treeview item being drag-reordered
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Core functions (widgetCore.c)
|
// Core functions (widgetCore.c)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Tree manipulation
|
||||||
void widgetAddChild(WidgetT *parent, WidgetT *child);
|
void widgetAddChild(WidgetT *parent, WidgetT *child);
|
||||||
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type);
|
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
|
||||||
void widgetClearFocus(WidgetT *root);
|
|
||||||
int32_t widgetCountVisibleChildren(const WidgetT *w);
|
|
||||||
void widgetDestroyChildren(WidgetT *w);
|
void widgetDestroyChildren(WidgetT *w);
|
||||||
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
|
|
||||||
WidgetT *widgetFindByAccel(WidgetT *root, char key);
|
// Allocate a new widget, set its type and vtable, and add it as a child.
|
||||||
|
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type);
|
||||||
|
|
||||||
|
// Focus management — Tab/Shift-Tab navigation walks the tree in creation
|
||||||
|
// order, skipping non-focusable and hidden widgets.
|
||||||
|
void widgetClearFocus(WidgetT *root);
|
||||||
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
|
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
|
||||||
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
|
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
|
||||||
|
|
||||||
|
// Alt+key accelerator lookup — searches the tree for a widget whose
|
||||||
|
// accelKey matches, used for keyboard navigation of labeled controls.
|
||||||
|
WidgetT *widgetFindByAccel(WidgetT *root, char key);
|
||||||
|
|
||||||
|
// Utility queries
|
||||||
|
int32_t widgetCountVisibleChildren(const WidgetT *w);
|
||||||
int32_t widgetFrameBorderWidth(const WidgetT *w);
|
int32_t widgetFrameBorderWidth(const WidgetT *w);
|
||||||
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
|
|
||||||
bool widgetIsFocusable(WidgetTypeE type);
|
bool widgetIsFocusable(WidgetTypeE type);
|
||||||
bool widgetIsBoxContainer(WidgetTypeE type);
|
bool widgetIsBoxContainer(WidgetTypeE type);
|
||||||
bool widgetIsHorizContainer(WidgetTypeE type);
|
bool widgetIsHorizContainer(WidgetTypeE type);
|
||||||
|
|
||||||
|
// Hit testing — find the deepest widget containing the point (x,y).
|
||||||
|
// Respects WCLASS_NO_HIT_RECURSE to stop at list/tree widgets.
|
||||||
|
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
|
||||||
|
|
||||||
|
// Translate arrow/PgUp/PgDn/Home/End keys into list index changes.
|
||||||
|
// Shared by ListBox, ListView, Dropdown, and TreeView.
|
||||||
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
|
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
|
||||||
|
|
||||||
|
// Compute popup position for a dropdown/combobox, clamped to screen bounds.
|
||||||
|
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
|
||||||
|
|
||||||
|
// Paint a generic popup item list (used by dropdown, combobox, and popup menus).
|
||||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
|
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
|
||||||
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
|
|
||||||
|
// Compute scrollbar thumb position and size for a given track length,
|
||||||
|
// content size, visible size, and scroll position. Used by both widget-
|
||||||
|
// internal scrollbars and the window-level scrollbar drawing code.
|
||||||
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
|
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Shared scrollbar functions (widgetScrollbar.c)
|
// Shared scrollbar functions (widgetScrollbar.c)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Widget-internal scrollbar rendering and hit testing, shared across
|
||||||
|
// ListBox, ListView, TreeView, TextArea, ScrollPane, and AnsiTerm.
|
||||||
|
// These operate on abstract track coordinates (position + length) so
|
||||||
|
// the same code handles both horizontal and vertical scrollbars.
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ScrollHitNoneE,
|
ScrollHitNoneE,
|
||||||
|
|
@ -177,6 +264,19 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Layout functions (widgetLayout.c)
|
// Layout functions (widgetLayout.c)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// The layout algorithm is a two-pass flexbox-like system:
|
||||||
|
//
|
||||||
|
// calcMinSize (bottom-up): each leaf widget reports its minimum size
|
||||||
|
// (e.g. button = text width + padding). Containers sum their children's
|
||||||
|
// minimums plus spacing along the main axis, and take the maximum of
|
||||||
|
// children's minimums on the cross axis.
|
||||||
|
//
|
||||||
|
// layout (top-down): starting from the available space (window content
|
||||||
|
// area), each container distributes space to children. Children that fit
|
||||||
|
// at their minimum get their minimum. Remaining space is distributed
|
||||||
|
// proportionally by weight to flexible children. This happens recursively
|
||||||
|
// down the tree.
|
||||||
|
|
||||||
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
|
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font);
|
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font);
|
||||||
|
|
@ -186,8 +286,16 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Event functions (widgetEvent.c)
|
// Event functions (widgetEvent.c)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// These are the WindowT callback implementations installed by wgtInitWindow().
|
||||||
|
// They bridge the window manager's callback interface to the widget tree's
|
||||||
|
// event dispatch. Each one locates the appropriate widget (via hit-test for
|
||||||
|
// mouse events, or the focused widget for keyboard events) and calls the
|
||||||
|
// widget's vtable handler.
|
||||||
|
|
||||||
|
// Sync window-level scrollbar ranges to match the widget tree's overflow.
|
||||||
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
|
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
|
||||||
|
|
||||||
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
|
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
|
||||||
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
||||||
void widgetOnPaint(WindowT *win, RectT *dirtyArea);
|
void widgetOnPaint(WindowT *win, RectT *dirtyArea);
|
||||||
|
|
@ -310,11 +418,23 @@ void widgetTextInputDestroy(WidgetT *w);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Per-widget mouse/key functions
|
// Per-widget mouse/key functions
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Shared text editing helpers used by TextInput, TextArea, ComboBox, and
|
||||||
|
// Spinner. The widgetTextEditOnKey function implements the common subset
|
||||||
|
// of single-line text editing behavior (cursor movement, selection,
|
||||||
|
// copy/paste, undo) shared across all text-editing widgets, so changes
|
||||||
|
// to editing behavior only need to happen in one place.
|
||||||
|
|
||||||
|
// Clear text selections in all widgets except the given one (ensures only
|
||||||
|
// one text selection is active at a time across the entire GUI).
|
||||||
void clearOtherSelections(WidgetT *except);
|
void clearOtherSelections(WidgetT *except);
|
||||||
|
|
||||||
void clipboardCopy(const char *text, int32_t len);
|
void clipboardCopy(const char *text, int32_t len);
|
||||||
const char *clipboardGet(int32_t *outLen);
|
const char *clipboardGet(int32_t *outLen);
|
||||||
bool isWordChar(char c);
|
bool isWordChar(char c);
|
||||||
|
|
||||||
|
// Detect double/triple clicks based on timing and position proximity.
|
||||||
|
// Returns 1 for single, 2 for double, 3 for triple click.
|
||||||
int32_t multiClickDetect(int32_t vx, int32_t vy);
|
int32_t multiClickDetect(int32_t vx, int32_t vy);
|
||||||
void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
|
@ -351,6 +471,11 @@ void widgetTabControlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
// Shared single-line text editing logic. Takes pointers to all the edit
|
||||||
|
// state fields so the same code works for TextInput, ComboBox, and Spinner
|
||||||
|
// (which store these fields in different union members). This avoids
|
||||||
|
// duplicating the full editing logic (cursor movement, word selection,
|
||||||
|
// clipboard, undo) three times.
|
||||||
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
|
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
|
||||||
void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
|
@ -359,9 +484,15 @@ int32_t wordStart(const char *buf, int32_t pos);
|
||||||
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
|
||||||
// Drag-reorder helpers (dispatch to ListBox/ListView/TreeView)
|
// Drag-reorder helpers — generic drag-and-drop reordering shared by
|
||||||
|
// ListBox, ListView, and TreeView. The update function tracks the mouse
|
||||||
|
// position and computes the drop target, the drop function commits the
|
||||||
|
// reorder by relinking the item in the data structure.
|
||||||
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
||||||
void widgetReorderDrop(WidgetT *w);
|
void widgetReorderDrop(WidgetT *w);
|
||||||
|
|
||||||
|
// Iterate to the next visible item in a tree view (skipping collapsed
|
||||||
|
// subtrees). Used for keyboard navigation and rendering.
|
||||||
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView);
|
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView);
|
||||||
|
|
||||||
#endif // WIDGET_INTERNAL_H
|
#endif // WIDGET_INTERNAL_H
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,19 @@
|
||||||
// widgetLabel.c — Label widget
|
// widgetLabel.c — Label widget
|
||||||
|
//
|
||||||
|
// Static text display — the simplest widget. Not focusable, not interactive.
|
||||||
|
// Supports accelerator keys via '&' prefix: when the user presses Alt+key,
|
||||||
|
// focus moves to the next focusable widget after this label. This follows
|
||||||
|
// the Win3.1 convention where labels act as keyboard shortcuts for adjacent
|
||||||
|
// controls (e.g., a label "&Name:" before a text input).
|
||||||
|
//
|
||||||
|
// The text pointer is stored directly (not copied) — the caller must ensure
|
||||||
|
// the string remains valid for the widget's lifetime, or use widgetLabelSetText
|
||||||
|
// to update it. This avoids unnecessary allocations for the common case of
|
||||||
|
// literal string labels.
|
||||||
|
//
|
||||||
|
// Background is transparent by default (bgColor == 0 means use the parent's
|
||||||
|
// content background color from the color scheme). The +1 in calcMinH adds a
|
||||||
|
// pixel of breathing room below the text baseline.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,30 @@
|
||||||
// widgetLayout.c — Layout engine (measure + arrange)
|
// widgetLayout.c — Layout engine (measure + arrange)
|
||||||
|
//
|
||||||
|
// Implements a two-pass layout algorithm inspired by CSS flexbox:
|
||||||
|
// Pass 1 (Measure): bottom-up calculation of each widget's minimum
|
||||||
|
// size. Leaf widgets report their intrinsic size (text width, etc.)
|
||||||
|
// and containers sum their children's minimums plus padding/spacing.
|
||||||
|
// Pass 2 (Arrange): top-down assignment of actual positions and sizes.
|
||||||
|
// The root gets the full available area, then each container divides
|
||||||
|
// its space among children based on their minimums and weight values.
|
||||||
|
//
|
||||||
|
// Why two passes instead of one:
|
||||||
|
// Containers can't assign sizes until they know all children's minimums
|
||||||
|
// (to calculate extra space for weight distribution). And children can't
|
||||||
|
// know their minimums until their subtrees are measured. So measure must
|
||||||
|
// be bottom-up before arrange can be top-down.
|
||||||
|
//
|
||||||
|
// Why flexbox-like rather than constraint-based:
|
||||||
|
// Flexbox maps naturally to the VBox/HBox container model — it's simple
|
||||||
|
// to understand, implement, and debug. Constraint-based layout (like
|
||||||
|
// Cassowary) would add significant complexity for little benefit in a
|
||||||
|
// DOS GUI where most layouts are linear stacks of widgets.
|
||||||
|
//
|
||||||
|
// Size hints use a tagged integer encoding (see dvxWidget.h):
|
||||||
|
// High 2 bits encode the unit type (pixels, characters, percent),
|
||||||
|
// low 30 bits encode the numeric value. This packs three distinct
|
||||||
|
// unit types into a single int32_t without needing a separate struct.
|
||||||
|
// The wgtResolveSize() function decodes the tag and converts to pixels.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -6,6 +32,15 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetCalcMinSizeBox
|
// widgetCalcMinSizeBox
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Measure pass for box containers (VBox, HBox, RadioGroup, StatusBar,
|
||||||
|
// Toolbar, Frame, TabPage). Recursively measures all visible children,
|
||||||
|
// then computes this container's minimum size as:
|
||||||
|
// main axis: sum of children minimums + gaps + padding
|
||||||
|
// cross axis: max of children minimums + padding
|
||||||
|
//
|
||||||
|
// "Main axis" is vertical for VBox, horizontal for HBox. The horiz
|
||||||
|
// flag flips the calculation so VBox and HBox share the same code.
|
||||||
|
|
||||||
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
bool horiz = widgetIsHorizContainer(w->type);
|
bool horiz = widgetIsHorizContainer(w->type);
|
||||||
|
|
@ -23,7 +58,9 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
gap = DEFAULT_SPACING;
|
gap = DEFAULT_SPACING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame: box starts at y + charHeight/2 so the title sits on the top line
|
// Frame reserves extra vertical space at the top so the frame title
|
||||||
|
// text can sit centered on the top border line (like Windows group boxes).
|
||||||
|
// Without this, the first child would overlap the title.
|
||||||
int32_t frameExtraTop = 0;
|
int32_t frameExtraTop = 0;
|
||||||
|
|
||||||
if (w->type == WidgetFrameE) {
|
if (w->type == WidgetFrameE) {
|
||||||
|
|
@ -31,7 +68,8 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
pad = DEFAULT_PADDING;
|
pad = DEFAULT_PADDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toolbar and StatusBar use tighter padding
|
// Toolbar and StatusBar use minimal padding to pack controls tightly,
|
||||||
|
// matching the compact feel of classic Windows/Motif toolbars.
|
||||||
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
|
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
|
||||||
pad = TOOLBAR_PAD;
|
pad = TOOLBAR_PAD;
|
||||||
gap = TOOLBAR_GAP;
|
gap = TOOLBAR_GAP;
|
||||||
|
|
@ -85,7 +123,17 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetCalcMinSizeTree
|
// widgetCalcMinSizeTree
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Top-level measure dispatcher. Recurses through the widget tree.
|
// Top-level measure dispatcher. Routes to the appropriate measure
|
||||||
|
// function based on widget type:
|
||||||
|
// - Box containers use the generic widgetCalcMinSizeBox()
|
||||||
|
// - Widgets with custom layout (TabControl, TreeView, ScrollPane,
|
||||||
|
// Splitter) provide their own calcMinSize via the vtable
|
||||||
|
// - Widgets without calcMinSize get 0x0 (they rely on size hints)
|
||||||
|
//
|
||||||
|
// After the type-specific measure, explicit minW/minH size hints
|
||||||
|
// are applied as a floor. This allows "this widget should be at
|
||||||
|
// least N pixels/chars wide" without changing the widget's natural
|
||||||
|
// size calculation. Hints only increase the minimum, never shrink it.
|
||||||
|
|
||||||
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
||||||
if (widgetIsBoxContainer(w->type)) {
|
if (widgetIsBoxContainer(w->type)) {
|
||||||
|
|
@ -97,7 +145,10 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
||||||
w->calcMinH = 0;
|
w->calcMinH = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply size hints (override calculated minimum)
|
// Size hints act as a floor: if the app specified wgtPixels(200) as
|
||||||
|
// minW, the widget will be at least 200px wide even if its intrinsic
|
||||||
|
// content is smaller. This is resolved at measure time (not arrange)
|
||||||
|
// so parent containers see the correct minimum when distributing space.
|
||||||
if (w->minW) {
|
if (w->minW) {
|
||||||
int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);
|
int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);
|
||||||
|
|
||||||
|
|
@ -119,6 +170,27 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetLayoutBox
|
// widgetLayoutBox
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Arrange pass for box containers. This is the core of the flexbox-
|
||||||
|
// like layout algorithm, working in two sub-passes:
|
||||||
|
//
|
||||||
|
// Sub-pass 1: Sum all children's minimum sizes and total weight.
|
||||||
|
// "Extra space" = available main-axis size - total minimum sizes.
|
||||||
|
//
|
||||||
|
// Sub-pass 2: Assign each child's position and size.
|
||||||
|
// - Each child gets at least its minimum size.
|
||||||
|
// - Extra space is distributed proportionally by weight (like CSS
|
||||||
|
// flex-grow). A child with weight=100 gets twice as much extra
|
||||||
|
// as one with weight=50.
|
||||||
|
// - If total weight is 0 (all children are fixed-size), extra space
|
||||||
|
// is distributed according to the container's alignment: start
|
||||||
|
// (default), center, or end.
|
||||||
|
// - maxW/maxH constraints cap a child's final size.
|
||||||
|
// - Cross-axis size is the full available cross dimension (minus
|
||||||
|
// any maxW/maxH constraint on that axis).
|
||||||
|
//
|
||||||
|
// After positioning each child, widgetLayoutChildren() recurses to
|
||||||
|
// lay out the child's own subtree.
|
||||||
|
|
||||||
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
|
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
bool horiz = widgetIsHorizContainer(w->type);
|
bool horiz = widgetIsHorizContainer(w->type);
|
||||||
|
|
@ -167,7 +239,9 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t availMain = horiz ? (innerW - totalGap) : (innerH - totalGap);
|
int32_t availMain = horiz ? (innerW - totalGap) : (innerH - totalGap);
|
||||||
int32_t availCross = horiz ? innerH : innerW;
|
int32_t availCross = horiz ? innerH : innerW;
|
||||||
|
|
||||||
// First pass: sum minimum sizes and total weight
|
// Sub-pass 1: accumulate children's minimum sizes and weights.
|
||||||
|
// totalMin is the space needed if every child gets exactly its minimum.
|
||||||
|
// totalWeight determines how to split any leftover space.
|
||||||
int32_t totalMin = 0;
|
int32_t totalMin = 0;
|
||||||
int32_t totalWeight = 0;
|
int32_t totalWeight = 0;
|
||||||
|
|
||||||
|
|
@ -187,7 +261,9 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
extraSpace = 0;
|
extraSpace = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute alignment offset for main axis
|
// Alignment only applies when no children have weight (all fixed-size).
|
||||||
|
// When weights exist, the extra space is consumed by weighted children
|
||||||
|
// and alignment has no effect.
|
||||||
int32_t alignOffset = 0;
|
int32_t alignOffset = 0;
|
||||||
|
|
||||||
if (totalWeight == 0 && extraSpace > 0) {
|
if (totalWeight == 0 && extraSpace > 0) {
|
||||||
|
|
@ -269,7 +345,11 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetLayoutChildren
|
// widgetLayoutChildren
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Top-level layout dispatcher.
|
// Top-level layout dispatcher. Mirrors the measure dispatcher
|
||||||
|
// in widgetCalcMinSizeTree(): box containers use the generic
|
||||||
|
// algorithm, widgets with custom layout (TabControl, TreeView,
|
||||||
|
// ScrollPane, Splitter) use their vtable entry, and leaf widgets
|
||||||
|
// do nothing (they have no children to lay out).
|
||||||
|
|
||||||
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
|
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
|
||||||
if (widgetIsBoxContainer(w->type)) {
|
if (widgetIsBoxContainer(w->type)) {
|
||||||
|
|
@ -283,6 +363,14 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtLayout
|
// wgtLayout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Public entry point: runs both passes on the entire widget tree.
|
||||||
|
// The root widget is positioned at (0,0) and given the full available
|
||||||
|
// area, then the arrange pass distributes space to its children.
|
||||||
|
//
|
||||||
|
// This is called from widgetManageScrollbars() and widgetOnPaint(),
|
||||||
|
// which may pass a virtual content size larger than the physical
|
||||||
|
// window if scrolling is needed.
|
||||||
|
|
||||||
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
|
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -305,6 +393,21 @@ void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtResolveSize
|
// wgtResolveSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Decodes a tagged size value into an actual pixel count.
|
||||||
|
//
|
||||||
|
// The tagged integer format uses the high 2 bits as a type tag:
|
||||||
|
// 00 = pixels (value is used directly)
|
||||||
|
// 01 = characters (value * charWidth, for text-relative sizing)
|
||||||
|
// 10 = percent (value% of parentSize, for responsive layouts)
|
||||||
|
//
|
||||||
|
// This encoding allows a single int32_t field to represent any of
|
||||||
|
// three unit types without needing a separate struct or enum.
|
||||||
|
// The tradeoff is that pixel values are limited to 30 bits (~1 billion),
|
||||||
|
// which is far more than any supported display resolution.
|
||||||
|
//
|
||||||
|
// A value of 0 means "auto" (use intrinsic/calculated size) — the
|
||||||
|
// caller checks for 0 before calling this function.
|
||||||
|
|
||||||
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
|
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
|
||||||
if (taggedSize == 0) {
|
if (taggedSize == 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,31 @@
|
||||||
// widgetListBox.c — ListBox widget (single and multi-select)
|
// widgetListBox.c — ListBox widget (single and multi-select)
|
||||||
|
//
|
||||||
|
// Scrollable list of text items with single-select or multi-select modes.
|
||||||
|
// Items are stored as external string pointers (not copied), with a vertical
|
||||||
|
// scrollbar appearing when the item count exceeds visible rows.
|
||||||
|
//
|
||||||
|
// Multi-select uses a parallel selBits array (one byte per item, 0 or 1)
|
||||||
|
// rather than a bitfield. Using a full byte per item wastes some memory but
|
||||||
|
// makes individual item toggle/test trivial without shift/mask operations,
|
||||||
|
// which matters when the selection code runs on every click and keyboard event.
|
||||||
|
//
|
||||||
|
// The selection model follows Windows explorer conventions:
|
||||||
|
// - Plain click: select one item, clear others, set anchor
|
||||||
|
// - Ctrl+click: toggle one item, update anchor
|
||||||
|
// - Shift+click: range select from anchor to clicked item
|
||||||
|
// - Shift+arrow: range select from anchor to cursor
|
||||||
|
// - Space: toggle current item (multi-select)
|
||||||
|
// - Ctrl+A: select all (multi-select)
|
||||||
|
//
|
||||||
|
// The "anchor" concept is key: it's the starting point for shift-select
|
||||||
|
// ranges. It's updated on non-shift clicks but stays fixed during shift
|
||||||
|
// operations, allowing the user to extend/shrink the selection by
|
||||||
|
// shift-clicking different endpoints.
|
||||||
|
//
|
||||||
|
// Drag-reorder support allows items to be rearranged by dragging. When
|
||||||
|
// enabled, a mouse-down initiates a drag (tracked via sDragReorder global),
|
||||||
|
// and a 2px horizontal line indicator shows the insertion point. The actual
|
||||||
|
// reordering is handled by the application's onReorder callback.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -22,6 +49,9 @@ static void selectRange(WidgetT *w, int32_t from, int32_t to);
|
||||||
// allocSelBits
|
// allocSelBits
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Allocate (or re-allocate) the selection bits array. Only allocated in
|
||||||
|
// multi-select mode — in single-select, selectedIdx alone tracks the
|
||||||
|
// selection. calloc initializes all bits to 0 (nothing selected).
|
||||||
static void allocSelBits(WidgetT *w) {
|
static void allocSelBits(WidgetT *w) {
|
||||||
if (w->as.listBox.selBits) {
|
if (w->as.listBox.selBits) {
|
||||||
free(w->as.listBox.selBits);
|
free(w->as.listBox.selBits);
|
||||||
|
|
@ -40,6 +70,9 @@ static void allocSelBits(WidgetT *w) {
|
||||||
// ensureScrollVisible
|
// ensureScrollVisible
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Adjust scroll position so the item at idx is within the visible viewport.
|
||||||
|
// If the item is above the viewport, scroll up to it. If below, scroll down
|
||||||
|
// to show it at the bottom. If already visible, do nothing.
|
||||||
static void ensureScrollVisible(WidgetT *w, int32_t idx) {
|
static void ensureScrollVisible(WidgetT *w, int32_t idx) {
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -66,6 +99,8 @@ static void ensureScrollVisible(WidgetT *w, int32_t idx) {
|
||||||
// selectRange
|
// selectRange
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Set selection bits for all items in the range [from, to] (inclusive,
|
||||||
|
// order-independent). Used for shift-click and shift-arrow range selection.
|
||||||
static void selectRange(WidgetT *w, int32_t from, int32_t to) {
|
static void selectRange(WidgetT *w, int32_t from, int32_t to) {
|
||||||
if (!w->as.listBox.selBits) {
|
if (!w->as.listBox.selBits) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -292,6 +327,10 @@ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
|
||||||
// widgetListBoxCalcMinSize
|
// widgetListBoxCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Min size accounts for the widest item, a scrollbar, padding, and border.
|
||||||
|
// Height is based on LISTBOX_MIN_ROWS (4 rows) so the listbox has a usable
|
||||||
|
// minimum height even when empty. The 8-character minimum width prevents
|
||||||
|
// the listbox from collapsing too narrow when items are short or absent.
|
||||||
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t maxItemW = w->as.listBox.maxItemLen * font->charWidth;
|
int32_t maxItemW = w->as.listBox.maxItemLen * font->charWidth;
|
||||||
int32_t minW = font->charWidth * 8;
|
int32_t minW = font->charWidth * 8;
|
||||||
|
|
@ -309,6 +348,10 @@ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetListBoxOnKey
|
// widgetListBoxOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Key handling: delegates to widgetNavigateIndex for cursor movement
|
||||||
|
// (Up, Down, Home, End, PgUp, PgDn) which returns the new index after
|
||||||
|
// navigation. This shared helper ensures consistent keyboard navigation
|
||||||
|
// across ListBox, ListView, and other scrollable widgets.
|
||||||
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) {
|
if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -384,6 +427,11 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetListBoxOnMouse
|
// widgetListBoxOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling: first checks if the click is on the scrollbar (right edge),
|
||||||
|
// then falls through to item click handling. The scrollbar hit-test uses
|
||||||
|
// widgetScrollbarHitTest which divides the scrollbar into arrow buttons, page
|
||||||
|
// regions, and thumb based on the click position. Item clicks are translated
|
||||||
|
// from pixel coordinates to item index using integer division by charHeight.
|
||||||
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
|
|
@ -490,6 +538,13 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetListBoxPaint
|
// widgetListBoxPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint: sunken bevel border, then iterate visible rows drawing text.
|
||||||
|
// Selected items get highlight background (full row width) with contrasting
|
||||||
|
// text color. In multi-select mode, the cursor item (selectedIdx) gets a
|
||||||
|
// dotted focus rect overlay — this is separate from the selection highlight
|
||||||
|
// so the user can see which item the keyboard cursor is on even when multiple
|
||||||
|
// items are selected. The scrollbar is drawn only when needed (item count
|
||||||
|
// exceeds visible rows), and the content width is reduced accordingly.
|
||||||
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
@ -551,7 +606,9 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw drag-reorder insertion indicator
|
// Draw drag-reorder insertion indicator: a 2px horizontal line at the
|
||||||
|
// drop position. The line is drawn between items (not on top of them)
|
||||||
|
// so it's clear where the dragged item will be inserted.
|
||||||
if (w->as.listBox.reorderable && w->as.listBox.dragIdx >= 0 && w->as.listBox.dropIdx >= 0) {
|
if (w->as.listBox.reorderable && w->as.listBox.dragIdx >= 0 && w->as.listBox.dropIdx >= 0) {
|
||||||
int32_t drop = w->as.listBox.dropIdx;
|
int32_t drop = w->as.listBox.dropIdx;
|
||||||
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
|
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,51 @@
|
||||||
// widgetListView.c — ListView (multi-column list) widget
|
// widgetListView.c — ListView (multi-column list) widget
|
||||||
|
//
|
||||||
|
// A multi-column list with clickable/sortable column headers, horizontal
|
||||||
|
// and vertical scrolling, column resize by dragging header borders, and
|
||||||
|
// multi-select support. This is the most complex widget in the toolkit.
|
||||||
|
//
|
||||||
|
// Data model: cell data is stored as a flat array of const char* pointers
|
||||||
|
// in row-major order (cellData[row * colCount + col]). This external data
|
||||||
|
// model (pointers, not copies) keeps memory usage minimal and avoids
|
||||||
|
// copying strings that the application already owns.
|
||||||
|
//
|
||||||
|
// ListView state is heap-allocated separately (ListViewDataT*) rather than
|
||||||
|
// inlined in the widget union because the state is too large for a union
|
||||||
|
// member — it includes per-column resolved widths, sort index, selection
|
||||||
|
// bits, and numerous scroll/drag state fields.
|
||||||
|
//
|
||||||
|
// Sort: clicking a column header toggles sort direction. Sorting uses an
|
||||||
|
// indirection array (sortIndex) rather than rearranging the cellData. This
|
||||||
|
// is critical because:
|
||||||
|
// 1. The application owns cellData and may not expect it to be mutated
|
||||||
|
// 2. Multi-select selBits are indexed by data row, not display row —
|
||||||
|
// rearranging data would invalidate all selection state
|
||||||
|
// 3. The indirection allows O(1) display↔data row mapping
|
||||||
|
//
|
||||||
|
// Insertion sort is used because it's stable (preserves original order for
|
||||||
|
// equal keys) and has good performance for the typical case of partially-sorted
|
||||||
|
// data. For the row counts typical in a DOS GUI (hundreds, not millions),
|
||||||
|
// insertion sort's O(n^2) worst case is acceptable and the constant factors
|
||||||
|
// are lower than quicksort.
|
||||||
|
//
|
||||||
|
// Column widths support four sizing modes via a tagged integer:
|
||||||
|
// - 0: auto-size (scan all data for widest string)
|
||||||
|
// - positive small: character count (resolved via font->charWidth)
|
||||||
|
// - positive large: pixel width
|
||||||
|
// - negative: percentage of parent width
|
||||||
|
// The resolution is done lazily in resolveColumnWidths, which is called on
|
||||||
|
// first paint or mouse event after data changes.
|
||||||
|
//
|
||||||
|
// Column resize: dragging a column header border (3px hot zone) resizes the
|
||||||
|
// column. The drag is tracked via sResizeListView/sResizeCol/sResizeStartX/
|
||||||
|
// sResizeOrigW globals. A minimum column width (LISTVIEW_MIN_COL_W) prevents
|
||||||
|
// columns from collapsing to zero.
|
||||||
|
//
|
||||||
|
// Horizontal scrolling is needed when total column width exceeds the widget's
|
||||||
|
// inner width. Both headers and data rows are offset by scrollPosH. The
|
||||||
|
// horizontal scrollbar appears automatically when needed, and its presence
|
||||||
|
// may trigger the vertical scrollbar to appear (and vice versa) — this
|
||||||
|
// two-pass logic handles the interdependency correctly.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -24,6 +71,8 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font);
|
||||||
// allocListViewSelBits
|
// allocListViewSelBits
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Same allocation strategy as ListBox's selBits — one byte per row,
|
||||||
|
// only allocated when multiSelect is enabled.
|
||||||
static void allocListViewSelBits(WidgetT *w) {
|
static void allocListViewSelBits(WidgetT *w) {
|
||||||
if (w->as.listView->selBits) {
|
if (w->as.listView->selBits) {
|
||||||
free(w->as.listView->selBits);
|
free(w->as.listView->selBits);
|
||||||
|
|
@ -46,6 +95,12 @@ static void allocListViewSelBits(WidgetT *w) {
|
||||||
// (stable) on the sort column. If no sort is active, frees
|
// (stable) on the sort column. If no sort is active, frees
|
||||||
// the index so paint uses natural order.
|
// the index so paint uses natural order.
|
||||||
|
|
||||||
|
// Build the sort indirection array. Rather than rearranging cellData (which
|
||||||
|
// the application owns and indexes into), we maintain a separate array that
|
||||||
|
// maps display-row → data-row. Paint and keyboard navigation work in display-row
|
||||||
|
// space, then map through sortIndex to get the data row for cell lookups and
|
||||||
|
// selection operations. This decoupling is essential because selBits are
|
||||||
|
// indexed by data row — sorting must not invalidate selection state.
|
||||||
static void listViewBuildSortIndex(WidgetT *w) {
|
static void listViewBuildSortIndex(WidgetT *w) {
|
||||||
int32_t rowCount = w->as.listView->rowCount;
|
int32_t rowCount = w->as.listView->rowCount;
|
||||||
int32_t sortCol = w->as.listView->sortCol;
|
int32_t sortCol = w->as.listView->sortCol;
|
||||||
|
|
@ -79,7 +134,13 @@ static void listViewBuildSortIndex(WidgetT *w) {
|
||||||
idx[i] = i;
|
idx[i] = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insertion sort — stable, O(n^2) but fine for typical row counts
|
// Insertion sort — stable (equal elements keep their original order),
|
||||||
|
// O(n^2) worst case but with very low constant factors. For typical
|
||||||
|
// ListView row counts (10s to low 100s), this beats quicksort because
|
||||||
|
// there's no recursion overhead, no stack usage, and the inner loop
|
||||||
|
// is a simple pointer chase. On a 486, the predictable memory access
|
||||||
|
// pattern also helps since insertion sort is cache-friendly (sequential
|
||||||
|
// access to adjacent elements).
|
||||||
bool ascending = (w->as.listView->sortDir == ListViewSortAscE);
|
bool ascending = (w->as.listView->sortDir == ListViewSortAscE);
|
||||||
|
|
||||||
for (int32_t i = 1; i < rowCount; i++) {
|
for (int32_t i = 1; i < rowCount; i++) {
|
||||||
|
|
@ -126,6 +187,14 @@ static void listViewBuildSortIndex(WidgetT *w) {
|
||||||
// to actual pixel widths. Auto-sized columns (width==0) scan
|
// to actual pixel widths. Auto-sized columns (width==0) scan
|
||||||
// cellData for the widest string in that column.
|
// cellData for the widest string in that column.
|
||||||
|
|
||||||
|
// Resolve tagged column widths to actual pixel values. Column widths in the
|
||||||
|
// ListViewColT definition can be specified as:
|
||||||
|
// 0 = auto (scan data for widest string, include header)
|
||||||
|
// positive = passed to wgtResolveSize (handles px, chars, or percent)
|
||||||
|
//
|
||||||
|
// Auto-sizing scans all rows for the widest string in that column, which
|
||||||
|
// is O(rows * cols) but only runs once per data change (totalColW is reset
|
||||||
|
// to 0 when data changes, triggering recalculation on next paint/mouse).
|
||||||
static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) {
|
static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t colCount = w->as.listView->colCount;
|
int32_t colCount = w->as.listView->colCount;
|
||||||
int32_t parentW = w->w - LISTVIEW_BORDER * 2;
|
int32_t parentW = w->w - LISTVIEW_BORDER * 2;
|
||||||
|
|
@ -174,6 +243,10 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) {
|
||||||
// Returns true if (vx, vy) is on a column header border (resize zone).
|
// Returns true if (vx, vy) is on a column header border (resize zone).
|
||||||
// Coordinates are in widget/virtual space (scroll-adjusted).
|
// Coordinates are in widget/virtual space (scroll-adjusted).
|
||||||
|
|
||||||
|
// Test if a point is on a column header border (within 3px). Used by the
|
||||||
|
// cursor-shape logic to show a resize cursor when hovering over column borders.
|
||||||
|
// The 3px zone on each side of the border line makes it easy to grab even
|
||||||
|
// on low-resolution displays where pixel-precise clicking is difficult.
|
||||||
bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
|
bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
|
||||||
if (!w || w->type != WidgetListViewE || w->as.listView->colCount == 0) {
|
if (!w || w->type != WidgetListViewE || w->as.listView->colCount == 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -208,6 +281,9 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
|
||||||
// widgetListViewDestroy
|
// widgetListViewDestroy
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Free all heap-allocated ListView state. The sortIndex and selBits are
|
||||||
|
// optional (may be NULL if no sort or single-select), so we check before
|
||||||
|
// freeing. The ListViewDataT struct itself is always freed.
|
||||||
void widgetListViewDestroy(WidgetT *w) {
|
void widgetListViewDestroy(WidgetT *w) {
|
||||||
if (w->as.listView->sortIndex) {
|
if (w->as.listView->sortIndex) {
|
||||||
free(w->as.listView->sortIndex);
|
free(w->as.listView->sortIndex);
|
||||||
|
|
@ -226,6 +302,11 @@ void widgetListViewDestroy(WidgetT *w) {
|
||||||
// wgtListView
|
// wgtListView
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Create a ListView widget. The ListViewDataT is heap-allocated separately
|
||||||
|
// because it's too large for the widget union (it contains fixed-size arrays
|
||||||
|
// for resolved column widths, plus numerous state fields for scrolling, sorting,
|
||||||
|
// selection, and drag-reorder). Default weight=100 makes the ListView expand
|
||||||
|
// to fill available space, which is the typical desired behavior.
|
||||||
WidgetT *wgtListView(WidgetT *parent) {
|
WidgetT *wgtListView(WidgetT *parent) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetListViewE);
|
WidgetT *w = widgetAlloc(parent, WidgetListViewE);
|
||||||
|
|
||||||
|
|
@ -400,6 +481,11 @@ void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
||||||
// wgtListViewSetReorderable
|
// wgtListViewSetReorderable
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Enable/disable drag-reorder. When reorderable is enabled, sorting is
|
||||||
|
// disabled because the two features are fundamentally incompatible: if the
|
||||||
|
// user manually arranges items, then sorts by a column, the manual order
|
||||||
|
// is lost. By clearing sortCol and freeing sortIndex, we ensure the display
|
||||||
|
// shows the natural data order which the user's drag operations will modify.
|
||||||
void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
|
void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
|
||||||
VALIDATE_WIDGET_VOID(w, WidgetListViewE);
|
VALIDATE_WIDGET_VOID(w, WidgetListViewE);
|
||||||
|
|
||||||
|
|
@ -487,6 +573,18 @@ void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetListViewOnKey
|
// widgetListViewOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Key handling must account for the sort indirection layer. Navigation happens
|
||||||
|
// in display-row space (what the user sees on screen), but selection and data
|
||||||
|
// access happen in data-row space (the original cellData indices). This means:
|
||||||
|
// 1. Convert selectedIdx (data row) to display row via linear search in sortIndex
|
||||||
|
// 2. Navigate in display space (widgetNavigateIndex)
|
||||||
|
// 3. Convert back to data row via sortIndex[displayRow]
|
||||||
|
// 4. Update selectedIdx and selBits using data-row indices
|
||||||
|
//
|
||||||
|
// Shift-select for sorted views is particularly tricky: the range must be
|
||||||
|
// computed in display-row space (what the user sees as contiguous) but applied
|
||||||
|
// to the data-row selBits. This means the selected data rows may not be
|
||||||
|
// contiguous in the original data order.
|
||||||
void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
if (!w || w->type != WidgetListViewE || w->as.listView->rowCount == 0) {
|
if (!w || w->type != WidgetListViewE || w->as.listView->rowCount == 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -627,6 +725,18 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetListViewOnMouse
|
// widgetListViewOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling is the most complex part of ListView. It handles four
|
||||||
|
// distinct hit regions (checked in priority order):
|
||||||
|
// 1. Vertical scrollbar (right edge, if visible)
|
||||||
|
// 2. Horizontal scrollbar (bottom edge, if visible)
|
||||||
|
// 3. Dead corner (when both scrollbars present — no action)
|
||||||
|
// 4. Column headers: resize drag (border +-3px) or sort toggle (click)
|
||||||
|
// 5. Data rows: item selection with Ctrl/Shift modifiers, double-click,
|
||||||
|
// and drag-reorder initiation
|
||||||
|
//
|
||||||
|
// The scrollbar visibility calculation is repeated here (matching paint)
|
||||||
|
// because scrollbar presence depends on content/widget dimensions which
|
||||||
|
// may have changed since the last paint.
|
||||||
void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->focused = true;
|
hit->focused = true;
|
||||||
|
|
||||||
|
|
@ -646,6 +756,10 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
bool needVSb = (hit->as.listView->rowCount > visibleRows);
|
bool needVSb = (hit->as.listView->rowCount > visibleRows);
|
||||||
bool needHSb = false;
|
bool needHSb = false;
|
||||||
|
|
||||||
|
// Scrollbar interdependency: adding a vertical scrollbar reduces innerW,
|
||||||
|
// which may cause columns to overflow and require a horizontal scrollbar.
|
||||||
|
// Adding a horizontal scrollbar reduces innerH, which may require a
|
||||||
|
// vertical scrollbar. This two-pass check handles the mutual dependency.
|
||||||
if (needVSb) {
|
if (needVSb) {
|
||||||
innerW -= WGT_SB_W;
|
innerW -= WGT_SB_W;
|
||||||
}
|
}
|
||||||
|
|
@ -772,7 +886,10 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
colX += cw;
|
colX += cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not on a border — check for sort click (disabled when reorderable)
|
// Not on a border — check for sort click (disabled when reorderable
|
||||||
|
// since sorting conflicts with manual ordering). Clicking a column
|
||||||
|
// toggles between ascending/descending; clicking a different column
|
||||||
|
// starts ascending.
|
||||||
if (!hit->as.listView->reorderable) {
|
if (!hit->as.listView->reorderable) {
|
||||||
colX = hit->x + LISTVIEW_BORDER - hit->as.listView->scrollPosH;
|
colX = hit->x + LISTVIEW_BORDER - hit->as.listView->scrollPosH;
|
||||||
|
|
||||||
|
|
@ -898,6 +1015,24 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
// widgetListViewPaint
|
// widgetListViewPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint: the most involved paint function in the widget toolkit. Renders
|
||||||
|
// in layers:
|
||||||
|
// 1. Outer sunken bevel border
|
||||||
|
// 2. Column headers (clipped to content width, scrolled by scrollPosH):
|
||||||
|
// each header is a raised bevel button with centered text and optional
|
||||||
|
// sort indicator (up/down triangle)
|
||||||
|
// 3. Data rows (clipped to data area, scrolled both H and V):
|
||||||
|
// background fill, then per-cell text rendering with alignment, then
|
||||||
|
// selection highlight, then cursor focus rect in multi-select mode
|
||||||
|
// 4. Drag-reorder insertion line (if active)
|
||||||
|
// 5. Scrollbars (V and/or H, with dead corner fill)
|
||||||
|
// 6. Outer focus rect
|
||||||
|
//
|
||||||
|
// Clip rectangles are saved/restored around the header and data sections to
|
||||||
|
// prevent rendering from bleeding into the scrollbar or border areas. This
|
||||||
|
// is cheaper than per-cell clipping and handles horizontal scroll correctly
|
||||||
|
// (partially visible columns at the edges are clipped by the display's clip
|
||||||
|
// rect rather than by the draw calls).
|
||||||
void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
@ -998,7 +1133,10 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort indicator
|
// Sort indicator: a small filled triangle (4px tall) in the header.
|
||||||
|
// Up triangle for ascending, down triangle for descending. Drawn as
|
||||||
|
// horizontal lines of increasing/decreasing width, same technique as
|
||||||
|
// the dropdown arrow glyph.
|
||||||
if (c == w->as.listView->sortCol && w->as.listView->sortDir != ListViewSortNoneE) {
|
if (c == w->as.listView->sortCol && w->as.listView->sortDir != ListViewSortNoneE) {
|
||||||
int32_t cx = hdrX + cw - LISTVIEW_SORT_W / 2 - LISTVIEW_PAD;
|
int32_t cx = hdrX + cw - LISTVIEW_SORT_W / 2 - LISTVIEW_PAD;
|
||||||
int32_t cy = baseY + headerH / 2;
|
int32_t cy = baseY + headerH / 2;
|
||||||
|
|
@ -1041,7 +1179,10 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
int32_t scrollPos = w->as.listView->scrollPos;
|
int32_t scrollPos = w->as.listView->scrollPos;
|
||||||
int32_t *sortIdx = w->as.listView->sortIndex;
|
int32_t *sortIdx = w->as.listView->sortIndex;
|
||||||
|
|
||||||
// Fill entire data area background first
|
// Fill entire data area background first. This is done as a single
|
||||||
|
// rectFill rather than per-row because it's cheaper to fill once and
|
||||||
|
// then overdraw selected rows than to compute and fill each unselected
|
||||||
|
// row's background separately.
|
||||||
rectFill(d, ops, baseX, dataY, innerW, innerH, bg);
|
rectFill(d, ops, baseX, dataY, innerW, innerH, bg);
|
||||||
|
|
||||||
bool multi = w->as.listView->multiSelect;
|
bool multi = w->as.listView->multiSelect;
|
||||||
|
|
@ -1089,6 +1230,12 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
|
|
||||||
int32_t tx = cellX + LISTVIEW_PAD;
|
int32_t tx = cellX + LISTVIEW_PAD;
|
||||||
|
|
||||||
|
// Column alignment: left (default), right, or center.
|
||||||
|
// Right/center alignment compute the available width and
|
||||||
|
// offset the text start position accordingly. This is done
|
||||||
|
// per-cell rather than using a general-purpose aligned draw
|
||||||
|
// function to keep the inner loop simple and avoid extra
|
||||||
|
// function call overhead per cell.
|
||||||
if (w->as.listView->cols[c].align == ListViewAlignRightE) {
|
if (w->as.listView->cols[c].align == ListViewAlignRightE) {
|
||||||
int32_t renderedW = cellLen * font->charWidth;
|
int32_t renderedW = cellLen * font->charWidth;
|
||||||
int32_t availW = cw - LISTVIEW_PAD * 2;
|
int32_t availW = cw - LISTVIEW_PAD * 2;
|
||||||
|
|
@ -1139,7 +1286,10 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
int32_t sbY = w->y + w->h - LISTVIEW_BORDER - WGT_SB_W;
|
int32_t sbY = w->y + w->h - LISTVIEW_BORDER - WGT_SB_W;
|
||||||
widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalColW, innerW, w->as.listView->scrollPosH);
|
widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalColW, innerW, w->as.listView->scrollPosH);
|
||||||
|
|
||||||
// Fill dead corner when both scrollbars present
|
// Fill the dead corner when both scrollbars are present. This is the
|
||||||
|
// small square at the intersection of the two scrollbars (bottom-right)
|
||||||
|
// that doesn't belong to either. Without filling it, stale content
|
||||||
|
// from previous paints would show through.
|
||||||
if (needVSb) {
|
if (needVSb) {
|
||||||
rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace);
|
rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
// widgetOps.c — Paint dispatcher and public widget operations
|
// widgetOps.c — Paint dispatcher and public widget operations
|
||||||
|
//
|
||||||
|
// This file contains two categories of functions:
|
||||||
|
// 1. The paint dispatcher (widgetPaintOne, widgetPaintOverlays, wgtPaint)
|
||||||
|
// which walks the widget tree and calls per-type paint functions.
|
||||||
|
// 2. Public operations (wgtSetText, wgtSetEnabled, wgtSetVisible,
|
||||||
|
// wgtFind, wgtDestroy, etc.) that form the widget system's public API.
|
||||||
|
//
|
||||||
|
// The paint dispatcher and the public operations are in the same file
|
||||||
|
// because they share the same invalidation infrastructure (wgtInvalidate
|
||||||
|
// and wgtInvalidatePaint).
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -7,8 +17,12 @@
|
||||||
// debugContainerBorder
|
// debugContainerBorder
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Draw a 1px border in a garish neon color derived from the widget
|
// Draws a 1px border in a neon color derived from the widget pointer.
|
||||||
// pointer so every container gets a distinct, ugly color.
|
// The Knuth multiplicative hash (2654435761) distributes pointer values
|
||||||
|
// across the palette evenly so adjacent containers get different colors.
|
||||||
|
// This is only active when sDebugLayout is true (toggled via
|
||||||
|
// wgtSetDebugLayout), used during development to visualize container
|
||||||
|
// boundaries and diagnose layout issues.
|
||||||
|
|
||||||
static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
|
static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
|
||||||
static const uint8_t palette[][3] = {
|
static const uint8_t palette[][3] = {
|
||||||
|
|
@ -41,8 +55,17 @@ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
|
||||||
// widgetPaintOne
|
// widgetPaintOne
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Paint a single widget and its children. Dispatches to per-widget
|
// Recursive paint walker. For each visible widget:
|
||||||
// paint functions defined in their respective files.
|
// 1. Call the widget's paint function (if any) via vtable.
|
||||||
|
// 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion —
|
||||||
|
// the widget's paint function already handled its children
|
||||||
|
// (e.g. TabControl only paints the active tab's children).
|
||||||
|
// 3. Otherwise, recurse into children (default child painting).
|
||||||
|
// 4. Draw debug borders on top if debug layout is enabled.
|
||||||
|
//
|
||||||
|
// The paint order is parent-before-children, which means parent
|
||||||
|
// backgrounds are drawn first and children paint on top. This is
|
||||||
|
// the standard painter's algorithm for nested UI elements.
|
||||||
|
|
||||||
void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
if (!w->visible) {
|
if (!w->visible) {
|
||||||
|
|
@ -80,7 +103,12 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
|
||||||
// ============================================================
|
// ============================================================
|
||||||
//
|
//
|
||||||
// Paints popup overlays (open dropdowns/comboboxes) on top of
|
// Paints popup overlays (open dropdowns/comboboxes) on top of
|
||||||
// the widget tree. Called after the main paint pass.
|
// the widget tree. Called AFTER the main paint pass so popups
|
||||||
|
// always render above all other widgets regardless of tree position.
|
||||||
|
//
|
||||||
|
// Only one popup can be open at a time (tracked by sOpenPopup).
|
||||||
|
// The tree-root ownership check prevents a popup from one window
|
||||||
|
// being painted into a different window's content buffer.
|
||||||
|
|
||||||
void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
if (!sOpenPopup) {
|
if (!sOpenPopup) {
|
||||||
|
|
@ -107,6 +135,18 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtDestroy
|
// wgtDestroy
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Destroys a widget and its entire subtree. The order is:
|
||||||
|
// 1. Unlink from parent (so the parent doesn't reference freed memory)
|
||||||
|
// 2. Recursively destroy all children (depth-first)
|
||||||
|
// 3. Call the widget's own destroy callback (free buffers, etc.)
|
||||||
|
// 4. Clear any global state that references this widget
|
||||||
|
// 5. Clear the window's root pointer if this was the root
|
||||||
|
// 6. Free the widget memory
|
||||||
|
//
|
||||||
|
// This ordering ensures that per-widget destroy callbacks can still
|
||||||
|
// access the widget's data (step 3 comes after child cleanup but
|
||||||
|
// before the widget itself is freed).
|
||||||
|
|
||||||
void wgtDestroy(WidgetT *w) {
|
void wgtDestroy(WidgetT *w) {
|
||||||
if (!w) {
|
if (!w) {
|
||||||
|
|
@ -152,6 +192,12 @@ void wgtDestroy(WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetHashName — djb2 hash for widget name lookup
|
// widgetHashName — djb2 hash for widget name lookup
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// The djb2 hash function (Dan Bernstein) is used for fast name-based
|
||||||
|
// widget lookup. The hash is stored in each widget's nameHash field
|
||||||
|
// so that wgtFind() can reject non-matches without calling strcmp()
|
||||||
|
// on every node. In a tree of 100 widgets, this typically reduces
|
||||||
|
// the search from 100 strcmp calls to 1-2.
|
||||||
|
|
||||||
static uint32_t widgetHashName(const char *s) {
|
static uint32_t widgetHashName(const char *s) {
|
||||||
uint32_t h = 5381;
|
uint32_t h = 5381;
|
||||||
|
|
@ -168,6 +214,11 @@ static uint32_t widgetHashName(const char *s) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtFindByHash — recursive search with hash fast-reject
|
// wgtFindByHash — recursive search with hash fast-reject
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Depth-first search by name with hash pre-screening. The hash
|
||||||
|
// comparison (integer ==) is much cheaper than strcmp and eliminates
|
||||||
|
// most non-matches. A full strcmp is only done when hashes match,
|
||||||
|
// which should be the target widget or a rare collision.
|
||||||
|
|
||||||
static WidgetT *wgtFindByHash(WidgetT *root, const char *name, uint32_t hash) {
|
static WidgetT *wgtFindByHash(WidgetT *root, const char *name, uint32_t hash) {
|
||||||
if (root->nameHash == hash && root->name[0] && strcmp(root->name, name) == 0) {
|
if (root->nameHash == hash && root->name[0] && strcmp(root->name, name) == 0) {
|
||||||
|
|
@ -219,6 +270,13 @@ void wgtSetName(WidgetT *w, const char *name) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtGetContext
|
// wgtGetContext
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Retrieves the AppContextT from any widget by walking up to the root.
|
||||||
|
// The root widget stores the context in its userData field (set during
|
||||||
|
// wgtInitWindow). This is the only way to get the AppContextT from
|
||||||
|
// deep inside the widget tree without passing it as a parameter
|
||||||
|
// through every function call. The walk is O(depth) but widget trees
|
||||||
|
// are shallow (typically 3-6 levels deep).
|
||||||
|
|
||||||
AppContextT *wgtGetContext(const WidgetT *w) {
|
AppContextT *wgtGetContext(const WidgetT *w) {
|
||||||
if (!w) {
|
if (!w) {
|
||||||
|
|
@ -238,6 +296,11 @@ AppContextT *wgtGetContext(const WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtGetText
|
// wgtGetText
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Polymorphic text getter — dispatches through the vtable to the
|
||||||
|
// appropriate getText implementation for the widget's type. Returns
|
||||||
|
// an empty string (not NULL) if the widget has no text or no getText
|
||||||
|
// handler, so callers don't need NULL checks.
|
||||||
|
|
||||||
const char *wgtGetText(const WidgetT *w) {
|
const char *wgtGetText(const WidgetT *w) {
|
||||||
if (!w) {
|
if (!w) {
|
||||||
|
|
@ -255,6 +318,15 @@ const char *wgtGetText(const WidgetT *w) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtInitWindow
|
// wgtInitWindow
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Sets up a window for widget-based content. Creates a root VBox
|
||||||
|
// container and installs the four window callbacks (onPaint, onMouse,
|
||||||
|
// onKey, onResize) that bridge WM events into the widget system.
|
||||||
|
//
|
||||||
|
// The root widget's userData points to the AppContextT, which is
|
||||||
|
// the bridge back to the display, font, colors, and blitOps needed
|
||||||
|
// for painting. This avoids threading the context through every
|
||||||
|
// widget function — any widget can retrieve it via wgtGetContext().
|
||||||
|
|
||||||
WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
|
WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
|
||||||
WidgetT *root = widgetAlloc(NULL, WidgetVBoxE);
|
WidgetT *root = widgetAlloc(NULL, WidgetVBoxE);
|
||||||
|
|
@ -279,6 +351,19 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtInvalidate
|
// wgtInvalidate
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Full invalidation: re-measures the widget tree, manages scrollbars,
|
||||||
|
// re-lays out, repaints, and dirties the window on screen.
|
||||||
|
//
|
||||||
|
// This is the "something structural changed" path — use when widget
|
||||||
|
// sizes may have changed (text changed, children added/removed,
|
||||||
|
// visibility toggled). If only visual state changed (cursor blink,
|
||||||
|
// selection highlight), use wgtInvalidatePaint() instead to skip
|
||||||
|
// the expensive measure/layout passes.
|
||||||
|
//
|
||||||
|
// The widgetOnPaint check ensures that custom paint handlers (used
|
||||||
|
// by some dialog implementations) aren't bypassed by the scrollbar
|
||||||
|
// management code.
|
||||||
|
|
||||||
void wgtInvalidate(WidgetT *w) {
|
void wgtInvalidate(WidgetT *w) {
|
||||||
if (!w || !w->window) {
|
if (!w || !w->window) {
|
||||||
|
|
@ -392,6 +477,10 @@ void wgtSetEnabled(WidgetT *w, bool enabled) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtSetText
|
// wgtSetText
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Polymorphic text setter. Dispatches to the type-specific setText
|
||||||
|
// via vtable, then does a full invalidation because changing text
|
||||||
|
// can change the widget's minimum size (triggering relayout).
|
||||||
|
|
||||||
void wgtSetText(WidgetT *w, const char *text) {
|
void wgtSetText(WidgetT *w, const char *text) {
|
||||||
if (!w) {
|
if (!w) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,18 @@
|
||||||
// widgetProgressBar.c — ProgressBar widget
|
// widgetProgressBar.c — ProgressBar widget
|
||||||
|
//
|
||||||
|
// A non-interactive fill-bar widget for displaying bounded progress.
|
||||||
|
// Supports both horizontal and vertical orientations via separate
|
||||||
|
// constructors rather than a runtime flag, because orientation is
|
||||||
|
// typically fixed at UI construction time and the two constructors
|
||||||
|
// read more clearly at the call site.
|
||||||
|
//
|
||||||
|
// Rendering: sunken 2px bevel border (inverted highlight/shadow to
|
||||||
|
// look recessed) with a solid fill rect proportional to value/maxValue.
|
||||||
|
// Uses integer division for fill size -- no floating point, which would
|
||||||
|
// be prohibitively expensive on a 486 without an FPU.
|
||||||
|
//
|
||||||
|
// The widget has no mouse or keyboard handlers -- it is purely display.
|
||||||
|
// Value is controlled externally through the set/get API.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -72,6 +86,10 @@ void wgtProgressBarSetValue(WidgetT *w, int32_t value) {
|
||||||
// widgetProgressBarCalcMinSize
|
// widgetProgressBarCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Min size is based on font metrics to keep the bar proportional to
|
||||||
|
// surrounding text. The cross-axis uses charHeight+4 (room for the
|
||||||
|
// bar plus 2px bevel on each side). The main axis uses 12 char widths
|
||||||
|
// so the bar is wide enough to show meaningful progress granularity.
|
||||||
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
if (w->as.progressBar.vertical) {
|
if (w->as.progressBar.vertical) {
|
||||||
w->calcMinW = font->charHeight + 4;
|
w->calcMinW = font->charHeight + 4;
|
||||||
|
|
@ -87,12 +105,21 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetProgressBarPaint
|
// widgetProgressBarPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint uses a two-pass approach: first the sunken bevel border, then
|
||||||
|
// the fill rect. The fill color defaults to activeTitleBg (the window
|
||||||
|
// title bar color) to provide strong visual contrast inside the
|
||||||
|
// recessed trough -- matching classic Win3.1/Motif progress bar style.
|
||||||
|
// When disabled, the fill becomes windowShadow to look grayed out.
|
||||||
|
//
|
||||||
|
// Vertical bars fill from bottom-up (natural "filling" metaphor),
|
||||||
|
// which requires computing the Y offset as (innerH - fillH).
|
||||||
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->activeTitleBg) : colors->windowShadow;
|
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->activeTitleBg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
// Sunken border
|
// Sunken border -- highlight/shadow are swapped vs raised bevel
|
||||||
|
// to create the "pressed in" trough appearance
|
||||||
BevelStyleT bevel;
|
BevelStyleT bevel;
|
||||||
bevel.highlight = colors->windowShadow;
|
bevel.highlight = colors->windowShadow;
|
||||||
bevel.shadow = colors->windowHighlight;
|
bevel.shadow = colors->windowHighlight;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,25 @@
|
||||||
// widgetRadio.c — RadioGroup and Radio button widgets
|
// widgetRadio.c — RadioGroup and Radio button widgets
|
||||||
|
//
|
||||||
|
// Two-level architecture: RadioGroupE is an invisible container that
|
||||||
|
// holds the selection state (selectedIdx), while RadioE children are
|
||||||
|
// the visible buttons. This separation means the group tracks which
|
||||||
|
// index is selected while each radio button only knows its own index,
|
||||||
|
// keeping the per-button state minimal.
|
||||||
|
//
|
||||||
|
// Selection state is stored as an integer index on the parent group
|
||||||
|
// rather than a pointer, because indices survive widget reordering
|
||||||
|
// and are trivially serializable. The index is auto-assigned at
|
||||||
|
// construction time based on sibling order.
|
||||||
|
//
|
||||||
|
// Rendering: diamond-shaped indicator (Motif-style) using scanline
|
||||||
|
// horizontal lines rather than a circle algorithm. This avoids any
|
||||||
|
// need for anti-aliasing or trigonometry -- purely integer H/V lines.
|
||||||
|
// The selection dot inside uses a hardcoded width table for a 6-row
|
||||||
|
// filled diamond, sized to look crisp at the fixed 12px box size.
|
||||||
|
//
|
||||||
|
// Keyboard: Space/Enter select the current radio. Arrow keys move
|
||||||
|
// focus AND selection together (standard radio group behavior --
|
||||||
|
// focus and selection are coupled, unlike checkboxes).
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -7,6 +28,9 @@
|
||||||
// wgtRadio
|
// wgtRadio
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Auto-index is computed by counting RadioE siblings before this one.
|
||||||
|
// This means radio buttons must be added in order and not reordered
|
||||||
|
// after construction -- which is the normal pattern for radio groups.
|
||||||
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetRadioE);
|
WidgetT *w = widgetAlloc(parent, WidgetRadioE);
|
||||||
|
|
||||||
|
|
@ -68,6 +92,9 @@ void widgetRadioSetText(WidgetT *w, const char *text) {
|
||||||
// widgetRadioCalcMinSize
|
// widgetRadioCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Shares CHECKBOX_BOX_SIZE/GAP constants with the checkbox widget
|
||||||
|
// so radio buttons and checkboxes align when placed in the same
|
||||||
|
// column, and the indicator + label layout is visually consistent.
|
||||||
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
||||||
textWidthAccel(font, w->as.radio.text);
|
textWidthAccel(font, w->as.radio.text);
|
||||||
|
|
@ -79,6 +106,15 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetRadioOnKey
|
// widgetRadioOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Arrow keys move focus AND selection simultaneously -- this is the
|
||||||
|
// standard radio group behavior where navigating with arrows also
|
||||||
|
// commits the selection. This differs from checkboxes where arrows
|
||||||
|
// just move focus and Space toggles. The coupling is deliberate:
|
||||||
|
// radio groups represent a single mutually-exclusive choice, so
|
||||||
|
// "looking at" an option means "choosing" it.
|
||||||
|
//
|
||||||
|
// Key codes use DOS BIOS scancode convention: high byte 0x01 flag
|
||||||
|
// ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left.
|
||||||
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
(void)mod;
|
(void)mod;
|
||||||
|
|
||||||
|
|
@ -170,6 +206,16 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetRadioPaint
|
// widgetRadioPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// The diamond radio indicator is rendered in three passes:
|
||||||
|
// 1. Interior fill -- scanline-based diamond fill for the background
|
||||||
|
// 2. Border -- upper-left edges in shadow, lower-right in highlight
|
||||||
|
// (sunken appearance, same lighting model as bevels)
|
||||||
|
// 3. Selection dot -- smaller filled diamond from a hardcoded 6-row
|
||||||
|
// width table. The static const avoids recomputation per paint.
|
||||||
|
//
|
||||||
|
// This approach avoids floating point entirely. Each scanline's
|
||||||
|
// left/right extent is computed with simple integer distance from
|
||||||
|
// the midpoint, making the diamond perfectly symmetric at any size.
|
||||||
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,31 @@
|
||||||
// widgetScrollPane.c — ScrollPane container widget
|
// widgetScrollPane.c — ScrollPane container widget
|
||||||
|
//
|
||||||
|
// A clipping container that allows its children (laid out as a
|
||||||
|
// vertical box) to overflow and be scrolled into view. This is the
|
||||||
|
// general-purpose scrollable container -- unlike TreeView or ListBox
|
||||||
|
// which have item-specific scrolling, ScrollPane can wrap any
|
||||||
|
// collection of arbitrary child widgets.
|
||||||
|
//
|
||||||
|
// Architecture: ScrollPane is both a layout container and a paint
|
||||||
|
// container (WCLASS_PAINTS_CHILDREN flag). It lays out children at
|
||||||
|
// their virtual positions (offset by scroll position), then during
|
||||||
|
// paint, sets a clip rect to the inner content area before painting
|
||||||
|
// children. This means children are positioned at coordinates that
|
||||||
|
// may be outside the visible area -- the clip rect handles hiding
|
||||||
|
// the overflow. This is simpler and more efficient than per-widget
|
||||||
|
// visibility culling for the small widget counts typical on 486 DOS.
|
||||||
|
//
|
||||||
|
// Scrollbar visibility uses a two-pass determination: first check if
|
||||||
|
// V scrollbar is needed, then check H (accounting for the space the
|
||||||
|
// V scrollbar consumed), then re-check V in case H scrollbar's
|
||||||
|
// appearance reduced available height. This handles the mutual
|
||||||
|
// dependency where adding one scrollbar may trigger the other.
|
||||||
|
//
|
||||||
|
// The scroll pane has its own copies of the scrollbar drawing routines
|
||||||
|
// (drawSPHScrollbar, drawSPVScrollbar) rather than using the shared
|
||||||
|
// widgetDrawScrollbarH/V because it uses its own SP_SB_W constant.
|
||||||
|
// This is a minor duplication tradeoff for allowing different scrollbar
|
||||||
|
// widths in different contexts.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -126,6 +153,12 @@ static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
|
||||||
// spCalcNeeds — determine scrollbar needs and inner dimensions
|
// spCalcNeeds — determine scrollbar needs and inner dimensions
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Central sizing function called by layout, paint, and mouse handlers.
|
||||||
|
// Computes the total content min size from children, then determines
|
||||||
|
// which scrollbars are needed and adjusts inner dimensions accordingly.
|
||||||
|
// The two-pass scrollbar dependency resolution handles the case where
|
||||||
|
// adding a V scrollbar shrinks the width enough to need an H scrollbar,
|
||||||
|
// which in turn shrinks the height enough to need a V scrollbar.
|
||||||
static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb) {
|
static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb) {
|
||||||
// Measure children
|
// Measure children
|
||||||
int32_t totalMinW = 0;
|
int32_t totalMinW = 0;
|
||||||
|
|
@ -190,6 +223,11 @@ static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMin
|
||||||
// widgetScrollPaneCalcMinSize
|
// widgetScrollPaneCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// The scroll pane reports a deliberately small min size (just enough
|
||||||
|
// for the scrollbar chrome) because its whole purpose is to contain
|
||||||
|
// content that doesn't fit. However, children still need their min
|
||||||
|
// sizes computed so spCalcNeeds can determine scrollbar visibility
|
||||||
|
// and the layout pass can distribute space correctly.
|
||||||
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// Recursively measure children so they have valid calcMinW/H
|
// Recursively measure children so they have valid calcMinW/H
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
|
@ -206,6 +244,18 @@ void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetScrollPaneLayout
|
// widgetScrollPaneLayout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Layout is a vertical box layout offset by the scroll position.
|
||||||
|
// Children are positioned at their "virtual" coordinates (baseX/baseY
|
||||||
|
// incorporate the negative scroll offset), so they may have negative
|
||||||
|
// or very large Y values. The paint pass clips to the visible area.
|
||||||
|
// This means child coordinates are always absolute screen coords,
|
||||||
|
// keeping the draw path simple -- no coordinate translation needed
|
||||||
|
// at paint time.
|
||||||
|
//
|
||||||
|
// Extra space distribution uses the same weight-based algorithm as
|
||||||
|
// the generic box layout: each child gets a share of surplus space
|
||||||
|
// proportional to its weight/totalWeight ratio. This allows stretch
|
||||||
|
// children inside the scrollable area.
|
||||||
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) {
|
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t contentMinW;
|
int32_t contentMinW;
|
||||||
int32_t contentMinH;
|
int32_t contentMinH;
|
||||||
|
|
@ -311,6 +361,11 @@ void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetScrollPaneOnKey
|
// widgetScrollPaneOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Keyboard scrolling uses font metrics for step sizes: charHeight for
|
||||||
|
// vertical line scroll, charWidth for horizontal, and the full inner
|
||||||
|
// height for page scroll. This makes scroll distance proportional to
|
||||||
|
// content size, which feels natural. The early return for unhandled
|
||||||
|
// keys avoids unnecessary invalidation.
|
||||||
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
(void)mod;
|
(void)mod;
|
||||||
|
|
||||||
|
|
@ -377,6 +432,15 @@ void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetScrollPaneOnMouse
|
// widgetScrollPaneOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling has priority order: V scrollbar > H scrollbar > dead
|
||||||
|
// corner > child content. The dead corner (where H and V scrollbars
|
||||||
|
// meet) is explicitly handled to prevent clicks from falling through
|
||||||
|
// to content behind it. Content clicks do recursive hit-testing into
|
||||||
|
// children and forward the mouse event, handling focus management
|
||||||
|
// along the way. This is necessary because scroll pane has
|
||||||
|
// WCLASS_NO_HIT_RECURSE -- the generic hit-test doesn't descend
|
||||||
|
// into scroll pane children since their coordinates may be outside
|
||||||
|
// the pane's visible bounds.
|
||||||
void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
|
|
@ -528,6 +592,12 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
// widgetScrollPanePaint
|
// widgetScrollPanePaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint saves and restores the clip rect around child painting.
|
||||||
|
// Children are painted with a clip rect that excludes the scrollbar
|
||||||
|
// area, so children that extend past the visible content area are
|
||||||
|
// automatically clipped. Scrollbars are painted after restoring the
|
||||||
|
// clip rect so they're always fully visible. The dead corner (when
|
||||||
|
// both scrollbars are present) is filled with windowFace color.
|
||||||
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
|
|
@ -604,6 +674,9 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
||||||
// wgtScrollPane
|
// wgtScrollPane
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Default weight=100 so the scroll pane stretches to fill available
|
||||||
|
// space in its parent container. Without this, a scroll pane in a
|
||||||
|
// vertical box would collapse to its minimal size.
|
||||||
WidgetT *wgtScrollPane(WidgetT *parent) {
|
WidgetT *wgtScrollPane(WidgetT *parent) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetScrollPaneE);
|
WidgetT *w = widgetAlloc(parent, WidgetScrollPaneE);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
// widgetScrollbar.c — Shared scrollbar painting and hit-testing
|
// widgetScrollbar.c — Shared scrollbar painting and hit-testing
|
||||||
|
//
|
||||||
|
// These are not widgets themselves -- they are stateless rendering and
|
||||||
|
// hit-testing utilities shared by ScrollPane, TreeView, TextArea,
|
||||||
|
// ListBox, and ListView. Each owning widget stores its own scroll
|
||||||
|
// position; these functions are purely geometric.
|
||||||
|
//
|
||||||
|
// The scrollbar model uses three parts: two arrow buttons (one at each
|
||||||
|
// end) and a proportional thumb in the track between them. Thumb size
|
||||||
|
// is proportional to (visibleSize / totalSize), clamped to SB_MIN_THUMB
|
||||||
|
// to remain grabbable even when content is very large. Thumb position
|
||||||
|
// maps linearly from scrollPos to track position.
|
||||||
|
//
|
||||||
|
// Arrow triangles are drawn with simple loop-based scanlines (4 rows),
|
||||||
|
// producing 7-pixel-wide arrow glyphs. This avoids any font or bitmap
|
||||||
|
// dependency for the scrollbar chrome.
|
||||||
|
//
|
||||||
|
// The minimum scrollbar length guard (sbW < WGT_SB_W * 3) ensures
|
||||||
|
// there is at least room for both arrow buttons plus a minimal track.
|
||||||
|
// If the container is too small, the scrollbar is simply not drawn
|
||||||
|
// rather than rendering a corrupted mess.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -119,6 +139,12 @@ void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
|
||||||
// widgetScrollbarHitTest
|
// widgetScrollbarHitTest
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Axis-agnostic hit test. The caller converts (vx,vy) into a 1D
|
||||||
|
// position along the scrollbar axis (relPos) and the scrollbar
|
||||||
|
// length (sbLen). Returns which zone was hit: arrow buttons,
|
||||||
|
// page-up/page-down trough regions, or the thumb itself.
|
||||||
|
// This factoring lets all scrollbar-owning widgets share the same
|
||||||
|
// logic without duplicating per-axis code.
|
||||||
ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
|
ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
|
||||||
if (relPos < WGT_SB_W) {
|
if (relPos < WGT_SB_W) {
|
||||||
return ScrollHitArrowDecE;
|
return ScrollHitArrowDecE;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,21 @@
|
||||||
// widgetSeparator.c — Separator widget (horizontal and vertical)
|
// widgetSeparator.c — Separator widget (horizontal and vertical)
|
||||||
|
//
|
||||||
|
// A purely decorative widget that draws a 2px etched line (shadow +
|
||||||
|
// highlight pair) to visually divide groups of widgets. The etched
|
||||||
|
// line technique (dark line followed by light line offset by 1px)
|
||||||
|
// creates a subtle 3D groove that matches the Motif/Win3.1 aesthetic.
|
||||||
|
//
|
||||||
|
// Separate constructors (wgtHSeparator/wgtVSeparator) rather than a
|
||||||
|
// runtime orientation parameter because separators are always fixed
|
||||||
|
// orientation at construction time.
|
||||||
|
//
|
||||||
|
// The min size in the cross-axis is SEPARATOR_THICKNESS (2px), while
|
||||||
|
// the main axis min is 0 -- the separator stretches to fill the
|
||||||
|
// available width/height in its parent layout. This makes separators
|
||||||
|
// work correctly in both HBox and VBox containers without explicit
|
||||||
|
// sizing.
|
||||||
|
//
|
||||||
|
// No mouse/key handlers -- purely decorative, non-focusable.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
// widgetSlider.c — Slider (trackbar) widget
|
// widgetSlider.c — Slider (trackbar) widget
|
||||||
|
//
|
||||||
|
// A continuous-value selector with a draggable thumb on a groove track.
|
||||||
|
// Supports both horizontal and vertical orientation. The value maps
|
||||||
|
// linearly to thumb position using integer arithmetic only.
|
||||||
|
//
|
||||||
|
// Rendering: thin sunken groove for the track, raised 2px-bevel thumb,
|
||||||
|
// and a center tick line on the thumb for grip feedback. The groove
|
||||||
|
// uses reversed highlight/shadow (same as progress bar trough) for
|
||||||
|
// the recessed look. The thumb uses the standard raised bevel.
|
||||||
|
//
|
||||||
|
// Interaction model: clicking on the track jumps the thumb to that
|
||||||
|
// position (instant seek), while clicking on the thumb starts a drag
|
||||||
|
// via the global sDragSlider/sDragOffset state. The drag offset stores
|
||||||
|
// where within the thumb the user grabbed, so the thumb doesn't snap
|
||||||
|
// to the cursor center during drag -- this feels much more natural.
|
||||||
|
//
|
||||||
|
// Keyboard: arrow keys increment/decrement by a computed step that
|
||||||
|
// scales with range (step = range/100 for ranges > 100, else 1).
|
||||||
|
// Home/End jump to min/max. This gives ~100 keyboard steps regardless
|
||||||
|
// of the actual value range.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -7,6 +27,7 @@
|
||||||
// wgtSlider
|
// wgtSlider
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Default weight=100 so the slider stretches in its parent layout.
|
||||||
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) {
|
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetSliderE);
|
WidgetT *w = widgetAlloc(parent, WidgetSliderE);
|
||||||
|
|
||||||
|
|
@ -57,6 +78,9 @@ void wgtSliderSetValue(WidgetT *w, int32_t value) {
|
||||||
// widgetSliderCalcMinSize
|
// widgetSliderCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Min size: 5 thumb-widths along the main axis gives enough room for
|
||||||
|
// the thumb to travel meaningfully. Cross-axis is thumb width + 4px
|
||||||
|
// margin so the groove has visual breathing room around the thumb.
|
||||||
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
(void)font;
|
(void)font;
|
||||||
|
|
||||||
|
|
@ -124,6 +148,15 @@ void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetSliderOnMouse
|
// widgetSliderOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse click distinguishes between thumb hit (start drag) and track
|
||||||
|
// hit (jump to position). The thumb hit area is the full SLIDER_THUMB_W
|
||||||
|
// pixel range at the current thumb position. The jump-to-position
|
||||||
|
// calculation centers the thumb at the click point by subtracting
|
||||||
|
// half the thumb width before converting pixel position to value.
|
||||||
|
//
|
||||||
|
// Drag state is stored in globals (sDragSlider, sDragOffset) rather
|
||||||
|
// than per-widget because only one slider can be dragged at a time.
|
||||||
|
// The event loop checks sDragSlider on mouse-move to continue the drag.
|
||||||
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
(void)root;
|
(void)root;
|
||||||
hit->focused = true;
|
hit->focused = true;
|
||||||
|
|
@ -189,6 +222,14 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetSliderPaint
|
// widgetSliderPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint: groove track centered in the cross-axis, thumb at value
|
||||||
|
// position. The thumb position formula is:
|
||||||
|
// thumbPos = ((value - minValue) * thumbRange) / range
|
||||||
|
// where thumbRange = widgetSize - SLIDER_THUMB_W.
|
||||||
|
// This maps value linearly to pixel position using integer math only.
|
||||||
|
// The groove is thin (SLIDER_TRACK_H = 4px) with a reversed bevel
|
||||||
|
// for the recessed look, while the thumb is the full widget height
|
||||||
|
// with a raised bevel for the 3D grab handle appearance.
|
||||||
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
// widgetSpacer.c — Spacer widget (invisible stretching element)
|
// widgetSpacer.c — Spacer widget (invisible stretching element)
|
||||||
|
//
|
||||||
|
// A zero-sized invisible widget with weight=100, used purely for
|
||||||
|
// layout control. It absorbs leftover space in box layouts, pushing
|
||||||
|
// sibling widgets apart. Common use: place a spacer between buttons
|
||||||
|
// in a toolbar to right-align some buttons, or between a label and
|
||||||
|
// a control to create elastic spacing.
|
||||||
|
//
|
||||||
|
// Because calcMinSize returns 0x0, the spacer takes no space when
|
||||||
|
// there is none to spare, but greedily absorbs extra space via its
|
||||||
|
// weight. This is the simplest possible layout primitive -- no paint,
|
||||||
|
// no mouse, no keyboard, no state. The entire widget is effectively
|
||||||
|
// just a weight value attached to a position in the sibling list.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,33 @@
|
||||||
// widgetSpinner.c — Spinner (numeric up/down) widget
|
// widgetSpinner.c — Spinner (numeric up/down) widget
|
||||||
|
//
|
||||||
|
// A hybrid widget combining a single-line text editor with up/down
|
||||||
|
// arrow buttons for numeric value entry. The user can either click
|
||||||
|
// the arrows, use Up/Down keys, or type a number directly.
|
||||||
|
//
|
||||||
|
// Design: the widget has two modes -- display mode (showing the
|
||||||
|
// formatted value) and edit mode (allowing free-form text input).
|
||||||
|
// Edit mode is entered on the first text-modifying keystroke and
|
||||||
|
// committed on Enter or when arrows are clicked. Escape cancels
|
||||||
|
// the edit and reverts to the pre-edit value. This two-mode design
|
||||||
|
// keeps the display clean (always showing a properly formatted
|
||||||
|
// number) while still allowing direct keyboard entry.
|
||||||
|
//
|
||||||
|
// The text editing delegates to widgetTextEditOnKey() -- the same
|
||||||
|
// shared single-line editing logic used by TextInput. This gives
|
||||||
|
// the spinner cursor movement, selection, cut/copy/paste, and undo
|
||||||
|
// for free. Input validation filters non-digit characters before
|
||||||
|
// they reach the editor, and only allows minus at position 0.
|
||||||
|
//
|
||||||
|
// Undo uses a single-level swap buffer (same as TextInput): the
|
||||||
|
// current state is copied to undoBuf before each mutation, and
|
||||||
|
// Ctrl+Z swaps current<->undo. This is simpler and cheaper than
|
||||||
|
// a multi-level undo stack for the small buffers involved.
|
||||||
|
//
|
||||||
|
// Rendering: sunken border enclosing the text area + two stacked
|
||||||
|
// raised-bevel arrow buttons on the right. The buttons extend to
|
||||||
|
// the widget's right edge (including the border width) so they
|
||||||
|
// look like they're part of the border chrome. The up/down buttons
|
||||||
|
// split the widget height evenly.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -59,6 +88,10 @@ static void spinnerCommitEdit(WidgetT *w) {
|
||||||
// spinnerFormat
|
// spinnerFormat
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Format always places the cursor at the end and resets scroll/selection.
|
||||||
|
// This is called after any value change to synchronize the text buffer
|
||||||
|
// with the numeric value. The cursor-at-end position matches user
|
||||||
|
// expectation after arrow-key increment/decrement.
|
||||||
static void spinnerFormat(WidgetT *w) {
|
static void spinnerFormat(WidgetT *w) {
|
||||||
w->as.spinner.len = snprintf(w->as.spinner.buf, sizeof(w->as.spinner.buf), "%d", (int)w->as.spinner.value);
|
w->as.spinner.len = snprintf(w->as.spinner.buf, sizeof(w->as.spinner.buf), "%d", (int)w->as.spinner.value);
|
||||||
w->as.spinner.cursorPos = w->as.spinner.len;
|
w->as.spinner.cursorPos = w->as.spinner.len;
|
||||||
|
|
@ -72,6 +105,10 @@ static void spinnerFormat(WidgetT *w) {
|
||||||
// spinnerStartEdit
|
// spinnerStartEdit
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Entering edit mode snapshots the buffer for undo so the user can
|
||||||
|
// revert to the pre-edit formatted value. The snapshot is only taken
|
||||||
|
// on the transition to editing, not on every keystroke, so repeated
|
||||||
|
// typing within one edit session can be undone all at once.
|
||||||
static void spinnerStartEdit(WidgetT *w) {
|
static void spinnerStartEdit(WidgetT *w) {
|
||||||
if (!w->as.spinner.editing) {
|
if (!w->as.spinner.editing) {
|
||||||
w->as.spinner.editing = true;
|
w->as.spinner.editing = true;
|
||||||
|
|
@ -107,6 +144,14 @@ const char *widgetSpinnerGetText(const WidgetT *w) {
|
||||||
// widgetSpinnerOnKey
|
// widgetSpinnerOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Key handling has two distinct paths: navigation keys (Up/Down/PgUp/
|
||||||
|
// PgDn) always commit any pending edit first, then adjust the numeric
|
||||||
|
// value directly. Text keys enter edit mode and are forwarded to the
|
||||||
|
// shared text editor. This split ensures arrow-key nudging always
|
||||||
|
// operates on the committed value, not on partially typed text.
|
||||||
|
//
|
||||||
|
// Page Up/Down use step*10 for coarser adjustment, matching the
|
||||||
|
// convention used by Windows spin controls.
|
||||||
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
int32_t step = w->as.spinner.step;
|
int32_t step = w->as.spinner.step;
|
||||||
|
|
||||||
|
|
@ -229,6 +274,14 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetSpinnerOnMouse
|
// widgetSpinnerOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse click regions: button area (right side) vs text area (left side).
|
||||||
|
// Button area is split vertically at the midpoint -- top half increments,
|
||||||
|
// bottom half decrements. Clicking a button commits any pending edit
|
||||||
|
// before adjusting the value, same as arrow keys.
|
||||||
|
//
|
||||||
|
// Text area clicks compute cursor position from pixel offset using the
|
||||||
|
// fixed-width font. Double-click selects all text (select-word doesn't
|
||||||
|
// make sense for numbers), entering edit mode to allow replacement.
|
||||||
void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->focused = true;
|
hit->focused = true;
|
||||||
|
|
||||||
|
|
@ -291,6 +344,17 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetSpinnerPaint
|
// widgetSpinnerPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint uses the same 3-run text rendering approach as TextInput:
|
||||||
|
// before-selection, selection (highlighted), after-selection. This
|
||||||
|
// avoids overdraw and gives correct selection highlighting with only
|
||||||
|
// one pass over the visible text. The scroll offset ensures the
|
||||||
|
// cursor is always visible even when the number is wider than the
|
||||||
|
// text area.
|
||||||
|
//
|
||||||
|
// The two buttons (up/down) extend SPINNER_BORDER pixels past the
|
||||||
|
// button area into the widget's right border so they visually merge
|
||||||
|
// with the outer bevel -- this is why btnW is btnW + SPINNER_BORDER
|
||||||
|
// in the drawBevel calls.
|
||||||
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetSpinnerPaint(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 fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,31 @@
|
||||||
// widgetSplitter.c — Splitter (draggable divider between two panes)
|
// widgetSplitter.c — Splitter (draggable divider between two panes)
|
||||||
|
//
|
||||||
|
// A container that divides its area between exactly two child widgets
|
||||||
|
// with a draggable divider bar between them. The divider position
|
||||||
|
// (dividerPos) is stored as a pixel offset from the leading edge
|
||||||
|
// (left for vertical, top for horizontal).
|
||||||
|
//
|
||||||
|
// Architecture: the splitter is a special container (has its own
|
||||||
|
// layout function) that manually positions its two children and the
|
||||||
|
// divider bar. It does NOT use the generic box layout. Children are
|
||||||
|
// clipped to their respective panes during painting to prevent
|
||||||
|
// overflow into the other pane.
|
||||||
|
//
|
||||||
|
// The divider bar (SPLITTER_BAR_W = 5px) is drawn as a raised bevel
|
||||||
|
// with a "gripper" pattern -- small embossed 2x2 bumps arranged
|
||||||
|
// in a line centered on the bar. This provides visual feedback that
|
||||||
|
// the bar is draggable, following the Win3.1/Motif convention.
|
||||||
|
//
|
||||||
|
// Drag state: divider dragging stores the clicked splitter widget
|
||||||
|
// and the mouse offset within the bar in globals (sDragSplitter,
|
||||||
|
// sDragSplitStart). The event loop handles mouse-move during drag
|
||||||
|
// by computing the new divider position from mouse coordinates and
|
||||||
|
// calling widgetSplitterClampPos to enforce minimum pane sizes.
|
||||||
|
//
|
||||||
|
// Minimum pane sizes come from children's calcMinW/H, with a floor
|
||||||
|
// of SPLITTER_MIN_PANE (20px) to prevent panes from collapsing to
|
||||||
|
// nothing. The clamp also ensures the divider can't be dragged past
|
||||||
|
// where the second pane would violate its minimum.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -15,6 +42,10 @@ static WidgetT *spSecondChild(WidgetT *w);
|
||||||
// spFirstChild — get first visible child
|
// spFirstChild — get first visible child
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// These helpers skip invisible children so the splitter works
|
||||||
|
// correctly even if one pane is hidden. The splitter only cares
|
||||||
|
// about the first two visible children; any additional children
|
||||||
|
// are ignored (though this shouldn't happen in practice).
|
||||||
static WidgetT *spFirstChild(WidgetT *w) {
|
static WidgetT *spFirstChild(WidgetT *w) {
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
if (c->visible) {
|
if (c->visible) {
|
||||||
|
|
@ -109,6 +140,12 @@ void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetSplitterLayout
|
// widgetSplitterLayout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Layout assigns each child its full pane area, then recurses into
|
||||||
|
// child layouts. The first child gets [0, dividerPos) and the second
|
||||||
|
// gets [dividerPos + SPLITTER_BAR_W, end). The divider position is
|
||||||
|
// clamped before use to respect minimum pane sizes, even if the
|
||||||
|
// user hasn't dragged yet (dividerPos=0 from construction gets
|
||||||
|
// clamped to minFirst).
|
||||||
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
|
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
WidgetT *c1 = spFirstChild(w);
|
WidgetT *c1 = spFirstChild(w);
|
||||||
WidgetT *c2 = spSecondChild(w);
|
WidgetT *c2 = spSecondChild(w);
|
||||||
|
|
@ -171,6 +208,12 @@ void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetSplitterOnMouse
|
// widgetSplitterOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling first checks if the click is on the divider bar.
|
||||||
|
// If so, it starts a drag. If not, it recursively hit-tests into
|
||||||
|
// children and forwards the event. This manual hit-test forwarding
|
||||||
|
// is needed because the splitter has WCLASS_NO_HIT_RECURSE (the
|
||||||
|
// generic hit-test would find the splitter but not recurse into its
|
||||||
|
// clipped children).
|
||||||
void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
int32_t pos = hit->as.splitter.dividerPos;
|
int32_t pos = hit->as.splitter.dividerPos;
|
||||||
|
|
||||||
|
|
@ -229,6 +272,16 @@ void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
// widgetSplitterPaint
|
// widgetSplitterPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint clips each child to its pane area, preventing overflow across
|
||||||
|
// the divider. The clip rect is saved/restored around each child's
|
||||||
|
// paint. The divider bar is painted last (on top) so it's always
|
||||||
|
// visible even if a child overflows.
|
||||||
|
//
|
||||||
|
// The gripper bumps use the classic embossed-dot technique: a
|
||||||
|
// highlight pixel at (x,y) and a shadow pixel at (x+1,y+1) create
|
||||||
|
// a tiny raised bump. 11 bumps spaced 3px apart center vertically
|
||||||
|
// (or horizontally) on the bar. This is purely decorative but
|
||||||
|
// provides the expected visual affordance for "draggable".
|
||||||
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
int32_t pos = w->as.splitter.dividerPos;
|
int32_t pos = w->as.splitter.dividerPos;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
// widgetStatusBar.c — StatusBar widget
|
// widgetStatusBar.c — StatusBar widget
|
||||||
|
//
|
||||||
|
// A horizontal container that draws a sunken border around each visible
|
||||||
|
// child to create the classic segmented status bar appearance. Children
|
||||||
|
// are typically labels or other simple widgets, laid out by the generic
|
||||||
|
// horizontal box layout. The status bar itself has no special layout
|
||||||
|
// logic -- it's a standard HBox with tight padding/spacing and the
|
||||||
|
// extra per-child sunken border decorations.
|
||||||
|
//
|
||||||
|
// The border drawing is done as an overlay on top of children rather
|
||||||
|
// than as part of the child's own paint, because it wraps around
|
||||||
|
// the child's allocated area (extending 1px past each edge). This
|
||||||
|
// keeps the sunken-panel effect consistent regardless of the child
|
||||||
|
// widget type.
|
||||||
|
//
|
||||||
|
// No mouse/key handlers -- the status bar is purely display. Children
|
||||||
|
// that are interactive (e.g., a clickable label) handle their own events.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -7,6 +23,8 @@
|
||||||
// wgtStatusBar
|
// wgtStatusBar
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Tight 2px padding and spacing keeps the status bar compact, using
|
||||||
|
// minimal vertical space at the bottom of a window.
|
||||||
WidgetT *wgtStatusBar(WidgetT *parent) {
|
WidgetT *wgtStatusBar(WidgetT *parent) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetStatusBarE);
|
WidgetT *w = widgetAlloc(parent, WidgetStatusBarE);
|
||||||
|
|
||||||
|
|
@ -23,6 +41,9 @@ WidgetT *wgtStatusBar(WidgetT *parent) {
|
||||||
// widgetStatusBarPaint
|
// widgetStatusBarPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Draws a 1px sunken bevel (reversed highlight/shadow) around each
|
||||||
|
// child. The bevel.face=0 with width=1 means only the border lines
|
||||||
|
// are drawn, not a filled interior -- the child paints its own content.
|
||||||
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,34 @@
|
||||||
// widgetTabControl.c — TabControl and TabPage widgets
|
// widgetTabControl.c — TabControl and TabPage widgets
|
||||||
|
//
|
||||||
|
// Two-level architecture: TabControlE is the container holding
|
||||||
|
// selection state and rendering the tab header strip. TabPageE
|
||||||
|
// children act as invisible sub-containers, each holding the content
|
||||||
|
// widgets for that page. Only the active page's children are visible
|
||||||
|
// and receive layout.
|
||||||
|
//
|
||||||
|
// Tab header rendering: each tab is a manually-drawn chrome piece
|
||||||
|
// (not a button widget) with top/left/right edges in highlight/shadow.
|
||||||
|
// The active tab is 2px taller than inactive tabs, extending down to
|
||||||
|
// overlap the content panel's top border -- this creates the classic
|
||||||
|
// "folder tab" illusion where the active tab appears connected to the
|
||||||
|
// panel below it. The panel's top border is erased under the active
|
||||||
|
// tab to complete the effect.
|
||||||
|
//
|
||||||
|
// Scrolling tab headers: when the total tab header width exceeds the
|
||||||
|
// available space, left/right arrow buttons appear and the header area
|
||||||
|
// becomes a clipped scrolling region. The scrollOffset tracks how many
|
||||||
|
// pixels the tab strip has scrolled. tabEnsureVisible() auto-scrolls
|
||||||
|
// to keep the active tab visible after keyboard navigation.
|
||||||
|
//
|
||||||
|
// Tab switching closes any open dropdown/combobox popup before
|
||||||
|
// switching, because the popup's owning widget may be on the
|
||||||
|
// now-hidden page and would become orphaned visually.
|
||||||
|
//
|
||||||
|
// Layout: all tab pages are positioned at the same content area
|
||||||
|
// coordinates, but only the active page has visible=true. This means
|
||||||
|
// widgetLayoutChildren is only called for the active page, saving
|
||||||
|
// layout computation for hidden pages. When switching tabs, the old
|
||||||
|
// page becomes invisible and the new page becomes visible + relaid out.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -175,6 +205,10 @@ WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
|
||||||
// widgetTabControlCalcMinSize
|
// widgetTabControlCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Min size: tab header height + the maximum min size across ALL pages
|
||||||
|
// (not just the active one). This ensures the tab control reserves
|
||||||
|
// enough space for the largest page, preventing resize flicker when
|
||||||
|
// switching tabs. Children are recursively measured.
|
||||||
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||||
int32_t maxPageW = 0;
|
int32_t maxPageW = 0;
|
||||||
|
|
@ -239,6 +273,10 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetTabControlOnKey
|
// widgetTabControlOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Keyboard navigation: Left/Right cycle through tabs with wrapping
|
||||||
|
// (modular arithmetic). Home/End jump to first/last tab. The tab
|
||||||
|
// control only handles these keys when it has focus -- if a child
|
||||||
|
// widget inside the active page has focus, keys go there instead.
|
||||||
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
(void)mod;
|
(void)mod;
|
||||||
|
|
||||||
|
|
@ -285,6 +323,11 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetTabControlOnMouse
|
// widgetTabControlOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse clicks in the tab header area walk the tab list computing
|
||||||
|
// accumulated X positions to find which tab was clicked. Only clicks
|
||||||
|
// in the header strip (top tabH pixels) are handled here -- clicks
|
||||||
|
// on the content area go through normal child hit-testing. Scroll
|
||||||
|
// arrow clicks adjust scrollOffset by 4 character widths at a time.
|
||||||
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->focused = true;
|
hit->focused = true;
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
|
|
@ -360,6 +403,21 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
// widgetTabControlPaint
|
// widgetTabControlPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint order: content panel first (raised bevel below the tab strip),
|
||||||
|
// then scroll arrows if needed, then tab headers in a clipped region,
|
||||||
|
// then the active page's children. Tab headers are painted with a clip
|
||||||
|
// rect so partially-scrolled tabs at the edges are cleanly truncated.
|
||||||
|
//
|
||||||
|
// The active tab is drawn 2px taller (extending from w->y instead of
|
||||||
|
// w->y+2) and erases the panel's top border beneath it (2px of
|
||||||
|
// contentBg), creating the visual connection between tab and panel.
|
||||||
|
// Inactive tabs sit 2px lower and draw a bottom border to separate
|
||||||
|
// them from the panel.
|
||||||
|
//
|
||||||
|
// Only the active page's children are painted (WCLASS_PAINTS_CHILDREN
|
||||||
|
// flag means the generic paint won't descend into tab control children).
|
||||||
|
// This is critical for performance on 486 -- we skip painting all
|
||||||
|
// hidden pages entirely.
|
||||||
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||||
bool scroll = tabNeedScroll(w, font);
|
bool scroll = tabNeedScroll(w, font);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,63 @@
|
||||||
// widgetTextInput.c — TextInput and TextArea widgets
|
// widgetTextInput.c — TextInput and TextArea widgets
|
||||||
|
//
|
||||||
|
// This file implements three text editing widgets plus shared
|
||||||
|
// infrastructure:
|
||||||
|
//
|
||||||
|
// 1. TextInput — single-line text field with scroll, selection,
|
||||||
|
// undo, password masking, and masked input (e.g., phone/SSN).
|
||||||
|
// 2. TextArea — multi-line text editor with row/col cursor,
|
||||||
|
// dual-axis scrolling, and full selection/clipboard support.
|
||||||
|
// 3. Shared infrastructure — clipboard, multi-click detection,
|
||||||
|
// word boundary logic, cross-widget selection clearing, and
|
||||||
|
// the single-line text editing engine (widgetTextEditOnKey).
|
||||||
|
//
|
||||||
|
// All text editing is done in-place in fixed-size char buffers
|
||||||
|
// allocated at widget creation. No dynamic resizing -- this keeps
|
||||||
|
// memory management simple and predictable on DOS where heap
|
||||||
|
// fragmentation is a real concern.
|
||||||
|
//
|
||||||
|
// The single-line editing engine (widgetTextEditOnKey) is factored
|
||||||
|
// out as a separate function that takes buffer/cursor/selection
|
||||||
|
// pointers as parameters, so it can be shared between TextInput,
|
||||||
|
// Spinner, and ComboBox without code duplication. Each widget
|
||||||
|
// passes its own state fields.
|
||||||
|
//
|
||||||
|
// Undo is single-level swap: before each mutation, the current
|
||||||
|
// buffer is copied to undoBuf. Ctrl+Z swaps current<->undo, so
|
||||||
|
// a second Ctrl+Z is "redo". This is simpler than a multi-level
|
||||||
|
// undo stack and sufficient for typical DOS text entry.
|
||||||
|
//
|
||||||
|
// Selection model: selStart/selEnd (single-line) or selAnchor/
|
||||||
|
// selCursor (multi-line) form a directed range. selStart is where
|
||||||
|
// the user began selecting, selEnd is where they stopped. The
|
||||||
|
// "low" end for deletion/copy is always min(start, end). The
|
||||||
|
// -1 sentinel means "no selection".
|
||||||
|
//
|
||||||
|
// Clipboard is a simple static buffer (4KB). This is a process-wide
|
||||||
|
// clipboard, not per-widget and not OS-integrated (DOS has no
|
||||||
|
// clipboard API). Text cut/copied from any widget is available to
|
||||||
|
// paste in any other widget.
|
||||||
|
//
|
||||||
|
// Multi-click detection uses clock() timestamps with a 500ms
|
||||||
|
// threshold and 4px spatial tolerance. Double-click selects word,
|
||||||
|
// triple-click selects line (TextArea) or all (TextInput).
|
||||||
|
//
|
||||||
|
// Cross-widget selection clearing: when a widget gains selection,
|
||||||
|
// clearOtherSelections() deselects any other widget that had an
|
||||||
|
// active selection. This prevents the confusing visual state of
|
||||||
|
// multiple selected ranges across different widgets. The tracking
|
||||||
|
// uses sLastSelectedWidget to achieve O(1) clearing rather than
|
||||||
|
// walking the entire widget tree.
|
||||||
|
//
|
||||||
|
// Masked input: a special TextInput mode where the buffer is
|
||||||
|
// pre-filled from a mask pattern (e.g., "###-##-####" for SSN).
|
||||||
|
// '#' accepts digits, 'A' accepts letters, '*' accepts any
|
||||||
|
// printable char. Literal characters in the mask are fixed and the
|
||||||
|
// cursor skips over them. This provides constrained input without
|
||||||
|
// needing a separate widget type.
|
||||||
|
//
|
||||||
|
// Password mode: renders bullets instead of characters (CP437 0xF9)
|
||||||
|
// and blocks copy/cut operations for security.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -126,6 +185,9 @@ int32_t wordEnd(const char *buf, int32_t len, int32_t pos) {
|
||||||
|
|
||||||
// Track the widget that last had an active selection so we can
|
// Track the widget that last had an active selection so we can
|
||||||
// clear it in O(1) instead of walking every widget in every window.
|
// clear it in O(1) instead of walking every widget in every window.
|
||||||
|
// This is critical for performance on 486 -- a brute-force walk
|
||||||
|
// across all widgets in all windows on every focus change would be
|
||||||
|
// prohibitive.
|
||||||
static WidgetT *sLastSelectedWidget = NULL;
|
static WidgetT *sLastSelectedWidget = NULL;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -181,6 +243,12 @@ static bool clearSelectionOnWidget(WidgetT *w) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Clears selection on the previously-selected widget (if different
|
||||||
|
// from the newly-focused one). Validates that the previous widget's
|
||||||
|
// window is still in the window stack before touching it -- the
|
||||||
|
// window may have been closed since sLastSelectedWidget was set.
|
||||||
|
// If the previous widget was in a different window, that window
|
||||||
|
// gets a full repaint to clear the stale selection highlight.
|
||||||
void clearOtherSelections(WidgetT *except) {
|
void clearOtherSelections(WidgetT *except) {
|
||||||
if (!except || !except->window || !except->window->widgetRoot) {
|
if (!except || !except->window || !except->window->widgetRoot) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -330,6 +398,11 @@ static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
|
||||||
// wgtTextArea
|
// wgtTextArea
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// TextArea allocates heap buffers for content and undo. maxLen controls
|
||||||
|
// the buffer size; 0 defaults to 256 bytes. The undo buffer is the
|
||||||
|
// same size as the content buffer to support full-buffer undo. Line
|
||||||
|
// count and max line length are cached (invalidated by textAreaDirtyCache)
|
||||||
|
// to avoid re-scanning the entire buffer on every paint/layout call.
|
||||||
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetTextAreaE);
|
WidgetT *w = widgetAlloc(parent, WidgetTextAreaE);
|
||||||
|
|
||||||
|
|
@ -364,6 +437,10 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
||||||
// wgtTextInput
|
// wgtTextInput
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// TextInput also heap-allocates buffers. The weight=100 default makes
|
||||||
|
// text inputs stretch to fill available width, which is the expected
|
||||||
|
// behavior in form layouts. Selection uses a start/end pair rather
|
||||||
|
// than anchor/cursor -- for single-line, the concepts are equivalent.
|
||||||
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) {
|
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) {
|
||||||
WidgetT *w = widgetAlloc(parent, WidgetTextInputE);
|
WidgetT *w = widgetAlloc(parent, WidgetTextInputE);
|
||||||
|
|
||||||
|
|
@ -397,6 +474,11 @@ WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Masked input pre-fills the buffer from the mask pattern: literal
|
||||||
|
// characters are placed directly, editable slots get '_' placeholder.
|
||||||
|
// The cursor starts at the first editable slot. The mask string is
|
||||||
|
// NOT copied -- it must remain valid for the widget's lifetime (caller
|
||||||
|
// typically passes a string literal).
|
||||||
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask) {
|
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask) {
|
||||||
if (!mask) {
|
if (!mask) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -483,6 +565,14 @@ static int32_t maskPrevSlot(const char *mask, int32_t pos) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Masked input key handling is entirely separate from the shared
|
||||||
|
// text editor because the editing semantics are fundamentally different:
|
||||||
|
// the buffer length is fixed (it's always maskLen), characters can
|
||||||
|
// only be placed in slot positions, and backspace clears a slot rather
|
||||||
|
// than removing a character. The cursor skips over literal characters
|
||||||
|
// when moving left/right. Cut clears slots to '_' rather than removing
|
||||||
|
// text. Paste fills consecutive slots, skipping non-matching clipboard
|
||||||
|
// characters.
|
||||||
static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
char *buf = w->as.textInput.buf;
|
char *buf = w->as.textInput.buf;
|
||||||
const char *mask = w->as.textInput.mask;
|
const char *mask = w->as.textInput.mask;
|
||||||
|
|
@ -787,6 +877,10 @@ static int32_t textAreaCountLines(const char *buf, int32_t len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Cached line count -- sentinel value -1 means "dirty, recompute".
|
||||||
|
// This avoids O(N) buffer scans on every paint frame. The cache is
|
||||||
|
// invalidated (set to -1) by textAreaDirtyCache() after any buffer
|
||||||
|
// mutation. The same pattern is used for max line length.
|
||||||
static int32_t textAreaGetLineCount(WidgetT *w) {
|
static int32_t textAreaGetLineCount(WidgetT *w) {
|
||||||
if (w->as.textArea.cachedLines < 0) {
|
if (w->as.textArea.cachedLines < 0) {
|
||||||
w->as.textArea.cachedLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len);
|
w->as.textArea.cachedLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len);
|
||||||
|
|
@ -946,6 +1040,23 @@ const char *widgetTextAreaGetText(const WidgetT *w) {
|
||||||
// widgetTextAreaOnKey
|
// widgetTextAreaOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// TextArea key handling is inline (not using widgetTextEditOnKey)
|
||||||
|
// because multi-line editing has fundamentally different cursor
|
||||||
|
// semantics: row/col instead of linear offset, desiredCol for
|
||||||
|
// vertical movement (so moving down from a long line to a short
|
||||||
|
// line remembers the original column), and Enter inserts newlines.
|
||||||
|
//
|
||||||
|
// The SEL_BEGIN/SEL_END/HAS_SEL macros factor out the repetitive
|
||||||
|
// selection-start/selection-extend pattern: SEL_BEGIN initializes
|
||||||
|
// the anchor at the current offset if Shift is held and no selection
|
||||||
|
// exists yet. SEL_END updates the selection cursor to the new
|
||||||
|
// position (or clears selection if Shift isn't held). This keeps
|
||||||
|
// the per-key handler code manageable despite the large number of
|
||||||
|
// key combinations.
|
||||||
|
//
|
||||||
|
// textAreaEnsureVisible() is called after every cursor movement to
|
||||||
|
// auto-scroll the viewport. It adjusts scrollRow/scrollCol so the
|
||||||
|
// cursor is within the visible range.
|
||||||
void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
if (!w->as.textArea.buf) {
|
if (!w->as.textArea.buf) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1473,6 +1584,14 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetTextAreaOnMouse
|
// widgetTextAreaOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling: scrollbar clicks (both V and H), then content area
|
||||||
|
// clicks. Content clicks convert pixel coordinates to row/col using
|
||||||
|
// font metrics and scroll offset. Multi-click: double-click selects
|
||||||
|
// word, triple-click selects entire line. Single click starts a
|
||||||
|
// drag-select (sets sDragTextSelect which the event loop monitors
|
||||||
|
// on mouse-move to extend the selection). The drag-select global
|
||||||
|
// is cleared on double/triple click since the selection is already
|
||||||
|
// complete.
|
||||||
void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
w->focused = true;
|
w->focused = true;
|
||||||
clearOtherSelections(w);
|
clearOtherSelections(w);
|
||||||
|
|
@ -1668,6 +1787,15 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetTextDragUpdate — update selection during mouse drag
|
// widgetTextDragUpdate — update selection during mouse drag
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Called by the event loop on mouse-move while sDragTextSelect is set.
|
||||||
|
// Extends the selection from the anchor to the current mouse position.
|
||||||
|
// Handles auto-scroll: when the mouse is past the widget edges, the
|
||||||
|
// scroll offset is nudged by one unit per event, creating a smooth
|
||||||
|
// scroll-while-dragging effect. This function handles TextInput,
|
||||||
|
// TextArea, ComboBox, and AnsiTerm -- all widgets that support
|
||||||
|
// drag-selection. The type switch is slightly ugly but avoids
|
||||||
|
// needing a virtual method for what is essentially the same operation
|
||||||
|
// with different field names.
|
||||||
void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
|
|
@ -1821,6 +1949,23 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetTextAreaPaint
|
// widgetTextAreaPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// TextArea paint uses an optimized incremental line-offset approach:
|
||||||
|
// instead of calling textAreaLineStart() for each visible row (which
|
||||||
|
// would re-scan from the buffer start each time), we compute the
|
||||||
|
// starting offset of the first visible line once, then advance it
|
||||||
|
// line by line. This makes the paint cost O(visRows * maxLineLen)
|
||||||
|
// rather than O(visRows * totalLines).
|
||||||
|
//
|
||||||
|
// Each line is drawn in up to 3 runs (before-selection, selection,
|
||||||
|
// after-selection) to avoid overdraw. Selection highlighting that
|
||||||
|
// extends past the end of a line (the newline itself is "selected")
|
||||||
|
// is rendered as a highlight-colored rectFill past the text.
|
||||||
|
//
|
||||||
|
// Scrollbars are drawn inline (not using the shared scrollbar
|
||||||
|
// functions) because TextArea scrolling is in rows/cols (logical
|
||||||
|
// units) rather than pixels, so the thumb calculation parameters
|
||||||
|
// differ. The dead corner between scrollbars is filled with
|
||||||
|
// windowFace color.
|
||||||
void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
@ -2138,6 +2283,11 @@ const char *widgetTextInputGetText(const WidgetT *w) {
|
||||||
// widgetTextInputOnKey
|
// widgetTextInputOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// TextInput key handling delegates to the shared widgetTextEditOnKey
|
||||||
|
// engine, passing pointers to its state fields. Masked input mode
|
||||||
|
// gets its own handler since the editing semantics are completely
|
||||||
|
// different. Password mode blocks copy/cut at this level before
|
||||||
|
// reaching the shared engine.
|
||||||
void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
if (!w->as.textInput.buf) {
|
if (!w->as.textInput.buf) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2170,6 +2320,12 @@ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetTextInputOnMouse
|
// widgetTextInputOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling for single-line input. Cursor position is computed
|
||||||
|
// from pixel offset using the fixed-width font (relX / charWidth +
|
||||||
|
// scrollOff). Multi-click: double-click selects word (using
|
||||||
|
// wordStart/wordEnd), triple-click selects all. Single click starts
|
||||||
|
// drag-select by setting both selStart and selEnd to the click
|
||||||
|
// position and registering sDragTextSelect.
|
||||||
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
w->focused = true;
|
w->focused = true;
|
||||||
clearOtherSelections(w);
|
clearOtherSelections(w);
|
||||||
|
|
@ -2222,6 +2378,15 @@ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// widgetTextInputPaint
|
// widgetTextInputPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// TextInput paint: sunken 2px bevel, then text with optional selection
|
||||||
|
// highlighting, then cursor line. Text is drawn from a display buffer
|
||||||
|
// that may be either the actual text or bullets (password mode, using
|
||||||
|
// CP437 character 0xF9 which renders as a small centered dot).
|
||||||
|
//
|
||||||
|
// The 3-run approach (before/during/after selection) draws text in a
|
||||||
|
// single pass without overdraw. The scroll offset ensures only the
|
||||||
|
// visible portion of the text is drawn. The cursor is a 1px-wide
|
||||||
|
// vertical line drawn at the character boundary.
|
||||||
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetTextInputPaint(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 fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
@ -2326,6 +2491,24 @@ void widgetTextInputSetText(WidgetT *w, const char *text) {
|
||||||
// widgetTextEditOnKey — shared single-line text editing logic
|
// widgetTextEditOnKey — shared single-line text editing logic
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// This is the core single-line text editing engine, parameterized by
|
||||||
|
// pointer to allow reuse across TextInput, Spinner, and ComboBox.
|
||||||
|
// All buffer manipulation (insert, delete, cursor movement, selection,
|
||||||
|
// clipboard, undo) is handled here. The caller passes pointers to
|
||||||
|
// its own state fields so this function can modify them directly.
|
||||||
|
//
|
||||||
|
// Key mapping follows DOS conventions for extended keys (scancode |
|
||||||
|
// 0x100): 0x4B=Left, 0x4D=Right, 0x47=Home, 0x4F=End, 0x53=Delete,
|
||||||
|
// 0x73=Ctrl+Left, 0x74=Ctrl+Right. Control characters: 1=Ctrl+A,
|
||||||
|
// 3=Ctrl+C, 22=Ctrl+V, 24=Ctrl+X, 26=Ctrl+Z.
|
||||||
|
//
|
||||||
|
// The "goto adjustScroll" pattern consolidates the scroll-offset
|
||||||
|
// adjustment and repaint that most key handlers need, reducing
|
||||||
|
// code duplication. The scroll offset keeps the cursor visible
|
||||||
|
// within the visible character range.
|
||||||
|
//
|
||||||
|
// Single-line paste strips newlines from clipboard content, which
|
||||||
|
// is important when pasting from TextArea to TextInput.
|
||||||
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) {
|
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) {
|
||||||
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
||||||
bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
|
bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,19 @@
|
||||||
// widgetToolbar.c — Toolbar widget
|
// widgetToolbar.c — Toolbar widget
|
||||||
|
//
|
||||||
|
// A horizontal container with a raised 1px bevel background, used to
|
||||||
|
// hold buttons, separators, spacers, and other control widgets in a
|
||||||
|
// strip at the top of a window. Like StatusBar, the toolbar is a
|
||||||
|
// standard HBox layout with custom paint -- child widgets position
|
||||||
|
// themselves via the generic horizontal box algorithm.
|
||||||
|
//
|
||||||
|
// The 1px raised bevel (vs the 2px used for window chrome) keeps the
|
||||||
|
// toolbar visually distinct but lightweight. The bevel's bottom shadow
|
||||||
|
// line serves as the separator between the toolbar and the content
|
||||||
|
// area below it.
|
||||||
|
//
|
||||||
|
// Tight 2px padding and spacing match the StatusBar for visual
|
||||||
|
// consistency between top and bottom chrome bars. Children (typically
|
||||||
|
// ImageButtons or small Buttons) handle their own mouse/key events.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -23,6 +38,9 @@ WidgetT *wgtToolbar(WidgetT *parent) {
|
||||||
// widgetToolbarPaint
|
// widgetToolbarPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Raised bevel with windowFace fill. The 1px bevel width is
|
||||||
|
// intentionally thinner than window chrome (2px) to keep the
|
||||||
|
// toolbar from looking too heavy.
|
||||||
void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,51 @@
|
||||||
// widgetTreeView.c — TreeView and TreeItem widgets
|
// widgetTreeView.c — TreeView and TreeItem widgets
|
||||||
|
//
|
||||||
|
// A hierarchical list with expand/collapse nodes, scrolling, multi-select,
|
||||||
|
// and drag-reorder support.
|
||||||
|
//
|
||||||
|
// Architecture: TreeViewE is the container/viewport widget. TreeItemE
|
||||||
|
// children are the actual data nodes, forming a tree via the standard
|
||||||
|
// widget parent/child/sibling links. This reuses the existing widget
|
||||||
|
// tree structure as the data model -- no separate tree data structure
|
||||||
|
// needed. Each TreeItemE can have child TreeItemEs for nesting.
|
||||||
|
//
|
||||||
|
// Traversal: the tree is navigated in depth-first order using
|
||||||
|
// nextVisibleItem/prevVisibleItem. "Visible" means the item's parent
|
||||||
|
// chain is fully expanded -- collapsed subtrees are skipped. These
|
||||||
|
// traversal functions are O(depth) worst case, which is fine for the
|
||||||
|
// moderate tree depths typical in a DOS GUI.
|
||||||
|
//
|
||||||
|
// Selection model: single-select uses selectedItem pointer on the
|
||||||
|
// TreeViewE. Multi-select adds per-item 'selected' booleans and an
|
||||||
|
// anchorItem for Shift+click range selection. The cursor (selectedItem)
|
||||||
|
// and selection are separate concepts in multi-select mode -- the
|
||||||
|
// cursor is the "current" item for keyboard navigation, while the
|
||||||
|
// selected set is the highlighted items. Space toggles individual
|
||||||
|
// items, Shift+arrow extends range from anchor.
|
||||||
|
//
|
||||||
|
// Scrolling: dual-axis with automatic scrollbar appearance using the
|
||||||
|
// same two-pass mutual-dependency resolution as ScrollPane. Vertical
|
||||||
|
// scroll is in pixels (not items), allowing smooth scrolling. The
|
||||||
|
// treeCalcScrollbarNeeds function centralizes the dimension/scrollbar
|
||||||
|
// computation shared by layout, paint, and mouse handlers.
|
||||||
|
//
|
||||||
|
// Rendering: items are painted recursively with depth-based indent.
|
||||||
|
// The expand/collapse icon is a 9x9 box with +/- inside (Windows
|
||||||
|
// Explorer style). Only items within the visible clip region are
|
||||||
|
// actually drawn -- items outside clipTop/clipBottom are skipped
|
||||||
|
// (though their Y positions are still accumulated for correct
|
||||||
|
// positioning of subsequent items).
|
||||||
|
//
|
||||||
|
// Drag-reorder: when enabled, mouse-down on an item sets dragItem.
|
||||||
|
// Mouse-move (handled in widgetEvent.c) updates dropTarget/dropAfter
|
||||||
|
// to show an insertion line. Mouse-up (widgetReorderDrop) performs
|
||||||
|
// the actual node reparenting via widgetRemoveChild/widgetAddChild.
|
||||||
|
//
|
||||||
|
// Performance note: calcTreeItemsHeight and calcTreeItemsMaxWidth
|
||||||
|
// walk the full visible tree on every call. For trees with hundreds
|
||||||
|
// of items this could be optimized with caching (as TextArea does
|
||||||
|
// for line count). In practice, DOS-era tree views are small enough
|
||||||
|
// that the linear scan is fast on a Pentium.
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
@ -25,6 +72,9 @@ static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *cur
|
||||||
// calcTreeItemsHeight
|
// calcTreeItemsHeight
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Recursively sums the pixel height of all visible (expanded) items.
|
||||||
|
// Each item is one charHeight row. Only descends into expanded nodes,
|
||||||
|
// so collapsed subtrees contribute zero height.
|
||||||
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font) {
|
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font) {
|
||||||
int32_t totalH = 0;
|
int32_t totalH = 0;
|
||||||
|
|
||||||
|
|
@ -48,6 +98,10 @@ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font) {
|
||||||
// calcTreeItemsMaxWidth
|
// calcTreeItemsMaxWidth
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Finds the widest visible item accounting for depth-based indent.
|
||||||
|
// Used to determine if a horizontal scrollbar is needed. The text
|
||||||
|
// width uses strlen * charWidth (monospace font assumption) rather
|
||||||
|
// than a proportional measurement function.
|
||||||
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth) {
|
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth) {
|
||||||
int32_t maxW = 0;
|
int32_t maxW = 0;
|
||||||
|
|
||||||
|
|
@ -143,6 +197,11 @@ static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x,
|
||||||
// Return the next visible tree item after the given item
|
// Return the next visible tree item after the given item
|
||||||
// (depth-first order: children first, then siblings, then uncle).
|
// (depth-first order: children first, then siblings, then uncle).
|
||||||
|
|
||||||
|
// Depth-first forward traversal: try children first (if expanded),
|
||||||
|
// then next sibling, then walk up to find an uncle. This gives the
|
||||||
|
// standard tree traversal order matching what the user sees on screen.
|
||||||
|
// The treeView parameter is the traversal boundary -- we stop walking
|
||||||
|
// up when we reach it.
|
||||||
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView) {
|
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
// If expanded with children, descend
|
// If expanded with children, descend
|
||||||
if (item->as.treeItem.expanded) {
|
if (item->as.treeItem.expanded) {
|
||||||
|
|
@ -206,6 +265,20 @@ static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
|
||||||
// paintTreeItems
|
// paintTreeItems
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Recursive item painting with visibility culling. Items whose Y
|
||||||
|
// range falls entirely outside [clipTop, clipBottom) are skipped --
|
||||||
|
// their Y is still accumulated so subsequent items position correctly,
|
||||||
|
// but no draw calls are made. This is the key performance optimization
|
||||||
|
// for large trees: only visible items incur draw cost.
|
||||||
|
//
|
||||||
|
// Each visible item draws: optional selection highlight background,
|
||||||
|
// expand/collapse icon (if has children), and text label. The expand
|
||||||
|
// icon is a 9x9 bordered box with a horizontal line (minus) when
|
||||||
|
// expanded, or horizontal + vertical lines (plus) when collapsed.
|
||||||
|
// This matches the Windows 95/NT Explorer style.
|
||||||
|
//
|
||||||
|
// In multi-select mode, a focus rect is drawn around the cursor item
|
||||||
|
// (selectedItem) to distinguish it from selected-but-not-cursor items.
|
||||||
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView) {
|
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView) {
|
||||||
bool multi = treeView->as.treeView.multiSelect;
|
bool multi = treeView->as.treeView.multiSelect;
|
||||||
|
|
||||||
|
|
@ -309,6 +382,10 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
|
||||||
// (depth-first order: last visible descendant of previous sibling,
|
// (depth-first order: last visible descendant of previous sibling,
|
||||||
// or parent).
|
// or parent).
|
||||||
|
|
||||||
|
// Depth-first backward traversal: find previous sibling, then descend
|
||||||
|
// to its last visible descendant (the deepest last-child chain). If
|
||||||
|
// no previous sibling, go to parent. This is the inverse of
|
||||||
|
// nextVisibleItem and produces the correct "up arrow" traversal order.
|
||||||
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
|
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
// Find previous sibling
|
// Find previous sibling
|
||||||
WidgetT *prevSib = NULL;
|
WidgetT *prevSib = NULL;
|
||||||
|
|
@ -358,6 +435,12 @@ static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
// Select all visible items between 'from' and 'to' (inclusive).
|
// Select all visible items between 'from' and 'to' (inclusive).
|
||||||
// Direction is auto-detected.
|
// Direction is auto-detected.
|
||||||
|
|
||||||
|
// Selects all visible items between 'from' and 'to' inclusive. The
|
||||||
|
// direction is auto-detected by walking forward from 'from' -- if
|
||||||
|
// 'to' is found, that's the forward direction; otherwise we swap.
|
||||||
|
// This handles both Shift+Down and Shift+Up range selection with
|
||||||
|
// the same code. The forward walk is O(N) in the worst case but
|
||||||
|
// range selection in trees is inherently O(N) anyway.
|
||||||
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to) {
|
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to) {
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -681,6 +764,10 @@ void wgtTreeItemSetSelected(WidgetT *w, bool selected) {
|
||||||
// widgetTreeViewCalcMinSize
|
// widgetTreeViewCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Min size: wide enough for one indent level + expand icon + 6 chars
|
||||||
|
// of text + border + vertical scrollbar. Tall enough for TREE_MIN_ROWS
|
||||||
|
// (4) items. This ensures the tree is usable even when space is tight,
|
||||||
|
// while the weight=100 default allows it to grow to fill available space.
|
||||||
void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t minContentW = TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP + 6 * font->charWidth;
|
int32_t minContentW = TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP + 6 * font->charWidth;
|
||||||
|
|
||||||
|
|
@ -693,6 +780,20 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetTreeViewOnKey
|
// widgetTreeViewOnKey
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Keyboard navigation follows Windows Explorer conventions:
|
||||||
|
// - Up/Down: move cursor to prev/next visible item
|
||||||
|
// - Right: expand collapsed node, or move to first child if expanded
|
||||||
|
// - Left: collapse expanded node, or move to parent if collapsed
|
||||||
|
// - Enter: toggle expand/collapse for parents, onClick for leaves
|
||||||
|
// - Space (multi-select): toggle selection of cursor item
|
||||||
|
//
|
||||||
|
// After cursor movement, the view auto-scrolls to keep the cursor
|
||||||
|
// visible. Multi-select range extension (Shift+arrow) clears existing
|
||||||
|
// selections and selects the range from anchor to cursor.
|
||||||
|
//
|
||||||
|
// The Right/Left expand/collapse behavior provides efficient keyboard
|
||||||
|
// tree navigation: Right drills in, Left backs out, without needing
|
||||||
|
// separate expand/collapse keys.
|
||||||
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
VALIDATE_WIDGET_VOID(w, WidgetTreeViewE);
|
VALIDATE_WIDGET_VOID(w, WidgetTreeViewE);
|
||||||
|
|
||||||
|
|
@ -848,6 +949,11 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
// widgetTreeViewLayout
|
// widgetTreeViewLayout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Layout assigns (x, y, w, h) to all visible tree items at their
|
||||||
|
// scroll-adjusted screen coordinates. Item width extends to the
|
||||||
|
// full inner width (or total content width if wider), so selection
|
||||||
|
// highlight bars span the full visible width. Auto-selects the first
|
||||||
|
// item if nothing is selected yet, providing a reasonable default.
|
||||||
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// Auto-select first item if nothing is selected
|
// Auto-select first item if nothing is selected
|
||||||
if (!w->as.treeView.selectedItem) {
|
if (!w->as.treeView.selectedItem) {
|
||||||
|
|
@ -881,6 +987,24 @@ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetTreeViewOnMouse
|
// widgetTreeViewOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Mouse handling priority: V scrollbar > H scrollbar > dead corner >
|
||||||
|
// tree item. Item clicks are resolved by treeItemAtY which walks the
|
||||||
|
// visible tree converting Y coordinates to items. The item's depth
|
||||||
|
// is computed by walking up the parent chain to determine the expand
|
||||||
|
// icon's X position and check if the click landed on it.
|
||||||
|
//
|
||||||
|
// Multi-select mouse behavior:
|
||||||
|
// - Plain click: select only this item (clear others), set anchor
|
||||||
|
// - Ctrl+click: toggle item, update anchor
|
||||||
|
// - Shift+click: range-select from anchor to clicked item
|
||||||
|
//
|
||||||
|
// Collapsing a node re-checks scroll bounds because content height
|
||||||
|
// may have decreased, and the current scroll position may now be
|
||||||
|
// past the new maximum.
|
||||||
|
//
|
||||||
|
// Drag-reorder initiation: plain click (no modifier, not on expand
|
||||||
|
// icon) sets dragItem and sDragReorder. The actual reorder drag
|
||||||
|
// tracking happens in widgetEvent.c during mouse-move.
|
||||||
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->focused = true;
|
hit->focused = true;
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
|
|
@ -1091,6 +1215,11 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
// widgetTreeViewPaint
|
// widgetTreeViewPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Paint: sunken border, then clipped content area for items +
|
||||||
|
// reorder indicator, then scrollbars outside the clip rect. The clip
|
||||||
|
// rect excludes the scrollbar area so items don't paint over scrollbars.
|
||||||
|
// Both scroll axes are applied to baseX/itemY so the tree content
|
||||||
|
// shifts correctly in both dimensions.
|
||||||
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@
|
||||||
// Module state
|
// Module state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Fixed-size app slot table. Using a static array (not dynamic) because:
|
||||||
|
// 1. The number of concurrent apps is bounded by available DOS memory
|
||||||
|
// 2. App IDs are slot indices, so a fixed array gives O(1) lookup
|
||||||
|
// 3. No fragmentation concerns from repeated malloc/free cycles
|
||||||
|
// Slot 0 is reserved (represents the shell itself); apps use slots 1..31.
|
||||||
static ShellAppT sApps[SHELL_MAX_APPS];
|
static ShellAppT sApps[SHELL_MAX_APPS];
|
||||||
int32_t sCurrentAppId = 0;
|
int32_t sCurrentAppId = 0;
|
||||||
|
|
||||||
|
|
@ -38,6 +43,9 @@ void shellTerminateAllApps(AppContextT *ctx);
|
||||||
// Static helpers
|
// Static helpers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Find the first free slot, starting at 1 (slot 0 is the shell).
|
||||||
|
// Returns the slot index which also serves as the app's unique ID.
|
||||||
|
// This linear scan is fine for 32 slots.
|
||||||
static int32_t allocSlot(void) {
|
static int32_t allocSlot(void) {
|
||||||
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
|
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
|
||||||
if (sApps[i].state == AppStateFreeE) {
|
if (sApps[i].state == AppStateFreeE) {
|
||||||
|
|
@ -49,6 +57,15 @@ static int32_t allocSlot(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Task entry point for main-loop apps. Runs in its own cooperative task.
|
||||||
|
// Sets sCurrentAppId before calling the app's entry point so that any
|
||||||
|
// GUI resources created during the app's lifetime are tagged with the
|
||||||
|
// correct owner. When the app's main loop returns (normal exit), the
|
||||||
|
// state is set to Terminating so the shell's main loop will reap it.
|
||||||
|
//
|
||||||
|
// If the app crashes (signal), the signal handler longjmps to main and
|
||||||
|
// this function never completes — the task is killed externally via
|
||||||
|
// shellForceKillApp + tsKill.
|
||||||
static void appTaskWrapper(void *arg) {
|
static void appTaskWrapper(void *arg) {
|
||||||
ShellAppT *app = (ShellAppT *)arg;
|
ShellAppT *app = (ShellAppT *)arg;
|
||||||
|
|
||||||
|
|
@ -80,12 +97,19 @@ void shellAppInit(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Forcible kill — no shutdown hook is called. Used for crashed apps
|
||||||
|
// (where running more app code would be unsafe) and for "End Task".
|
||||||
|
// Cleanup order matters: windows first (removes them from the compositor),
|
||||||
|
// then the task (frees the stack), then the DXE handle (unmaps the code).
|
||||||
|
// If we closed the DXE first, destroying windows could call into unmapped
|
||||||
|
// callback code and crash the shell.
|
||||||
void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
|
void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
|
||||||
if (!app || app->state == AppStateFreeE) {
|
if (!app || app->state == AppStateFreeE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy all windows belonging to this app
|
// Destroy all windows belonging to this app. Walk backwards because
|
||||||
|
// dvxDestroyWindow removes the window from the stack, shifting indices.
|
||||||
for (int32_t i = ctx->stack.count - 1; i >= 0; i--) {
|
for (int32_t i = ctx->stack.count - 1; i >= 0; i--) {
|
||||||
if (ctx->stack.windows[i]->appId == app->appId) {
|
if (ctx->stack.windows[i]->appId == app->appId) {
|
||||||
dvxDestroyWindow(ctx, ctx->stack.windows[i]);
|
dvxDestroyWindow(ctx, ctx->stack.windows[i]);
|
||||||
|
|
@ -123,6 +147,11 @@ ShellAppT *shellGetApp(int32_t appId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Load a DXE app: dlopen the module, resolve symbols, set up context, launch.
|
||||||
|
// DXE3 is DJGPP's dynamic linking system — similar to dlopen/dlsym on Unix.
|
||||||
|
// Each .app file is a DXE3 shared object that exports _appDescriptor and
|
||||||
|
// _appMain (and optionally _appShutdown). The leading underscore is the
|
||||||
|
// COFF symbol convention; DJGPP's dlsym expects it.
|
||||||
int32_t shellLoadApp(AppContextT *ctx, const char *path) {
|
int32_t shellLoadApp(AppContextT *ctx, const char *path) {
|
||||||
// Allocate a slot
|
// Allocate a slot
|
||||||
int32_t id = allocSlot();
|
int32_t id = allocSlot();
|
||||||
|
|
@ -182,7 +211,10 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
|
||||||
app->dxeCtx.shellCtx = ctx;
|
app->dxeCtx.shellCtx = ctx;
|
||||||
app->dxeCtx.appId = id;
|
app->dxeCtx.appId = id;
|
||||||
|
|
||||||
// Derive app directory from path (everything up to last '/' or '\')
|
// Derive app directory from path (everything up to last '/' or '\').
|
||||||
|
// This lets apps load resources relative to their own location rather
|
||||||
|
// than the shell's working directory. Handles both Unix and DOS path
|
||||||
|
// separators because DJGPP accepts either.
|
||||||
snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path);
|
snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path);
|
||||||
|
|
||||||
char *lastSlash = strrchr(app->dxeCtx.appDir, '/');
|
char *lastSlash = strrchr(app->dxeCtx.appDir, '/');
|
||||||
|
|
@ -199,7 +231,9 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
|
||||||
app->dxeCtx.appDir[1] = '\0';
|
app->dxeCtx.appDir[1] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch
|
// Launch. Set sCurrentAppId before any app code runs so that window
|
||||||
|
// creation wrappers stamp the correct owner. Reset to 0 afterward so
|
||||||
|
// shell-initiated operations (e.g., message boxes) aren't misattributed.
|
||||||
sCurrentAppId = id;
|
sCurrentAppId = id;
|
||||||
|
|
||||||
if (desc->hasMainLoop) {
|
if (desc->hasMainLoop) {
|
||||||
|
|
@ -218,7 +252,10 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
|
||||||
|
|
||||||
app->mainTaskId = (uint32_t)taskId;
|
app->mainTaskId = (uint32_t)taskId;
|
||||||
} else {
|
} else {
|
||||||
// Callback-only: call entry directly in task 0
|
// Callback-only: call entry directly in task 0 (the shell).
|
||||||
|
// The app creates its windows and returns. From this point on,
|
||||||
|
// the app lives entirely through event callbacks dispatched by
|
||||||
|
// the shell's dvxUpdate loop. No separate task or stack needed.
|
||||||
app->entryFn(&app->dxeCtx);
|
app->entryFn(&app->dxeCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,6 +267,11 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Graceful reap — called from shellReapApps when an app has reached
|
||||||
|
// the Terminating state. Unlike forceKill, this calls the app's
|
||||||
|
// shutdown hook (if provided) giving it a chance to save state, close
|
||||||
|
// files, etc. The sCurrentAppId is set during the shutdown call so
|
||||||
|
// any final resource operations are attributed correctly.
|
||||||
void shellReapApp(AppContextT *ctx, ShellAppT *app) {
|
void shellReapApp(AppContextT *ctx, ShellAppT *app) {
|
||||||
if (!app || app->state == AppStateFreeE) {
|
if (!app || app->state == AppStateFreeE) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -267,6 +309,11 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Called every frame from the shell's main loop. Scans for apps that have
|
||||||
|
// entered the Terminating state (either their task returned or their last
|
||||||
|
// window was closed) and cleans them up. The deferred-reap design avoids
|
||||||
|
// destroying resources in the middle of a callback chain — the app marks
|
||||||
|
// itself for termination, and cleanup happens at a safe top-level point.
|
||||||
bool shellReapApps(AppContextT *ctx) {
|
bool shellReapApps(AppContextT *ctx) {
|
||||||
bool reaped = false;
|
bool reaped = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
// shellApp.h — DVX Shell application lifecycle types and API
|
// shellApp.h — DVX Shell application lifecycle types and API
|
||||||
|
//
|
||||||
|
// The shell supports two kinds of DXE apps:
|
||||||
|
//
|
||||||
|
// 1. Callback-only apps (hasMainLoop = false):
|
||||||
|
// appMain() is called directly in the shell's task 0. It creates
|
||||||
|
// windows, registers event callbacks, and returns immediately. The
|
||||||
|
// app then lives entirely through GUI callbacks driven by the shell's
|
||||||
|
// main loop. This is simpler and cheaper (no extra stack/task) and
|
||||||
|
// is the right choice for apps that are purely event-driven (dialogs,
|
||||||
|
// simple tools). Lifecycle ends when the last window is closed.
|
||||||
|
//
|
||||||
|
// 2. Main-loop apps (hasMainLoop = true):
|
||||||
|
// A dedicated cooperative task is created for the app. appMain() runs
|
||||||
|
// in that task and can do its own polling/processing loop, calling
|
||||||
|
// tsYield() to share CPU. This is needed for apps that have continuous
|
||||||
|
// work (terminal emulators, games, long computations). Lifecycle ends
|
||||||
|
// when appMain() returns or the task is killed.
|
||||||
|
//
|
||||||
|
// Both types use the same DXE interface (appDescriptor + appMain), so
|
||||||
|
// app authors choose by setting hasMainLoop in their descriptor.
|
||||||
#ifndef SHELL_APP_H
|
#ifndef SHELL_APP_H
|
||||||
#define SHELL_APP_H
|
#define SHELL_APP_H
|
||||||
|
|
||||||
|
|
@ -14,8 +34,13 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define SHELL_APP_NAME_MAX 64
|
#define SHELL_APP_NAME_MAX 64
|
||||||
|
// Fixed array size for app slots. A small fixed limit avoids dynamic
|
||||||
|
// allocation complexity and matches the practical limit of a DOS desktop.
|
||||||
|
// Slot 0 is reserved for the shell itself, so 31 app slots are usable.
|
||||||
#define SHELL_MAX_APPS 32
|
#define SHELL_MAX_APPS 32
|
||||||
|
|
||||||
|
// Every DXE app exports a global AppDescriptorT named "appDescriptor".
|
||||||
|
// The shell reads it at load time to determine how to launch the app.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[SHELL_APP_NAME_MAX];
|
char name[SHELL_APP_NAME_MAX];
|
||||||
bool hasMainLoop;
|
bool hasMainLoop;
|
||||||
|
|
@ -27,6 +52,12 @@ typedef struct {
|
||||||
// App context (passed to appMain)
|
// App context (passed to appMain)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Passed as the sole argument to appMain(). Gives the app access to
|
||||||
|
// the shell's GUI context (for creating windows, drawing, etc.) and
|
||||||
|
// its own identity. appDir is derived from the .app file path at load
|
||||||
|
// time so the app can find its own resources (icons, data files) via
|
||||||
|
// relative paths — important because the working directory is shared
|
||||||
|
// by all apps and can't be changed per-app in DOS.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
AppContextT *shellCtx; // the shell's GUI context
|
AppContextT *shellCtx; // the shell's GUI context
|
||||||
int32_t appId; // this app's ID
|
int32_t appId; // this app's ID
|
||||||
|
|
@ -37,6 +68,11 @@ typedef struct {
|
||||||
// Per-app state
|
// Per-app state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// State machine: Free -> Loaded -> Running -> Terminating -> Free
|
||||||
|
// LoadedE is transient (only during shellLoadApp before entry is called).
|
||||||
|
// TerminatingE means the app's task has exited but cleanup (window
|
||||||
|
// destruction, DXE unload) hasn't happened yet — the shell's main loop
|
||||||
|
// reaps these each frame via shellReapApps().
|
||||||
typedef enum {
|
typedef enum {
|
||||||
AppStateFreeE, // slot available
|
AppStateFreeE, // slot available
|
||||||
AppStateLoadedE, // DXE loaded, not yet started
|
AppStateLoadedE, // DXE loaded, not yet started
|
||||||
|
|
@ -61,7 +97,13 @@ typedef struct {
|
||||||
// Shell global state
|
// Shell global state
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Current app ID for resource tracking (0 = shell)
|
// Current app ID for resource tracking (0 = shell).
|
||||||
|
// Set before calling any app code (entry, shutdown, callbacks) so that
|
||||||
|
// wrapper functions (shellWrapCreateWindow, etc.) can stamp newly created
|
||||||
|
// resources with the correct owner. This is the mechanism that lets the
|
||||||
|
// shell know which windows belong to which app, enabling per-app cleanup
|
||||||
|
// on crash or termination. It's a global rather than a thread-local
|
||||||
|
// because cooperative multitasking means only one task runs at a time.
|
||||||
extern int32_t sCurrentAppId;
|
extern int32_t sCurrentAppId;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -72,15 +114,25 @@ extern int32_t sCurrentAppId;
|
||||||
void shellAppInit(void);
|
void shellAppInit(void);
|
||||||
|
|
||||||
// Load and start an app from a DXE file. Returns app ID (>= 1) or -1 on error.
|
// Load and start an app from a DXE file. Returns app ID (>= 1) or -1 on error.
|
||||||
|
// For callback-only apps, appMain runs synchronously and returns before
|
||||||
|
// shellLoadApp returns. For main-loop apps, a task is created and the
|
||||||
|
// app starts running on the next tsYield.
|
||||||
int32_t shellLoadApp(AppContextT *ctx, const char *path);
|
int32_t shellLoadApp(AppContextT *ctx, const char *path);
|
||||||
|
|
||||||
// Reap finished callback-only apps (call each frame from main loop)
|
// Reap finished apps (call each frame from main loop). Returns true if
|
||||||
|
// any apps were reaped, so the caller can notify the desktop to refresh.
|
||||||
|
// For callback-only apps, termination is triggered by shellWrapDestroyWindow
|
||||||
|
// when the last window closes. For main-loop apps, termination happens
|
||||||
|
// when appMain returns (via appTaskWrapper marking AppStateTerminatingE).
|
||||||
bool shellReapApps(AppContextT *ctx);
|
bool shellReapApps(AppContextT *ctx);
|
||||||
|
|
||||||
// Gracefully shut down a single app
|
// Gracefully shut down a single app — calls shutdownFn if present,
|
||||||
|
// destroys windows, kills task, closes DXE handle.
|
||||||
void shellReapApp(AppContextT *ctx, ShellAppT *app);
|
void shellReapApp(AppContextT *ctx, ShellAppT *app);
|
||||||
|
|
||||||
// Forcibly kill an app (Task Manager "End Task")
|
// Forcibly kill an app (Task Manager "End Task"). Skips shutdownFn —
|
||||||
|
// used when the app is hung or has crashed and cannot be trusted to
|
||||||
|
// run its own cleanup code.
|
||||||
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
|
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
|
||||||
|
|
||||||
// Terminate all running apps (shell shutdown)
|
// Terminate all running apps (shell shutdown)
|
||||||
|
|
@ -114,7 +166,10 @@ void shellExportInit(void);
|
||||||
#define SHELL_DESKTOP_APP "apps/progman/progman.app"
|
#define SHELL_DESKTOP_APP "apps/progman/progman.app"
|
||||||
|
|
||||||
// Register a callback for app state changes (load, reap, crash).
|
// Register a callback for app state changes (load, reap, crash).
|
||||||
// The desktop app calls this during appMain to receive notifications.
|
// The desktop app (Program Manager) calls this during appMain to receive
|
||||||
|
// notifications so it can refresh its task list / window list display.
|
||||||
|
// Only one callback is supported — the desktop is always loaded first
|
||||||
|
// and is the only consumer.
|
||||||
void shellRegisterDesktopUpdate(void (*updateFn)(void));
|
void shellRegisterDesktopUpdate(void (*updateFn)(void));
|
||||||
|
|
||||||
#endif // SHELL_APP_H
|
#endif // SHELL_APP_H
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,30 @@
|
||||||
//
|
//
|
||||||
// Exports all dvx*/wgt*/ts* symbols that DXE apps need. A few functions
|
// Exports all dvx*/wgt*/ts* symbols that DXE apps need. A few functions
|
||||||
// are wrapped for resource tracking (window ownership via appId).
|
// are wrapped for resource tracking (window ownership via appId).
|
||||||
|
//
|
||||||
|
// DXE3 is DJGPP's dynamic linking mechanism. Unlike ELF shared libraries,
|
||||||
|
// DXE modules have no implicit access to the host's symbol table. Every
|
||||||
|
// function or variable the DXE needs must be explicitly listed in an
|
||||||
|
// export table registered via dlregsym() BEFORE any dlopen() call. If a
|
||||||
|
// symbol is missing, dlopen() returns NULL with a "symbol not found" error.
|
||||||
|
//
|
||||||
|
// This file is essentially the ABI contract between the shell and apps.
|
||||||
|
// Three categories of exports:
|
||||||
|
//
|
||||||
|
// 1. Wrapped functions: dvxCreateWindow, dvxCreateWindowCentered,
|
||||||
|
// dvxDestroyWindow. These are intercepted to stamp win->appId for
|
||||||
|
// resource ownership tracking. The DXE sees them under their original
|
||||||
|
// names — the app code calls dvxCreateWindow() normally and gets our
|
||||||
|
// wrapper transparently.
|
||||||
|
//
|
||||||
|
// 2. Direct exports: all other dvx/wgt/wm/ts functions. These are safe
|
||||||
|
// to call without shell-side interception.
|
||||||
|
//
|
||||||
|
// 3. libc functions: DXE modules are statically linked against DJGPP's
|
||||||
|
// libc, but DJGPP's DXE3 loader requires explicit re-export of any
|
||||||
|
// libc symbols the module references. Without these entries, the DXE
|
||||||
|
// would fail to load with unresolved symbol errors. This is a DXE3
|
||||||
|
// design limitation — there's no automatic fallback to the host's libc.
|
||||||
|
|
||||||
#include "shellApp.h"
|
#include "shellApp.h"
|
||||||
#include "dvxApp.h"
|
#include "dvxApp.h"
|
||||||
|
|
@ -34,6 +58,11 @@ static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win);
|
||||||
// Wrapper: dvxCreateWindow — stamps win->appId
|
// Wrapper: dvxCreateWindow — stamps win->appId
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// The wrapper calls the real dvxCreateWindow, then tags the result with
|
||||||
|
// sCurrentAppId. This is how the shell knows which app owns which window,
|
||||||
|
// enabling per-app window cleanup on crash/termination. The app never
|
||||||
|
// sees the difference — the wrapper has the same signature and is
|
||||||
|
// exported under the same name as the original function.
|
||||||
static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
|
static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
|
||||||
WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable);
|
WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable);
|
||||||
|
|
||||||
|
|
@ -64,6 +93,12 @@ static WindowT *shellWrapCreateWindowCentered(AppContextT *ctx, const char *titl
|
||||||
// Wrapper: dvxDestroyWindow — checks for last-window reap
|
// Wrapper: dvxDestroyWindow — checks for last-window reap
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Beyond just destroying the window, this wrapper implements the lifecycle
|
||||||
|
// rule for callback-only apps: when their last window closes, they're done.
|
||||||
|
// Main-loop apps manage their own lifetime (their task returns from
|
||||||
|
// appMain), so this check only applies to callback-only apps.
|
||||||
|
// The appId is captured before destruction because the window struct is
|
||||||
|
// freed by dvxDestroyWindow.
|
||||||
static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) {
|
static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) {
|
||||||
int32_t appId = win->appId;
|
int32_t appId = win->appId;
|
||||||
|
|
||||||
|
|
@ -96,11 +131,14 @@ static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) {
|
||||||
// Export table
|
// Export table
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// We use extern_asm to get the actual addresses for wrapped functions
|
// DXE_EXPORT_TABLE generates a DXE symbol table array. DXE_EXPORT(fn)
|
||||||
// since we export under the original names but point to our wrappers.
|
// expands to { "_fn", (void *)fn } — the underscore prefix matches COFF
|
||||||
|
// symbol naming. For wrapped functions we use raw entries with explicit
|
||||||
|
// names so the DXE sees "_dvxCreateWindow" but gets our wrapper's address.
|
||||||
|
|
||||||
DXE_EXPORT_TABLE(shellExportTable)
|
DXE_EXPORT_TABLE(shellExportTable)
|
||||||
// Wrapped functions (exported under original names)
|
// Wrapped functions (exported under original names, but pointing to
|
||||||
|
// our wrappers that add resource tracking)
|
||||||
{ "_dvxCreateWindow", (void *)shellWrapCreateWindow },
|
{ "_dvxCreateWindow", (void *)shellWrapCreateWindow },
|
||||||
{ "_dvxDestroyWindow", (void *)shellWrapDestroyWindow },
|
{ "_dvxDestroyWindow", (void *)shellWrapDestroyWindow },
|
||||||
|
|
||||||
|
|
@ -331,7 +369,10 @@ DXE_EXPORT_TABLE(shellExportTable)
|
||||||
DXE_EXPORT(wgtLayout)
|
DXE_EXPORT(wgtLayout)
|
||||||
DXE_EXPORT(wgtPaint)
|
DXE_EXPORT(wgtPaint)
|
||||||
|
|
||||||
// taskswitch.h
|
// taskswitch.h — only yield and query functions are exported.
|
||||||
|
// tsCreate/tsKill/etc. are NOT exported because apps should not
|
||||||
|
// manipulate the task system directly — the shell manages task
|
||||||
|
// lifecycle through shellLoadApp/shellForceKillApp.
|
||||||
DXE_EXPORT(tsYield)
|
DXE_EXPORT(tsYield)
|
||||||
DXE_EXPORT(tsCurrentId)
|
DXE_EXPORT(tsCurrentId)
|
||||||
DXE_EXPORT(tsActiveCount)
|
DXE_EXPORT(tsActiveCount)
|
||||||
|
|
@ -349,6 +390,12 @@ DXE_EXPORT_TABLE(shellExportTable)
|
||||||
DXE_EXPORT(shellRunningAppCount)
|
DXE_EXPORT(shellRunningAppCount)
|
||||||
DXE_EXPORT(shellRegisterDesktopUpdate)
|
DXE_EXPORT(shellRegisterDesktopUpdate)
|
||||||
|
|
||||||
|
// libc exports below. DXE3 modules are compiled as relocatable objects,
|
||||||
|
// not fully linked executables. Any libc function the DXE calls must be
|
||||||
|
// re-exported here so the DXE3 loader can resolve the reference at
|
||||||
|
// dlopen time. Forgetting an entry produces a cryptic "unresolved
|
||||||
|
// symbol" error at load time — no lazy binding fallback exists.
|
||||||
|
|
||||||
// libc — memory
|
// libc — memory
|
||||||
DXE_EXPORT(malloc)
|
DXE_EXPORT(malloc)
|
||||||
DXE_EXPORT(free)
|
DXE_EXPORT(free)
|
||||||
|
|
@ -419,6 +466,9 @@ DXE_EXPORT_END
|
||||||
// shellRegisterExports
|
// shellRegisterExports
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// dlregsym registers our export table with DJGPP's DXE3 runtime.
|
||||||
|
// Must be called once before any dlopen — subsequent dlopen calls
|
||||||
|
// will search this table to resolve DXE symbol references.
|
||||||
static void shellRegisterExports(void) {
|
static void shellRegisterExports(void) {
|
||||||
dlregsym(shellExportTable);
|
dlregsym(shellExportTable);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,26 @@
|
||||||
// Initializes the GUI, task system, DXE export table, and loads
|
// Initializes the GUI, task system, DXE export table, and loads
|
||||||
// the desktop app. Runs the cooperative main loop, yielding to
|
// the desktop app. Runs the cooperative main loop, yielding to
|
||||||
// app tasks and reaping terminated apps each frame.
|
// app tasks and reaping terminated apps each frame.
|
||||||
|
//
|
||||||
|
// The main loop design (dvxUpdate + tsYield + reap + notify):
|
||||||
|
// Each iteration does four things:
|
||||||
|
// 1. dvxUpdate: processes input events, dispatches callbacks, composites
|
||||||
|
// dirty rects, flushes to the LFB. This is the shell's primary job.
|
||||||
|
// 2. tsYield: gives CPU time to app tasks. Without this, main-loop apps
|
||||||
|
// would never run because the shell task would monopolize the CPU.
|
||||||
|
// 3. shellReapApps: cleans up any apps that terminated during this frame
|
||||||
|
// (either their task returned or their last window was closed).
|
||||||
|
// 4. desktopUpdate: notifies the desktop app (Program Manager) if any
|
||||||
|
// apps were reaped, so it can refresh its task list.
|
||||||
|
//
|
||||||
|
// Crash recovery uses setjmp/longjmp:
|
||||||
|
// The shell installs signal handlers for SIGSEGV, SIGFPE, SIGILL. If a
|
||||||
|
// crash occurs in an app task, the handler longjmps back to the setjmp
|
||||||
|
// point in main(). This works because longjmp restores the main task's
|
||||||
|
// stack frame regardless of which task was running. tsRecoverToMain()
|
||||||
|
// then fixes the scheduler's bookkeeping, and the crashed app is killed.
|
||||||
|
// This gives the shell Windows 3.1-style fault tolerance — one bad app
|
||||||
|
// doesn't take down the whole system.
|
||||||
|
|
||||||
#include "shellApp.h"
|
#include "shellApp.h"
|
||||||
#include "dvxDialog.h"
|
#include "dvxDialog.h"
|
||||||
|
|
@ -13,6 +33,8 @@
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
// DJGPP-specific: provides __djgpp_exception_state_ptr for accessing
|
||||||
|
// CPU register state at the point of the exception
|
||||||
#include <sys/exceptn.h>
|
#include <sys/exceptn.h>
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -20,7 +42,11 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static AppContextT sCtx;
|
static AppContextT sCtx;
|
||||||
|
// setjmp buffer for crash recovery. The crash handler longjmps here to
|
||||||
|
// return control to the shell's main loop after an app crashes.
|
||||||
static jmp_buf sCrashJmp;
|
static jmp_buf sCrashJmp;
|
||||||
|
// Volatile because it's written from a signal handler context. Tells
|
||||||
|
// the recovery code which signal fired (for logging/diagnostics).
|
||||||
static volatile int sCrashSignal = 0;
|
static volatile int sCrashSignal = 0;
|
||||||
static FILE *sLogFile = NULL;
|
static FILE *sLogFile = NULL;
|
||||||
static void (*sDesktopUpdateFn)(void) = NULL;
|
static void (*sDesktopUpdateFn)(void) = NULL;
|
||||||
|
|
@ -40,6 +66,16 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
|
||||||
// crashHandler — catch page faults and other fatal signals
|
// crashHandler — catch page faults and other fatal signals
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Signal handler for fatal exceptions. DJGPP uses System V signal
|
||||||
|
// semantics where the handler is reset to SIG_DFL after each delivery,
|
||||||
|
// so we must re-install it before doing anything else.
|
||||||
|
//
|
||||||
|
// The longjmp is the key to crash recovery: it unwinds whatever stack
|
||||||
|
// we're on (potentially a crashed app's task stack) and restores the
|
||||||
|
// main task's stack frame to the setjmp point in main(). This is safe
|
||||||
|
// because cooperative switching means the main task's stack is always
|
||||||
|
// intact — it was cleanly suspended at a yield point. The crashed
|
||||||
|
// task's stack is abandoned (and later freed by tsKill).
|
||||||
static void crashHandler(int sig) {
|
static void crashHandler(int sig) {
|
||||||
logCrash(sig);
|
logCrash(sig);
|
||||||
|
|
||||||
|
|
@ -66,6 +102,12 @@ static void desktopUpdate(void) {
|
||||||
// idleYield — called when no dirty rects need compositing
|
// idleYield — called when no dirty rects need compositing
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Registered as sCtx.idleCallback. dvxUpdate calls this when it has
|
||||||
|
// processed all pending events and there are no dirty rects to composite.
|
||||||
|
// Instead of busy-spinning, we yield to app tasks — this is where most
|
||||||
|
// of the CPU time for main-loop apps comes from when the UI is idle.
|
||||||
|
// The tsActiveCount > 1 check avoids the overhead of a tsYield call
|
||||||
|
// (which would do a scheduler scan) when the shell is the only task.
|
||||||
static void idleYield(void *ctx) {
|
static void idleYield(void *ctx) {
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
|
|
||||||
|
|
@ -90,6 +132,11 @@ static void installCrashHandler(void) {
|
||||||
// logCrash — write exception details to the log
|
// logCrash — write exception details to the log
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
// Dump as much diagnostic info as possible before longjmp destroys the
|
||||||
|
// crash context. This runs inside the signal handler, so only
|
||||||
|
// async-signal-safe functions should be used — but since we're in
|
||||||
|
// DJGPP (single-threaded DOS), reentrancy isn't a practical concern
|
||||||
|
// and vfprintf/fflush are safe to call here.
|
||||||
static void logCrash(int sig) {
|
static void logCrash(int sig) {
|
||||||
const char *sigName = "UNKNOWN";
|
const char *sigName = "UNKNOWN";
|
||||||
|
|
||||||
|
|
@ -118,7 +165,10 @@ static void logCrash(int sig) {
|
||||||
shellLog("Crashed in shell (task 0)");
|
shellLog("Crashed in shell (task 0)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump CPU registers from exception state
|
// __djgpp_exception_state_ptr is a DJGPP extension that captures the
|
||||||
|
// full CPU register state at the point of the exception. This gives
|
||||||
|
// us the faulting EIP, stack pointer, and all GPRs — invaluable for
|
||||||
|
// post-mortem debugging of app crashes from the log file.
|
||||||
jmp_buf *estate = __djgpp_exception_state_ptr;
|
jmp_buf *estate = __djgpp_exception_state_ptr;
|
||||||
|
|
||||||
if (estate) {
|
if (estate) {
|
||||||
|
|
@ -203,7 +253,10 @@ int main(void) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shell task (task 0) gets high priority for responsive UI
|
// Shell task (task 0) gets high priority so the UI remains responsive
|
||||||
|
// even when app tasks are CPU-hungry. With HIGH priority (11 credits
|
||||||
|
// per epoch) vs app tasks at NORMAL (6 credits), the shell gets
|
||||||
|
// roughly twice as many scheduling turns as any single app.
|
||||||
tsSetPriority(0, TS_PRIORITY_HIGH);
|
tsSetPriority(0, TS_PRIORITY_HIGH);
|
||||||
|
|
||||||
// Register DXE export table
|
// Register DXE export table
|
||||||
|
|
@ -212,7 +265,10 @@ int main(void) {
|
||||||
// Initialize app slot table
|
// Initialize app slot table
|
||||||
shellAppInit();
|
shellAppInit();
|
||||||
|
|
||||||
// Set up idle callback for cooperative yielding
|
// Set up idle callback for cooperative yielding. When dvxUpdate has
|
||||||
|
// no work to do (no input events, no dirty rects), it calls this
|
||||||
|
// instead of busy-looping. This is the main mechanism for giving
|
||||||
|
// app tasks CPU time during quiet periods.
|
||||||
sCtx.idleCallback = idleYield;
|
sCtx.idleCallback = idleYield;
|
||||||
sCtx.idleCtx = &sCtx;
|
sCtx.idleCtx = &sCtx;
|
||||||
|
|
||||||
|
|
@ -231,16 +287,23 @@ int main(void) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install crash handler after everything is initialized
|
// Install crash handler after everything is initialized — if
|
||||||
|
// initialization itself crashes, we want the default DJGPP behavior
|
||||||
|
// (abort with register dump) rather than our recovery path, because
|
||||||
|
// the system isn't in a recoverable state yet.
|
||||||
installCrashHandler();
|
installCrashHandler();
|
||||||
|
|
||||||
shellLog("DVX Shell ready.");
|
shellLog("DVX Shell ready.");
|
||||||
|
|
||||||
// Set recovery point for crash handler
|
// Set recovery point for crash handler. setjmp returns 0 on initial
|
||||||
|
// call (falls through to the main loop). On a crash, longjmp makes
|
||||||
|
// setjmp return non-zero, entering this recovery block. The recovery
|
||||||
|
// code runs on the main task's stack (restored by longjmp) so it's
|
||||||
|
// safe to call any shell function.
|
||||||
if (setjmp(sCrashJmp) != 0) {
|
if (setjmp(sCrashJmp) != 0) {
|
||||||
// Returned here from crash handler via longjmp.
|
// Returned here from crash handler via longjmp.
|
||||||
// If the crash was in a non-main task, the task switcher still
|
// The task switcher's currentIdx still points to the crashed task.
|
||||||
// thinks that task is running. Fix it before doing anything else.
|
// Fix it before doing anything else so the scheduler is consistent.
|
||||||
tsRecoverToMain();
|
tsRecoverToMain();
|
||||||
|
|
||||||
shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId);
|
shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId);
|
||||||
|
|
@ -262,7 +325,12 @@ int main(void) {
|
||||||
desktopUpdate();
|
desktopUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main loop
|
// Main loop — runs until dvxQuit() sets sCtx.running = false.
|
||||||
|
// Two yield points per iteration: one explicit (below) and one via
|
||||||
|
// the idle callback inside dvxUpdate. The explicit yield here ensures
|
||||||
|
// app tasks get CPU time even during busy frames (lots of repaints).
|
||||||
|
// Without it, a flurry of mouse-move events could starve app tasks
|
||||||
|
// because dvxUpdate would keep finding work to do and never call idle.
|
||||||
while (sCtx.running) {
|
while (sCtx.running) {
|
||||||
dvxUpdate(&sCtx);
|
dvxUpdate(&sCtx);
|
||||||
|
|
||||||
|
|
@ -271,7 +339,9 @@ int main(void) {
|
||||||
tsYield();
|
tsYield();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reap terminated apps and notify desktop if anything changed
|
// Reap terminated apps and notify desktop if anything changed.
|
||||||
|
// This is the safe point for cleanup — we're at the top of the
|
||||||
|
// main loop, not inside any callback or compositor operation.
|
||||||
if (shellReapApps(&sCtx)) {
|
if (shellReapApps(&sCtx)) {
|
||||||
desktopUpdate();
|
desktopUpdate();
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +349,9 @@ int main(void) {
|
||||||
|
|
||||||
shellLog("DVX Shell shutting down...");
|
shellLog("DVX Shell shutting down...");
|
||||||
|
|
||||||
// Clean shutdown: terminate all apps
|
// Clean shutdown: terminate all apps first (destroys windows, kills
|
||||||
|
// tasks, closes DXE handles), then tear down the task system and GUI
|
||||||
|
// in reverse initialization order.
|
||||||
shellTerminateAllApps(&sCtx);
|
shellTerminateAllApps(&sCtx);
|
||||||
|
|
||||||
tsShutdown();
|
tsShutdown();
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,18 @@
|
||||||
// Frame format (before byte stuffing):
|
// Frame format (before byte stuffing):
|
||||||
// [0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
|
// [0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
|
||||||
//
|
//
|
||||||
// Byte stuffing:
|
// The leading 0x7E is the frame flag. A trailing 0x7E closes the frame and
|
||||||
// 0x7E -> 0x7D 0x5E
|
// also serves as the flag for the next frame (back-to-back). SEQ is an 8-bit
|
||||||
// 0x7D -> 0x7D 0x5D
|
// sequence number that wraps naturally. TYPE identifies DATA/ACK/NAK/RST.
|
||||||
|
// LEN is the payload byte count (0-255). CRC covers SEQ through PAYLOAD.
|
||||||
//
|
//
|
||||||
// CRC-16-CCITT over SEQ+TYPE+LEN+PAYLOAD
|
// Byte stuffing (transparency):
|
||||||
|
// 0x7E -> 0x7D 0x5E (flag byte escaped)
|
||||||
|
// 0x7D -> 0x7D 0x5D (escape byte itself escaped)
|
||||||
|
// XOR with 0x20 is the standard HDLC transparency method.
|
||||||
|
//
|
||||||
|
// CRC-16-CCITT (polynomial 0x1021) over SEQ+TYPE+LEN+PAYLOAD.
|
||||||
|
// Initial value 0xFFFF. Stored little-endian in the frame.
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
@ -47,20 +54,29 @@
|
||||||
// Receive buffer must hold at least one max-size stuffed frame
|
// Receive buffer must hold at least one max-size stuffed frame
|
||||||
#define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64)
|
#define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64)
|
||||||
|
|
||||||
// Retransmit timeout in milliseconds
|
// Retransmit timeout. 500ms is conservative for a local serial link (RTT
|
||||||
|
// is < 1ms) but accounts for the remote side being busy. On a real BBS
|
||||||
|
// connection through the proxy, the round-trip includes TCP latency.
|
||||||
#define RETRANSMIT_TIMEOUT_MS 500
|
#define RETRANSMIT_TIMEOUT_MS 500
|
||||||
|
|
||||||
// Receive state machine
|
// Receive state machine: three states for HDLC deframing.
|
||||||
#define RX_STATE_HUNT 0 // scanning for FLAG_BYTE
|
// HUNT: discarding bytes until a flag (0x7E) is seen — sync acquisition.
|
||||||
#define RX_STATE_ACTIVE 1 // receiving frame data
|
// ACTIVE: accumulating frame bytes, watching for flag (end of frame) or
|
||||||
#define RX_STATE_ESCAPE 2 // next byte is escaped
|
// escape (next byte is XOR'd).
|
||||||
|
// ESCAPE: the previous byte was 0x7D; XOR this byte with 0x20 to recover
|
||||||
|
// the original value.
|
||||||
|
#define RX_STATE_HUNT 0
|
||||||
|
#define RX_STATE_ACTIVE 1
|
||||||
|
#define RX_STATE_ESCAPE 2
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Types
|
// Types
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Transmit window slot
|
// Transmit window slot: retains a copy of sent data so we can retransmit
|
||||||
|
// on NAK or timeout without the caller keeping its buffer alive. The timer
|
||||||
|
// tracks when this slot was last (re)transmitted for timeout detection.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t data[PKT_MAX_PAYLOAD];
|
uint8_t data[PKT_MAX_PAYLOAD];
|
||||||
int len;
|
int len;
|
||||||
|
|
@ -68,20 +84,23 @@ typedef struct {
|
||||||
clock_t timer;
|
clock_t timer;
|
||||||
} TxSlotT;
|
} TxSlotT;
|
||||||
|
|
||||||
// Connection state
|
// Connection state. txSlots is a circular window indexed by [0..txCount-1],
|
||||||
|
// where slot 0 is the oldest unacked frame (sequence txAckSeq) and
|
||||||
|
// slot txCount-1 is the newest. When an ACK advances txAckSeq, we shift
|
||||||
|
// slots down (implicitly, by incrementing txAckSeq and decrementing txCount).
|
||||||
struct PktConnS {
|
struct PktConnS {
|
||||||
int com;
|
int com;
|
||||||
int windowSize;
|
int windowSize;
|
||||||
PktRecvCallbackT callback;
|
PktRecvCallbackT callback;
|
||||||
void *callbackCtx;
|
void *callbackCtx;
|
||||||
|
|
||||||
// Transmit state
|
// Transmit state (Go-Back-N sender)
|
||||||
uint8_t txNextSeq; // next sequence number to assign
|
uint8_t txNextSeq; // next sequence number to assign
|
||||||
uint8_t txAckSeq; // oldest unacknowledged sequence
|
uint8_t txAckSeq; // oldest unacknowledged sequence
|
||||||
TxSlotT txSlots[PKT_MAX_WINDOW];
|
TxSlotT txSlots[PKT_MAX_WINDOW];
|
||||||
int txCount; // number of slots in use
|
int txCount; // number of slots in use
|
||||||
|
|
||||||
// Receive state
|
// Receive state (Go-Back-N receiver: only accepts in-order frames)
|
||||||
uint8_t rxExpectSeq; // next expected sequence number
|
uint8_t rxExpectSeq; // next expected sequence number
|
||||||
uint8_t rxState; // RX_STATE_*
|
uint8_t rxState; // RX_STATE_*
|
||||||
uint8_t rxFrame[MAX_FRAME_SIZE];
|
uint8_t rxFrame[MAX_FRAME_SIZE];
|
||||||
|
|
@ -150,6 +169,10 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq);
|
||||||
// CRC computation
|
// CRC computation
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Table-driven CRC-16-CCITT. Processing one byte per iteration with a
|
||||||
|
// 256-entry table is ~10x faster than bit-by-bit on a 486. The table
|
||||||
|
// costs 512 bytes of .rodata — a worthwhile trade for a function called
|
||||||
|
// on every frame received and transmitted.
|
||||||
static uint16_t crcCalc(const uint8_t *data, int len) {
|
static uint16_t crcCalc(const uint8_t *data, int len) {
|
||||||
uint16_t crc = 0xFFFF;
|
uint16_t crc = 0xFFFF;
|
||||||
|
|
||||||
|
|
@ -165,6 +188,10 @@ static uint16_t crcCalc(const uint8_t *data, int len) {
|
||||||
// Frame transmission
|
// Frame transmission
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Build a raw frame, compute CRC, byte-stuff it, and transmit.
|
||||||
|
// The stuffed buffer can be up to 2x the raw size (every byte might need
|
||||||
|
// escaping) plus the flags. This is stack-allocated because frames are small
|
||||||
|
// and we're not in a deeply recursive call path.
|
||||||
static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) {
|
static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) {
|
||||||
uint8_t raw[MAX_FRAME_SIZE];
|
uint8_t raw[MAX_FRAME_SIZE];
|
||||||
uint8_t stuffed[MAX_STUFFED_SIZE];
|
uint8_t stuffed[MAX_STUFFED_SIZE];
|
||||||
|
|
@ -229,6 +256,10 @@ static void sendRst(PktConnT *conn) {
|
||||||
// Sequence number helpers
|
// Sequence number helpers
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Check if a sequence number falls within a window starting at base.
|
||||||
|
// Works correctly with 8-bit wrap-around because unsigned subtraction
|
||||||
|
// wraps mod 256 — if seq is "ahead" of base by less than size, diff
|
||||||
|
// will be a small positive number.
|
||||||
static int seqInWindow(uint8_t seq, uint8_t base, int size) {
|
static int seqInWindow(uint8_t seq, uint8_t base, int size) {
|
||||||
uint8_t diff = seq - base;
|
uint8_t diff = seq - base;
|
||||||
return diff < (uint8_t)size;
|
return diff < (uint8_t)size;
|
||||||
|
|
@ -248,6 +279,18 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq) {
|
||||||
// Frame processing
|
// Frame processing
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Handle a complete, de-stuffed frame. CRC is verified first; on failure,
|
||||||
|
// we NAK to request retransmission of the frame we actually expected.
|
||||||
|
//
|
||||||
|
// For DATA frames: Go-Back-N receiver logic — only accept if seq matches
|
||||||
|
// rxExpectSeq (strictly in-order). Out-of-order frames within the window
|
||||||
|
// trigger a NAK; duplicates and out-of-window frames are silently dropped.
|
||||||
|
//
|
||||||
|
// For ACK frames: cumulative acknowledgement. The ACK carries the next
|
||||||
|
// expected sequence number, so we free all slots up to that point.
|
||||||
|
//
|
||||||
|
// For NAK frames: the receiver wants us to retransmit from a specific
|
||||||
|
// sequence. Go-Back-N retransmits that frame AND all subsequent ones.
|
||||||
static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
|
static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
|
||||||
uint8_t seq;
|
uint8_t seq;
|
||||||
uint8_t type;
|
uint8_t type;
|
||||||
|
|
@ -333,6 +376,12 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
|
||||||
// Receive byte processing (state machine)
|
// Receive byte processing (state machine)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Feed one byte from the serial port into the HDLC deframing state machine.
|
||||||
|
// The flag byte (0x7E) serves double duty: it ends the current frame AND
|
||||||
|
// starts the next one. This means back-to-back frames share a single flag
|
||||||
|
// byte, saving bandwidth. A frame is only processed if it meets the minimum
|
||||||
|
// size requirement (header + CRC), so spurious flags between frames are
|
||||||
|
// harmless (they just produce zero-length "frames" that are discarded).
|
||||||
static void rxProcessByte(PktConnT *conn, uint8_t byte) {
|
static void rxProcessByte(PktConnT *conn, uint8_t byte) {
|
||||||
switch (conn->rxState) {
|
switch (conn->rxState) {
|
||||||
case RX_STATE_HUNT:
|
case RX_STATE_HUNT:
|
||||||
|
|
@ -379,6 +428,11 @@ static void rxProcessByte(PktConnT *conn, uint8_t byte) {
|
||||||
// Retransmit check
|
// Retransmit check
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Timer-based retransmission for slots that haven't been ACK'd within the
|
||||||
|
// timeout. This handles the case where an ACK or NAK was lost — without
|
||||||
|
// this, the connection would stall forever. Each slot is retransmitted
|
||||||
|
// independently and its timer is reset, creating exponential backoff
|
||||||
|
// behavior naturally (each retransmit resets the timer).
|
||||||
static void retransmitCheck(PktConnT *conn) {
|
static void retransmitCheck(PktConnT *conn) {
|
||||||
clock_t now = clock();
|
clock_t now = clock();
|
||||||
clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
|
clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
|
||||||
|
|
@ -450,6 +504,12 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main polling function — must be called frequently (each iteration of the
|
||||||
|
// app's main loop or event loop). It drains the serial port's RX buffer,
|
||||||
|
// feeds bytes through the deframing state machine, and checks for
|
||||||
|
// retransmit timeouts. The callback is invoked synchronously for each
|
||||||
|
// complete, verified, in-order data frame, so the caller should be prepared
|
||||||
|
// for re-entrant calls to pktSend from within the callback.
|
||||||
int pktPoll(PktConnT *conn) {
|
int pktPoll(PktConnT *conn) {
|
||||||
char buf[128];
|
char buf[128];
|
||||||
int nRead;
|
int nRead;
|
||||||
|
|
@ -500,6 +560,11 @@ int pktReset(PktConnT *conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Send a data packet. If block=true and the window is full, polls in a
|
||||||
|
// tight loop until space opens up (an ACK arrives). The data is copied
|
||||||
|
// into a retransmit slot before sending so the caller can reuse its buffer
|
||||||
|
// immediately. The window slot tracks the sequence number and timestamp
|
||||||
|
// for retransmission.
|
||||||
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
|
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
|
||||||
TxSlotT *slot;
|
TxSlotT *slot;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,28 @@
|
||||||
// Packetized serial transport with HDLC-style framing and sliding window
|
// Packetized serial transport with HDLC-style framing and sliding window
|
||||||
// Provides reliable, ordered delivery over an unreliable serial link.
|
// Provides reliable, ordered delivery over an unreliable serial link.
|
||||||
|
//
|
||||||
|
// Why HDLC framing:
|
||||||
|
// HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit
|
||||||
|
// variable-length frames on a raw byte stream. The 0x7E flag byte marks
|
||||||
|
// frame boundaries; byte-stuffing (escaping 0x7E and 0x7D within data)
|
||||||
|
// ensures the flag is unambiguous. This is proven, lightweight, and
|
||||||
|
// requires zero buffering at the framing layer. Alternative approaches
|
||||||
|
// like length-prefixed framing are fragile on noisy links because a
|
||||||
|
// corrupted length field desynchronizes the receiver permanently.
|
||||||
|
//
|
||||||
|
// Why Go-Back-N ARQ:
|
||||||
|
// Go-Back-N is simpler than Selective Repeat (no out-of-order reassembly
|
||||||
|
// buffer needed on the receiver) and works well for the low bandwidth-delay
|
||||||
|
// product of a serial link. On a 115200 bps local serial connection, the
|
||||||
|
// round-trip time is negligible, so the window rarely fills. GBN's
|
||||||
|
// retransmit-all-from-NAK behavior wastes bandwidth on lossy links, but
|
||||||
|
// serial links are nearly lossless — the CRC check is primarily a safety
|
||||||
|
// net for electrical noise, not a routine error recovery mechanism.
|
||||||
|
//
|
||||||
|
// CRC-16-CCITT:
|
||||||
|
// Standard polynomial used in HDLC/X.25. Table-driven for speed
|
||||||
|
// (256-entry lookup table trades 512 bytes of ROM for ~10x faster CRC
|
||||||
|
// than the bit-by-bit method, important on a 486).
|
||||||
|
|
||||||
#ifndef PACKET_H
|
#ifndef PACKET_H
|
||||||
#define PACKET_H
|
#define PACKET_H
|
||||||
|
|
@ -7,13 +30,16 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
// Maximum payload per packet (excluding header/CRC)
|
// Maximum payload per packet (excluding header/CRC). 255 bytes fits in a
|
||||||
|
// single LEN byte and keeps total frame size manageable for serial buffers.
|
||||||
#define PKT_MAX_PAYLOAD 255
|
#define PKT_MAX_PAYLOAD 255
|
||||||
|
|
||||||
// Default sliding window size (1-8)
|
// Default sliding window size (1-8). 4 allows up to 4 unacknowledged
|
||||||
|
// frames in flight, providing good throughput without excessive buffering.
|
||||||
#define PKT_DEFAULT_WINDOW 4
|
#define PKT_DEFAULT_WINDOW 4
|
||||||
|
|
||||||
// Maximum window size
|
// Maximum window size. Limited to 8 because sequence numbers are 8-bit
|
||||||
|
// and we need seq space > 2*window to avoid ambiguity (255 > 2*8).
|
||||||
#define PKT_MAX_WINDOW 8
|
#define PKT_MAX_WINDOW 8
|
||||||
|
|
||||||
// Error codes
|
// Error codes
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,25 @@
|
||||||
// SecLink proxy — bridges an 86Box serial connection to a telnet BBS
|
// SecLink proxy — bridges an 86Box serial connection to a telnet BBS
|
||||||
//
|
//
|
||||||
|
// Architecture:
|
||||||
// 86Box (DOS terminal) ←→ TCP ←→ proxy ←→ TCP ←→ BBS
|
// 86Box (DOS terminal) ←→ TCP ←→ proxy ←→ TCP ←→ BBS
|
||||||
// secLink protocol plain telnet
|
// secLink protocol plain telnet
|
||||||
//
|
//
|
||||||
|
// The proxy runs on Linux and sits between two TCP connections:
|
||||||
|
// 1. Left side: 86Box connects via its serial-over-TCP feature. The proxy
|
||||||
|
// speaks the full secLink protocol (packet framing, DH handshake,
|
||||||
|
// XTEA encryption) over this connection.
|
||||||
|
// 2. Right side: plain telnet to a BBS. The proxy handles telnet IAC
|
||||||
|
// negotiation and strips control sequences before forwarding clean
|
||||||
|
// data to the DOS side.
|
||||||
|
//
|
||||||
|
// This design keeps all crypto complexity on the proxy side (Linux has
|
||||||
|
// /dev/urandom for good entropy) and inside the DOS app. The BBS sees
|
||||||
|
// a normal telnet client and doesn't know encryption is involved.
|
||||||
|
//
|
||||||
|
// The socket shim (sockShim.c) makes the TCP socket look like a COM port
|
||||||
|
// to the secLink stack, allowing full code reuse of the packet and
|
||||||
|
// security layers without modification.
|
||||||
|
//
|
||||||
// Usage: proxy [listen_port] [bbs_host] [bbs_port]
|
// Usage: proxy [listen_port] [bbs_host] [bbs_port]
|
||||||
// Defaults: 2323 10.1.0.244 2023
|
// Defaults: 2323 10.1.0.244 2023
|
||||||
|
|
||||||
|
|
@ -34,7 +51,9 @@
|
||||||
#define CHANNEL_TERMINAL 0
|
#define CHANNEL_TERMINAL 0
|
||||||
#define POLL_TIMEOUT_MS 10
|
#define POLL_TIMEOUT_MS 10
|
||||||
|
|
||||||
// Telnet protocol bytes
|
// Telnet protocol bytes (RFC 854). The proxy must handle these because BBS
|
||||||
|
// servers send telnet negotiation sequences that would corrupt the terminal
|
||||||
|
// display if passed through raw.
|
||||||
#define TEL_IAC 255
|
#define TEL_IAC 255
|
||||||
#define TEL_DONT 254
|
#define TEL_DONT 254
|
||||||
#define TEL_DO 253
|
#define TEL_DO 253
|
||||||
|
|
@ -49,7 +68,9 @@
|
||||||
#define TELOPT_TTYPE 24
|
#define TELOPT_TTYPE 24
|
||||||
#define TELOPT_NAWS 31
|
#define TELOPT_NAWS 31
|
||||||
|
|
||||||
// Telnet parser states
|
// Telnet parser states. The parser is a simple state machine that strips
|
||||||
|
// IAC sequences from the BBS data stream. It persists across read() calls
|
||||||
|
// (sTelState is static) because a sequence can span TCP segment boundaries.
|
||||||
#define TS_DATA 0
|
#define TS_DATA 0
|
||||||
#define TS_IAC 1
|
#define TS_IAC 1
|
||||||
#define TS_WILL 2
|
#define TS_WILL 2
|
||||||
|
|
@ -151,12 +172,14 @@ static int createListenSocket(int port) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// secLink receive callback — called when decrypted data arrives from the DOS
|
||||||
|
// terminal. Before the BBS is connected, we watch for an ENTER keypress as
|
||||||
|
// a "ready" signal so the user can see the terminal is working before we
|
||||||
|
// connect to the BBS (which would immediately start sending ANSI art).
|
||||||
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) {
|
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) {
|
||||||
int bbsFd = *(int *)ctx;
|
int bbsFd = *(int *)ctx;
|
||||||
|
|
||||||
(void)channel;
|
(void)channel;
|
||||||
|
|
||||||
// Check for ENTER before BBS is connected
|
|
||||||
if (!sGotEnter) {
|
if (!sGotEnter) {
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
if (data[i] == '\r' || data[i] == '\n') {
|
if (data[i] == '\r' || data[i] == '\n') {
|
||||||
|
|
@ -179,6 +202,9 @@ static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t chann
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// On Linux, /dev/urandom provides high-quality entropy without blocking.
|
||||||
|
// This is vastly better than the PIT-based entropy on DOS — one of the
|
||||||
|
// advantages of running the proxy on the host OS.
|
||||||
static void seedRng(void) {
|
static void seedRng(void) {
|
||||||
uint8_t entropy[32];
|
uint8_t entropy[32];
|
||||||
FILE *f;
|
FILE *f;
|
||||||
|
|
@ -216,8 +242,13 @@ static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Filter telnet IAC sequences from BBS data.
|
// Filter telnet IAC sequences from BBS data and handle negotiation.
|
||||||
// Handles negotiation by responding appropriately.
|
// We accept ECHO and SGA (Suppress Go Ahead) because most BBSes require them
|
||||||
|
// for character-at-a-time mode. We accept TTYPE and NAWS for terminal type
|
||||||
|
// and window size negotiation. Everything else is refused. Subnegotiations
|
||||||
|
// (SB...SE) are consumed silently — we don't actually respond with terminal
|
||||||
|
// type or window size data, but accepting the option prevents the BBS from
|
||||||
|
// falling back to line mode.
|
||||||
// Returns the number of clean data bytes written to 'out'.
|
// Returns the number of clean data bytes written to 'out'.
|
||||||
static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
|
static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
|
||||||
int outLen = 0;
|
int outLen = 0;
|
||||||
|
|
@ -351,6 +382,8 @@ int main(int argc, char *argv[]) {
|
||||||
bbsPort = atoi(argv[3]);
|
bbsPort = atoi(argv[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore SIGPIPE so write() to a closed socket returns EPIPE instead of
|
||||||
|
// killing the process. Essential for any TCP server/proxy.
|
||||||
signal(SIGPIPE, SIG_IGN);
|
signal(SIGPIPE, SIG_IGN);
|
||||||
struct sigaction sa;
|
struct sigaction sa;
|
||||||
sa.sa_handler = sigHandler;
|
sa.sa_handler = sigHandler;
|
||||||
|
|
@ -387,7 +420,9 @@ int main(int argc, char *argv[]) {
|
||||||
printf("86Box connected.\n");
|
printf("86Box connected.\n");
|
||||||
sClientFd = clientFd;
|
sClientFd = clientFd;
|
||||||
|
|
||||||
// Associate socket with COM0 for the secLink stack
|
// Map the TCP socket to "COM0" so the secLink stack (which calls rs232Read/
|
||||||
|
// rs232Write) transparently operates over the TCP connection instead of
|
||||||
|
// a real UART. The sockShim intercepts rs232 calls and redirects to socket I/O.
|
||||||
sockShimSetFd(0, clientFd);
|
sockShimSetFd(0, clientFd);
|
||||||
|
|
||||||
// Seed RNG from /dev/urandom and open secLink
|
// Seed RNG from /dev/urandom and open secLink
|
||||||
|
|
@ -442,7 +477,10 @@ int main(int argc, char *argv[]) {
|
||||||
int flags = fcntl(bbsFd, F_GETFL, 0);
|
int flags = fcntl(bbsFd, F_GETFL, 0);
|
||||||
fcntl(bbsFd, F_SETFL, flags | O_NONBLOCK);
|
fcntl(bbsFd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
|
||||||
// Main proxy loop
|
// Main proxy loop: poll both sockets with a short timeout so secLinkPoll
|
||||||
|
// runs frequently enough for ACK processing and retransmits. Data flows:
|
||||||
|
// 86Box -> secLink (decrypt) -> callback -> write to BBS
|
||||||
|
// BBS -> read -> telnet filter -> secLink (encrypt) -> 86Box
|
||||||
fds[0].fd = clientFd;
|
fds[0].fd = clientFd;
|
||||||
fds[0].events = POLLIN;
|
fds[0].events = POLLIN;
|
||||||
fds[1].fd = bbsFd;
|
fds[1].fd = bbsFd;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
// Socket shim — rs232-compatible API over TCP sockets
|
// Socket shim — rs232-compatible API over TCP sockets
|
||||||
|
//
|
||||||
|
// Maps up to 4 "COM ports" to TCP socket file descriptors. Reads use
|
||||||
|
// MSG_DONTWAIT (non-blocking) to match rs232Read's non-blocking semantics.
|
||||||
|
// Writes are blocking (loop until all bytes sent) to match rs232Write's
|
||||||
|
// guarantee of complete delivery.
|
||||||
|
//
|
||||||
|
// The shim does NOT close sockets in rs232Close — socket lifecycle is
|
||||||
|
// managed by the proxy's main() function. This avoids double-close bugs
|
||||||
|
// when secLinkClose calls rs232Close but the proxy still needs the fd.
|
||||||
|
|
||||||
#include "sockShim.h"
|
#include "sockShim.h"
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
|
@ -32,6 +41,8 @@ int rs232Close(int com) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Serial parameters are ignored — TCP handles framing, flow control, and
|
||||||
|
// error correction. We just validate that a socket has been assigned.
|
||||||
int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
|
int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
|
||||||
(void)bps;
|
(void)bps;
|
||||||
(void)dataBits;
|
(void)dataBits;
|
||||||
|
|
@ -51,6 +62,10 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Non-blocking read matching rs232Read semantics:
|
||||||
|
// > 0: bytes read
|
||||||
|
// 0: no data available (EAGAIN)
|
||||||
|
// -1: error or connection closed (maps to PKT_ERR_DISCONNECTED in pktPoll)
|
||||||
int rs232Read(int com, char *data, int len) {
|
int rs232Read(int com, char *data, int len) {
|
||||||
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
|
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -64,6 +79,7 @@ int rs232Read(int com, char *data, int len) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (n == 0) {
|
if (n == 0) {
|
||||||
|
// TCP FIN — peer closed connection
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +87,9 @@ int rs232Read(int com, char *data, int len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Blocking write: loops until all bytes are sent. MSG_NOSIGNAL prevents
|
||||||
|
// SIGPIPE on a closed connection (returns EPIPE instead). This matches
|
||||||
|
// rs232Write's blocking-until-complete behavior.
|
||||||
int rs232Write(int com, const char *data, int len) {
|
int rs232Write(int com, const char *data, int len) {
|
||||||
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
|
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
|
||||||
return RS232_ERR_NOT_OPEN;
|
return RS232_ERR_NOT_OPEN;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,21 @@
|
||||||
// Socket shim — provides rs232-compatible API backed by TCP sockets
|
// Socket shim — provides rs232-compatible API backed by TCP sockets
|
||||||
// Used by the Linux proxy to reuse the packet and secLink layers.
|
// Used by the Linux proxy to reuse the packet and secLink layers.
|
||||||
|
//
|
||||||
|
// Design: the packet and secLink layers call rs232Read/rs232Write assuming
|
||||||
|
// a real UART underneath. By providing a socket-backed implementation with
|
||||||
|
// the same function signatures, we can reuse the entire protocol stack
|
||||||
|
// on Linux without any #ifdefs in the packet or secLink code.
|
||||||
|
//
|
||||||
|
// The #define RS232_H below is intentional: it prevents the real rs232.h
|
||||||
|
// from being included if someone includes both headers. The shim only
|
||||||
|
// implements the subset of rs232 functions that packet and secLink use
|
||||||
|
// (open, close, read, write). Hardware-specific functions (IRQ, FIFO,
|
||||||
|
// flow control) are not needed and not provided.
|
||||||
|
|
||||||
#ifndef SOCKSHIM_H
|
#ifndef SOCKSHIM_H
|
||||||
#define SOCKSHIM_H
|
#define SOCKSHIM_H
|
||||||
|
|
||||||
// Block the real rs232.h from being included
|
// Prevent the real rs232.h from being included alongside this shim
|
||||||
#define RS232_H
|
#define RS232_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,16 @@
|
||||||
// Defines and macros
|
// Defines and macros
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Port I/O helpers (outportb returns void in DJGPP, but macros need the value)
|
// Port I/O helpers that return the written value. outportb/outportw are void
|
||||||
|
// in DJGPP, but our macros (UART_WRITE_IER, etc.) use comma expressions to
|
||||||
|
// cache the written value in the port state struct for later reads without
|
||||||
|
// hitting the hardware again.
|
||||||
#define OUTP(a, b) (outportb((a), (b)), (b))
|
#define OUTP(a, b) (outportb((a), (b)), (b))
|
||||||
#define OUTPW(a, w) (outportw((a), (w)), (w))
|
#define OUTPW(a, w) (outportw((a), (w)), (w))
|
||||||
|
|
||||||
// Buffer sizes (power of 2)
|
// Buffer sizes must be power of 2 so (index & MASK) wraps correctly.
|
||||||
|
// 2048 bytes balances memory usage (8KB per port for both buffers) against
|
||||||
|
// burst tolerance at 115200 bps.
|
||||||
#define RX_BUFFER_BITS 11 // 2048
|
#define RX_BUFFER_BITS 11 // 2048
|
||||||
#define TX_BUFFER_BITS 11 // 2048
|
#define TX_BUFFER_BITS 11 // 2048
|
||||||
#define RX_BUFFER_SIZE (1L << RX_BUFFER_BITS)
|
#define RX_BUFFER_SIZE (1L << RX_BUFFER_BITS)
|
||||||
|
|
@ -36,7 +41,10 @@
|
||||||
#define TX_HIGH_WATER (TX_BUFFER_SIZE * TX_HIGH_PERCENT / 100UL)
|
#define TX_HIGH_WATER (TX_BUFFER_SIZE * TX_HIGH_PERCENT / 100UL)
|
||||||
#define TX_LOW_WATER (TX_BUFFER_SIZE * TX_LOW_PERCENT / 100UL)
|
#define TX_LOW_WATER (TX_BUFFER_SIZE * TX_LOW_PERCENT / 100UL)
|
||||||
|
|
||||||
// RX buffer operations
|
// Ring buffer macros. These advance head/tail BEFORE accessing the slot
|
||||||
|
// (pre-increment style). The +1-before-mask pattern means slot 0 is never
|
||||||
|
// used and the full/empty distinction works: head==tail means empty,
|
||||||
|
// (head+1)&mask==tail means full (one slot wasted to distinguish states).
|
||||||
#define RX_READ(C) (C)->rxBuff[(C)->rxTail = ((C)->rxTail + 1) & RX_BUFFER_MASK]
|
#define RX_READ(C) (C)->rxBuff[(C)->rxTail = ((C)->rxTail + 1) & RX_BUFFER_MASK]
|
||||||
#define RX_WRITE(C, D) (C)->rxBuff[(C)->rxHead = ((C)->rxHead + 1) & RX_BUFFER_MASK] = (D)
|
#define RX_WRITE(C, D) (C)->rxBuff[(C)->rxHead = ((C)->rxHead + 1) & RX_BUFFER_MASK] = (D)
|
||||||
#define RX_INIT(C) (C)->rxHead = (C)->rxTail = 0
|
#define RX_INIT(C) (C)->rxHead = (C)->rxTail = 0
|
||||||
|
|
@ -306,6 +314,20 @@ static void removeIrqHandler(int irq);
|
||||||
// Interrupt service routine
|
// Interrupt service routine
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Single shared ISR for all COM ports. ISR sharing is necessary because
|
||||||
|
// COM1/COM3 typically share IRQ4 and COM2/COM4 share IRQ3. Having one ISR
|
||||||
|
// that polls all ports avoids the complexity of per-IRQ handlers.
|
||||||
|
//
|
||||||
|
// The ISR design follows a careful protocol:
|
||||||
|
// 1. Mask all COM IRQs on the PIC to prevent re-entry
|
||||||
|
// 2. STI to allow higher-priority interrupts (timer, keyboard) through
|
||||||
|
// 3. Loop over all open ports, draining each UART's pending interrupts
|
||||||
|
// 4. CLI, send EOI to PIC, re-enable COM IRQs, STI before IRET
|
||||||
|
//
|
||||||
|
// This "mask-then-STI" pattern is standard for slow device ISRs on PC
|
||||||
|
// hardware — it prevents the same IRQ from re-entering while still allowing
|
||||||
|
// the timer tick and keyboard to function during potentially long UART
|
||||||
|
// processing.
|
||||||
static void comGeneralIsr(void) {
|
static void comGeneralIsr(void) {
|
||||||
Rs232StateT *comMin = &sComPorts[0];
|
Rs232StateT *comMin = &sComPorts[0];
|
||||||
Rs232StateT *comMax = &sComPorts[COM_MAX];
|
Rs232StateT *comMax = &sComPorts[COM_MAX];
|
||||||
|
|
@ -313,9 +335,6 @@ static void comGeneralIsr(void) {
|
||||||
uint8_t data;
|
uint8_t data;
|
||||||
uint8_t slaveTriggered = 0;
|
uint8_t slaveTriggered = 0;
|
||||||
Rs232StateT *com;
|
Rs232StateT *com;
|
||||||
|
|
||||||
// Disable IRQs for all open COM ports, then re-enable CPU interrupts
|
|
||||||
// so other faster devices can be serviced
|
|
||||||
for (com = comMin; com <= comMax; com++) {
|
for (com = comMin; com <= comMax; com++) {
|
||||||
if (com->isOpen) {
|
if (com->isOpen) {
|
||||||
if (com->irq > 7) {
|
if (com->irq > 7) {
|
||||||
|
|
@ -385,6 +404,9 @@ static void comGeneralIsr(void) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IIR_TX_HOLD_EMPTY: {
|
case IIR_TX_HOLD_EMPTY: {
|
||||||
|
// Drain up to FIFO_SIZE bytes from TX ring buffer to UART.
|
||||||
|
// Writing more than one byte is only safe on 16550A+ (has FIFO);
|
||||||
|
// on 8250/16450 FIFO_SIZE effectively limits to 1 byte anyway.
|
||||||
int cnt;
|
int cnt;
|
||||||
for (cnt = 0; cnt < FIFO_SIZE && com->txFlowOn && !TX_EMPTY(com); cnt++) {
|
for (cnt = 0; cnt < FIFO_SIZE && com->txFlowOn && !TX_EMPTY(com); cnt++) {
|
||||||
UART_WRITE_DATA(com, TX_READ(com));
|
UART_WRITE_DATA(com, TX_READ(com));
|
||||||
|
|
@ -414,8 +436,9 @@ static void comGeneralIsr(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must explicitly STI before IRET because IRET won't always
|
// Must explicitly STI before IRET because under DPMI (and some
|
||||||
// restore interrupts in a virtual environment
|
// virtualizers like DOSBox/86Box), IRET's implicit IF restore from
|
||||||
|
// the flags on the stack doesn't always work as expected.
|
||||||
asm("STI");
|
asm("STI");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,11 +447,20 @@ static void comGeneralIsr(void) {
|
||||||
// DPMI utility functions
|
// DPMI utility functions
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// These functions wrap DJGPP's DPMI interface for installing protected-mode
|
||||||
|
// interrupt handlers. Under DPMI, the ISR code and data must be locked in
|
||||||
|
// physical memory to prevent page faults during interrupt handling — a page
|
||||||
|
// fault inside an ISR would be fatal. The IRET wrapper is also allocated
|
||||||
|
// by DPMI to handle the real-mode-to-protected-mode transition.
|
||||||
|
|
||||||
static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info) {
|
static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info) {
|
||||||
_go32_dpmi_get_protected_mode_interrupt_vector(vector, info);
|
_go32_dpmi_get_protected_mode_interrupt_vector(vector, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Lock the ISR code and its data (sComPorts array) in physical memory.
|
||||||
|
// The IRET wrapper handles the stack frame that DPMI uses to dispatch
|
||||||
|
// hardware interrupts to protected-mode code.
|
||||||
static int dpmiLockMemory(void) {
|
static int dpmiLockMemory(void) {
|
||||||
unsigned long dataAddr;
|
unsigned long dataAddr;
|
||||||
unsigned long codeAddr;
|
unsigned long codeAddr;
|
||||||
|
|
@ -488,6 +520,12 @@ static void dpmiUnlockMemory(void) {
|
||||||
// IRQ management
|
// IRQ management
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Auto-detect which IRQ a UART is wired to by generating a TX hold empty
|
||||||
|
// interrupt and reading the PIC's IRR (Interrupt Request Register) to see
|
||||||
|
// which line went high. Uses a double-check pattern: enable interrupt,
|
||||||
|
// read IRR, disable, read IRR again, mask out persistent bits, then
|
||||||
|
// enable once more to confirm. Falls back to the default IRQ if detection
|
||||||
|
// fails (e.g., on virtualized hardware that doesn't model IRR accurately).
|
||||||
static int findIrq(int comport) {
|
static int findIrq(int comport) {
|
||||||
Rs232StateT *com = &sComPorts[comport];
|
Rs232StateT *com = &sComPorts[comport];
|
||||||
uint8_t imrM = PIC_READ_IMR(PIC_MASTER);
|
uint8_t imrM = PIC_READ_IMR(PIC_MASTER);
|
||||||
|
|
@ -564,6 +602,9 @@ static int findIrq(int comport) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Release this port's IRQ. If another port shares the same IRQ line,
|
||||||
|
// the handler stays installed — only the last user removes it. This is
|
||||||
|
// critical for COM1/COM3 IRQ sharing.
|
||||||
static void freeIrq(int comport) {
|
static void freeIrq(int comport) {
|
||||||
Rs232StateT *com = &sComPorts[comport];
|
Rs232StateT *com = &sComPorts[comport];
|
||||||
Rs232StateT *comMin = &sComPorts[0];
|
Rs232StateT *comMin = &sComPorts[0];
|
||||||
|
|
@ -956,6 +997,11 @@ int rs232GetTxBuffered(int com) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Detect UART type by probing hardware features. The detection sequence:
|
||||||
|
// 1. Write/read scratch register — 8250 doesn't have one
|
||||||
|
// 2. Enable FIFO and check IIR bits 7:6 — distinguishes 16450/16550/16550A
|
||||||
|
// This matters because only 16550A has a reliable 16-byte FIFO; the
|
||||||
|
// original 16550 FIFO is buggy and should not be enabled.
|
||||||
int rs232GetUartType(int com) {
|
int rs232GetUartType(int com) {
|
||||||
Rs232StateT *port = &sComPorts[com];
|
Rs232StateT *port = &sComPorts[com];
|
||||||
uint8_t scratch;
|
uint8_t scratch;
|
||||||
|
|
@ -1016,7 +1062,10 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
|
||||||
RX_INIT(port);
|
RX_INIT(port);
|
||||||
TX_INIT(port);
|
TX_INIT(port);
|
||||||
|
|
||||||
// Read COM base address from BIOS data area (0040:0000-0040:0007)
|
// Read COM base address from BIOS data area at 0040:0000. The BIOS
|
||||||
|
// stores up to 4 COM port base addresses as 16-bit words at 40:00-40:07.
|
||||||
|
// This is more reliable than hardcoding 0x3F8/0x2F8 because BIOS setup
|
||||||
|
// may have remapped ports or detected them in a different order.
|
||||||
if (rs232SetBase(com, _farpeekw(_dos_ds, 0x400 + (com << 1))) != RS232_SUCCESS) {
|
if (rs232SetBase(com, _farpeekw(_dos_ds, 0x400 + (com << 1))) != RS232_SUCCESS) {
|
||||||
return RS232_ERR_NO_UART;
|
return RS232_ERR_NO_UART;
|
||||||
}
|
}
|
||||||
|
|
@ -1369,6 +1418,12 @@ int rs232SetStop(int com, int stopBits) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Blocking polled write: busy-waits on LSR_TX_HOLD_EMPTY for each byte.
|
||||||
|
// This bypasses the TX ring buffer entirely and writes directly to the UART.
|
||||||
|
// Used by the packet layer for frame transmission where we want guaranteed
|
||||||
|
// delivery order and don't want ISR-driven TX reordering complications.
|
||||||
|
// The trade-off is CPU burn during the wait, but at 115200 bps each byte
|
||||||
|
// takes only ~87us, and frame sizes are small (< 520 bytes worst case).
|
||||||
int rs232Write(int com, const char *data, int len) {
|
int rs232Write(int com, const char *data, int len) {
|
||||||
Rs232StateT *port = &sComPorts[com];
|
Rs232StateT *port = &sComPorts[com];
|
||||||
int i;
|
int i;
|
||||||
|
|
@ -1398,6 +1453,10 @@ int rs232Write(int com, const char *data, int len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Non-blocking buffered write: pushes data into the TX ring buffer and
|
||||||
|
// enables the TX_HOLD_EMPTY interrupt so the ISR drains it asynchronously.
|
||||||
|
// Returns the number of bytes actually buffered (may be less than len if
|
||||||
|
// the buffer is full). This is the ISR-driven counterpart to rs232Write.
|
||||||
int rs232WriteBuf(int com, const char *data, int len) {
|
int rs232WriteBuf(int com, const char *data, int len) {
|
||||||
Rs232StateT *port = &sComPorts[com];
|
Rs232StateT *port = &sComPorts[com];
|
||||||
int i;
|
int i;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,25 @@
|
||||||
//
|
//
|
||||||
// ISR-driven UART communication with ring buffers and flow control.
|
// ISR-driven UART communication with ring buffers and flow control.
|
||||||
// Supports up to 4 simultaneous COM ports with auto-detected IRQ.
|
// Supports up to 4 simultaneous COM ports with auto-detected IRQ.
|
||||||
|
//
|
||||||
|
// Architecture:
|
||||||
|
// A single ISR (comGeneralIsr) handles all open COM ports. When any
|
||||||
|
// UART interrupts, the ISR loops over all open ports draining their
|
||||||
|
// FIFOs into per-port ring buffers. The application reads from these
|
||||||
|
// buffers at its leisure. This decouples the UART's byte-at-a-time
|
||||||
|
// pace from application-level processing.
|
||||||
|
//
|
||||||
|
// Ring buffers are power-of-2 sized (2048 bytes) so head/tail index
|
||||||
|
// wrapping is a single AND, not a modulo — critical for ISR speed.
|
||||||
|
//
|
||||||
|
// Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates entirely within
|
||||||
|
// the ISR using watermark thresholds. When the RX buffer is 80% full,
|
||||||
|
// we signal the remote to stop; at 20%, we allow it to resume. This
|
||||||
|
// prevents buffer overflow without application involvement.
|
||||||
|
//
|
||||||
|
// The ISR and its data structures are locked in memory via DPMI to
|
||||||
|
// prevent page faults during interrupt handling — a requirement for
|
||||||
|
// any ISR running under a DPMI host (DOS extender, Windows, etc.).
|
||||||
|
|
||||||
#ifndef RS232_H
|
#ifndef RS232_H
|
||||||
#define RS232_H
|
#define RS232_H
|
||||||
|
|
@ -22,7 +41,11 @@
|
||||||
#define RS232_HANDSHAKE_RTSCTS 2
|
#define RS232_HANDSHAKE_RTSCTS 2
|
||||||
#define RS232_HANDSHAKE_DTRDSR 3
|
#define RS232_HANDSHAKE_DTRDSR 3
|
||||||
|
|
||||||
// UART Types
|
// UART Types (detected by probing scratch register and FIFO capability)
|
||||||
|
// 8250: No scratch register, no FIFO (original IBM PC)
|
||||||
|
// 16450: Has scratch register, no FIFO
|
||||||
|
// 16550: Has FIFO but it's buggy (rare)
|
||||||
|
// 16550A: Has working 16-byte FIFO (most common in 486+ era hardware)
|
||||||
#define RS232_UART_UNKNOWN 0
|
#define RS232_UART_UNKNOWN 0
|
||||||
#define RS232_UART_8250 1
|
#define RS232_UART_8250 1
|
||||||
#define RS232_UART_16450 2
|
#define RS232_UART_16450 2
|
||||||
|
|
@ -77,6 +100,9 @@ int rs232GetTxBuffered(int com);
|
||||||
int rs232GetUartType(int com);
|
int rs232GetUartType(int com);
|
||||||
|
|
||||||
// Read/write
|
// Read/write
|
||||||
|
// rs232Read: non-blocking drain from RX ring buffer, returns bytes read
|
||||||
|
// rs232Write: blocking polled write directly to UART THR (bypasses TX buffer)
|
||||||
|
// rs232WriteBuf: non-blocking write to TX ring buffer, ISR drains to UART
|
||||||
int rs232Read(int com, char *data, int len);
|
int rs232Read(int com, char *data, int len);
|
||||||
int rs232Write(int com, const char *data, int len);
|
int rs232Write(int com, const char *data, int len);
|
||||||
int rs232WriteBuf(int com, const char *data, int len);
|
int rs232WriteBuf(int com, const char *data, int len);
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,17 @@
|
||||||
// 3. Derive separate TX/RX cipher keys based on public key ordering
|
// 3. Derive separate TX/RX cipher keys based on public key ordering
|
||||||
// 4. Transition to READY — all subsequent encrypted packets use the keys
|
// 4. Transition to READY — all subsequent encrypted packets use the keys
|
||||||
//
|
//
|
||||||
|
// The handshake uses the packet layer's reliable delivery, so lost packets
|
||||||
|
// are automatically retransmitted. Both sides can send their public key
|
||||||
|
// simultaneously — there's no initiator/responder distinction.
|
||||||
|
//
|
||||||
// Directionality: the side with the lexicographically lower public key
|
// Directionality: the side with the lexicographically lower public key
|
||||||
// uses master XOR 0xAA for TX and master XOR 0x55 for RX. The other
|
// uses master XOR 0xAA for TX and master XOR 0x55 for RX. The other
|
||||||
// side uses the reverse. This prevents CTR counter collisions.
|
// side uses the reverse. This is critical: if both sides used the same
|
||||||
|
// key and counter for CTR mode, they'd produce identical keystreams,
|
||||||
|
// and XOR'ing two ciphertexts would reveal the XOR of the plaintexts.
|
||||||
|
// The XOR-derived directional keys ensure each direction has a unique
|
||||||
|
// keystream even though both start their counters at zero.
|
||||||
//
|
//
|
||||||
// Channel header byte: bit 7 = encrypted, bits 6..0 = channel (0-127)
|
// Channel header byte: bit 7 = encrypted, bits 6..0 = channel (0-127)
|
||||||
|
|
||||||
|
|
@ -68,6 +76,11 @@ static void internalRecv(void *ctx, const uint8_t *data, int len);
|
||||||
// Static functions (alphabetical)
|
// Static functions (alphabetical)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Called when we've received the remote's public key. Computes the DH
|
||||||
|
// shared secret, derives directional cipher keys, and transitions to READY.
|
||||||
|
// After this, the DH context (containing the private key) is destroyed
|
||||||
|
// immediately — forward secrecy principle: even if the long-term state is
|
||||||
|
// compromised later, past session keys can't be recovered.
|
||||||
static void completeHandshake(SecLinkT *link) {
|
static void completeHandshake(SecLinkT *link) {
|
||||||
uint8_t masterKey[SEC_XTEA_KEY_SIZE];
|
uint8_t masterKey[SEC_XTEA_KEY_SIZE];
|
||||||
uint8_t txKey[SEC_XTEA_KEY_SIZE];
|
uint8_t txKey[SEC_XTEA_KEY_SIZE];
|
||||||
|
|
@ -98,6 +111,12 @@ static void completeHandshake(SecLinkT *link) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Internal packet-layer callback. Routes incoming packets based on state:
|
||||||
|
// - During handshake: expects a 128-byte DH public key
|
||||||
|
// - When ready: strips the channel header, decrypts if flagged, and
|
||||||
|
// forwards plaintext to the user callback
|
||||||
|
// The channel header is always one byte, so the minimum valid data packet
|
||||||
|
// is 1 byte (header only, zero-length payload).
|
||||||
static void internalRecv(void *ctx, const uint8_t *data, int len) {
|
static void internalRecv(void *ctx, const uint8_t *data, int len) {
|
||||||
SecLinkT *link = (SecLinkT *)ctx;
|
SecLinkT *link = (SecLinkT *)ctx;
|
||||||
|
|
||||||
|
|
@ -287,8 +306,10 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
|
||||||
return SECLINK_ERR_PARAM;
|
return SECLINK_ERR_PARAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-blocking sends, check window space BEFORE encrypting
|
// CRITICAL: check window space BEFORE encrypting. If we encrypted first
|
||||||
// to avoid advancing the cipher counter on a failed send
|
// and then the send failed, the cipher counter would have advanced but
|
||||||
|
// the data wouldn't have been sent, permanently desynchronizing the
|
||||||
|
// TX cipher state from the remote's RX cipher state.
|
||||||
if (!block && !pktCanSend(link->pkt)) {
|
if (!block && !pktCanSend(link->pkt)) {
|
||||||
return SECLINK_ERR_SEND;
|
return SECLINK_ERR_SEND;
|
||||||
}
|
}
|
||||||
|
|
@ -316,6 +337,11 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Convenience function for sending large buffers. Splits the data into
|
||||||
|
// SECLINK_MAX_PAYLOAD (254 byte) chunks and sends each one, blocking until
|
||||||
|
// the send window has room. The receiver sees multiple packets on the same
|
||||||
|
// channel and must reassemble if needed. Always blocking because the caller
|
||||||
|
// expects the entire buffer to be sent when this returns.
|
||||||
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) {
|
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) {
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
// Secure serial link — convenience wrapper tying rs232 + packet + security
|
// Secure serial link — convenience wrapper tying rs232 + packet + security
|
||||||
//
|
//
|
||||||
|
// This is the top-level API for the serial/networking stack. It composes
|
||||||
|
// three layers into one:
|
||||||
|
// rs232 — ISR-driven UART I/O with ring buffers
|
||||||
|
// packet — HDLC framing + CRC-16 + Go-Back-N ARQ (reliable delivery)
|
||||||
|
// security — DH key exchange + XTEA-CTR encryption
|
||||||
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
// 1. secLinkOpen() — opens COM port, sets up packet framing
|
// 1. secLinkOpen() — opens COM port, sets up packet framing
|
||||||
// 2. secLinkHandshake() — DH key exchange (blocks until both sides complete)
|
// 2. secLinkHandshake() — DH key exchange (blocks until both sides complete)
|
||||||
|
|
@ -7,9 +13,17 @@
|
||||||
// 4. secLinkPoll() — receive, decrypt if needed, deliver to callback
|
// 4. secLinkPoll() — receive, decrypt if needed, deliver to callback
|
||||||
// 5. secLinkClose() — tear everything down
|
// 5. secLinkClose() — tear everything down
|
||||||
//
|
//
|
||||||
|
// Channel multiplexing:
|
||||||
// Each packet carries a one-byte header: bit 7 = encrypted flag,
|
// Each packet carries a one-byte header: bit 7 = encrypted flag,
|
||||||
// bits 6..0 = channel number (0-127). The callback receives plaintext
|
// bits 6..0 = channel number (0-127). This allows multiple logical streams
|
||||||
// regardless of whether encryption was used.
|
// (e.g., terminal data, file transfer, control messages) over a single
|
||||||
|
// serial link without needing separate framing or sequencing per stream.
|
||||||
|
// The callback receives plaintext regardless of whether encryption was used.
|
||||||
|
//
|
||||||
|
// Mixed clear/encrypted traffic:
|
||||||
|
// Unencrypted packets can be sent before or after the handshake. This
|
||||||
|
// allows a startup protocol (e.g., version negotiation) before keys are
|
||||||
|
// exchanged. Encrypted packets require a completed handshake.
|
||||||
|
|
||||||
#ifndef SECLINK_H
|
#ifndef SECLINK_H
|
||||||
#define SECLINK_H
|
#define SECLINK_H
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,13 @@ static const BigNumT sDhPrime = { .w = {
|
||||||
// Generator g = 2
|
// Generator g = 2
|
||||||
static const BigNumT sDhGenerator = { .w = { 2 } };
|
static const BigNumT sDhGenerator = { .w = { 2 } };
|
||||||
|
|
||||||
// Montgomery constants (computed lazily)
|
// Montgomery constants (computed lazily on first DH operation).
|
||||||
static BigNumT sDhR2; // R^2 mod p
|
// These are expensive to compute (~2048 shift-and-subtract operations for R2)
|
||||||
static uint32_t sDhM0Inv; // -p[0]^(-1) mod 2^32
|
// but only need to be done once since we always use the same prime.
|
||||||
|
// R = 2^1024 in Montgomery arithmetic; R^2 mod p is the conversion factor.
|
||||||
|
// m0inv = -p[0]^(-1) mod 2^32 is the Montgomery reduction constant.
|
||||||
|
static BigNumT sDhR2;
|
||||||
|
static uint32_t sDhM0Inv;
|
||||||
static bool sDhInited = false;
|
static bool sDhInited = false;
|
||||||
|
|
||||||
// RNG state
|
// RNG state
|
||||||
|
|
@ -185,6 +189,18 @@ static void bnFromBytes(BigNumT *a, const uint8_t *buf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Modular exponentiation using Montgomery multiplication.
|
||||||
|
//
|
||||||
|
// Montgomery multiplication replaces expensive modular reduction (division
|
||||||
|
// by a 1024-bit number) with cheaper additions and right-shifts, at the
|
||||||
|
// cost of converting operands to/from "Montgomery form" (multiply by R mod m).
|
||||||
|
// For exponentiation where we do hundreds of multiplications with the same
|
||||||
|
// modulus, the conversion cost is amortized and the net speedup is ~3-5x
|
||||||
|
// over schoolbook multiply-then-reduce on a 486.
|
||||||
|
//
|
||||||
|
// Uses left-to-right binary (square-and-multiply) scanning of the exponent.
|
||||||
|
// For a 256-bit private exponent, this is ~256 squarings + ~128 multiplies
|
||||||
|
// on average (half the bits are 1).
|
||||||
static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp, const BigNumT *mod, uint32_t m0inv, const BigNumT *r2) {
|
static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp, const BigNumT *mod, uint32_t m0inv, const BigNumT *r2) {
|
||||||
BigNumT montBase;
|
BigNumT montBase;
|
||||||
BigNumT montResult;
|
BigNumT montResult;
|
||||||
|
|
@ -224,6 +240,19 @@ static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp, c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Montgomery multiplication: computes (a * b * R^(-1)) mod m without division.
|
||||||
|
//
|
||||||
|
// The algorithm processes one word of 'a' per outer iteration (32 words for
|
||||||
|
// 1024-bit numbers). For each word:
|
||||||
|
// 1. Accumulate a[i] * b into the temporary product t
|
||||||
|
// 2. Compute the Montgomery reduction factor u = t[0] * m0inv (mod 2^32)
|
||||||
|
// 3. Add u * mod to t and shift right by 32 bits (the division by 2^32)
|
||||||
|
//
|
||||||
|
// The shift-and-reduce avoids explicit modular reduction. After all 32
|
||||||
|
// iterations, the result is in [0, 2m), so a single conditional subtraction
|
||||||
|
// brings it into [0, m). This is the CIOS (Coarsely Integrated Operand
|
||||||
|
// Scanning) variant, which is cache-friendly because it accesses 'b' and
|
||||||
|
// 'mod' sequentially in the inner loops.
|
||||||
static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b, const BigNumT *mod, uint32_t m0inv) {
|
static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b, const BigNumT *mod, uint32_t m0inv) {
|
||||||
uint32_t t[BN_WORDS + 1];
|
uint32_t t[BN_WORDS + 1];
|
||||||
uint32_t u;
|
uint32_t u;
|
||||||
|
|
@ -319,9 +348,12 @@ static void bnToBytes(uint8_t *buf, const BigNumT *a) {
|
||||||
// Helper functions (alphabetical)
|
// Helper functions (alphabetical)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Compute -m0^(-1) mod 2^32 using Newton's method for modular inverse.
|
||||||
|
// Starting from x=1 (which is always a valid initial approximation for
|
||||||
|
// odd m0), each iteration doubles the number of correct bits. After 5
|
||||||
|
// iterations we have 32 correct bits (1->2->4->8->16->32). This is the
|
||||||
|
// standard approach for computing the Montgomery constant.
|
||||||
static uint32_t computeM0Inv(uint32_t m0) {
|
static uint32_t computeM0Inv(uint32_t m0) {
|
||||||
// Newton's method: compute m0^(-1) mod 2^32
|
|
||||||
// Converges quadratically: 1 → 2 → 4 → 8 → 16 → 32 correct bits
|
|
||||||
uint32_t x = 1;
|
uint32_t x = 1;
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
|
|
@ -333,9 +365,11 @@ static uint32_t computeM0Inv(uint32_t m0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Compute R^2 mod m where R = 2^1024. This is the Montgomery domain
|
||||||
|
// conversion factor. We compute it by repeated doubling (shift left by 1)
|
||||||
|
// with modular reduction, which is simple but takes 2048 iterations.
|
||||||
|
// Only done once at initialization time.
|
||||||
static void computeR2(BigNumT *r2, const BigNumT *m) {
|
static void computeR2(BigNumT *r2, const BigNumT *m) {
|
||||||
// Compute R^2 mod m where R = 2^1024
|
|
||||||
// Method: start with 1, double 2048 times, reduce mod m each step
|
|
||||||
bnSet(r2, 1);
|
bnSet(r2, 1);
|
||||||
|
|
||||||
for (int i = 0; i < 2 * BN_BITS; i++) {
|
for (int i = 0; i < 2 * BN_BITS; i++) {
|
||||||
|
|
@ -358,8 +392,10 @@ static void dhInit(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Volatile pointer prevents the compiler from optimizing away the zeroing
|
||||||
|
// as a dead store. Critical for clearing key material — without volatile,
|
||||||
|
// the compiler sees that ptr is about to be freed and removes the memset.
|
||||||
static void secureZero(void *ptr, int len) {
|
static void secureZero(void *ptr, int len) {
|
||||||
// Volatile prevents the compiler from optimizing away the zeroing
|
|
||||||
volatile uint8_t *p = (volatile uint8_t *)ptr;
|
volatile uint8_t *p = (volatile uint8_t *)ptr;
|
||||||
|
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
|
|
@ -368,6 +404,11 @@ static void secureZero(void *ptr, int len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// XTEA block cipher: encrypts an 8-byte block in-place. The Feistel network
|
||||||
|
// uses 32 rounds (vs TEA's 32 or 64). Each round mixes the halves using
|
||||||
|
// shifts, adds, and XORs — no S-boxes, no lookup tables, no key schedule.
|
||||||
|
// The delta constant (golden ratio * 2^32) ensures each round uses a
|
||||||
|
// different effective key, preventing slide attacks.
|
||||||
static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]) {
|
static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]) {
|
||||||
uint32_t v0 = v[0];
|
uint32_t v0 = v[0];
|
||||||
uint32_t v1 = v[1];
|
uint32_t v1 = v[1];
|
||||||
|
|
@ -388,8 +429,12 @@ static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]) {
|
||||||
// RNG functions (alphabetical)
|
// RNG functions (alphabetical)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Mix additional entropy into the RNG state. XOR-folding into the key is
|
||||||
|
// simple and cannot reduce entropy (XOR with random data is a bijection).
|
||||||
|
// The re-mix step (encrypting the key with itself) diffuses the new entropy
|
||||||
|
// across all key bits so that even a single byte of good entropy improves
|
||||||
|
// the entire key state.
|
||||||
void secRngAddEntropy(const uint8_t *data, int len) {
|
void secRngAddEntropy(const uint8_t *data, int len) {
|
||||||
// XOR additional entropy into the key
|
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
((uint8_t *)sRng.key)[i % 16] ^= data[i];
|
((uint8_t *)sRng.key)[i % 16] ^= data[i];
|
||||||
}
|
}
|
||||||
|
|
@ -409,8 +454,12 @@ void secRngAddEntropy(const uint8_t *data, int len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Generate pseudorandom bytes using XTEA-CTR DRBG. Each call encrypts
|
||||||
|
// the monotonically increasing counter with the RNG key, producing 8 bytes
|
||||||
|
// of keystream per block. The counter never repeats (64-bit space), so
|
||||||
|
// the output is a pseudorandom stream as long as the key has sufficient
|
||||||
|
// entropy. Auto-seeds from hardware entropy on first use as a safety net.
|
||||||
void secRngBytes(uint8_t *buf, int len) {
|
void secRngBytes(uint8_t *buf, int len) {
|
||||||
// Auto-seed from hardware if never seeded
|
|
||||||
if (!sRng.seeded) {
|
if (!sRng.seeded) {
|
||||||
uint8_t entropy[16];
|
uint8_t entropy[16];
|
||||||
int got = secRngGatherEntropy(entropy, sizeof(entropy));
|
int got = secRngGatherEntropy(entropy, sizeof(entropy));
|
||||||
|
|
@ -440,10 +489,15 @@ void secRngBytes(uint8_t *buf, int len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Gather hardware entropy from the PIT (Programmable Interval Timer) and
|
||||||
|
// BIOS tick count. The PIT runs at 1.193182 MHz, so its LSBs change rapidly
|
||||||
|
// and provide ~10 bits of entropy per read (depending on timing jitter).
|
||||||
|
// The BIOS tick at 18.2 Hz adds a few more bits. Two PIT readings with
|
||||||
|
// the intervening code execution provide some jitter. Total: roughly 20
|
||||||
|
// bits of real entropy — not enough alone, but sufficient to seed the DRBG
|
||||||
|
// when supplemented by user interaction timing.
|
||||||
int secRngGatherEntropy(uint8_t *buf, int len) {
|
int secRngGatherEntropy(uint8_t *buf, int len) {
|
||||||
int out = 0;
|
int out = 0;
|
||||||
|
|
||||||
// Read PIT channel 0 counter (1.193 MHz, ~10 bits of entropy in LSBs)
|
|
||||||
outportb(0x43, 0x00);
|
outportb(0x43, 0x00);
|
||||||
uint8_t pitLo = inportb(0x40);
|
uint8_t pitLo = inportb(0x40);
|
||||||
uint8_t pitHi = inportb(0x40);
|
uint8_t pitHi = inportb(0x40);
|
||||||
|
|
@ -469,6 +523,9 @@ int secRngGatherEntropy(uint8_t *buf, int len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize the RNG from entropy. The 64-byte discard at the end is
|
||||||
|
// standard DRBG practice — it advances the state past any weak initial
|
||||||
|
// output that might leak information about the seed material.
|
||||||
void secRngSeed(const uint8_t *entropy, int len) {
|
void secRngSeed(const uint8_t *entropy, int len) {
|
||||||
memset(&sRng, 0, sizeof(sRng));
|
memset(&sRng, 0, sizeof(sRng));
|
||||||
|
|
||||||
|
|
@ -494,6 +551,11 @@ void secRngSeed(const uint8_t *entropy, int len) {
|
||||||
// DH functions (alphabetical)
|
// DH functions (alphabetical)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
// Compute the shared secret from the remote side's public key.
|
||||||
|
// Validates that the remote key is in [2, p-2] to prevent small-subgroup
|
||||||
|
// attacks (keys of 0, 1, or p-1 would produce trivially guessable secrets).
|
||||||
|
// The shared secret is remote^private mod p, which both sides compute
|
||||||
|
// independently and arrive at the same value (the DH property).
|
||||||
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) {
|
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) {
|
||||||
BigNumT remote;
|
BigNumT remote;
|
||||||
BigNumT two;
|
BigNumT two;
|
||||||
|
|
@ -534,6 +596,12 @@ SecDhT *secDhCreate(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Derive a symmetric key from the 128-byte shared secret by XOR-folding.
|
||||||
|
// This is a simple key derivation function: each byte of the secret is
|
||||||
|
// XOR'd into the output key at position (i % keyLen). For a 16-byte XTEA
|
||||||
|
// key, each output byte is the XOR of 8 secret bytes, providing good
|
||||||
|
// mixing. A proper KDF (HKDF, etc.) would be better but adds complexity
|
||||||
|
// and code size for marginal benefit in this use case.
|
||||||
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen) {
|
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen) {
|
||||||
uint8_t secretBytes[BN_BYTES];
|
uint8_t secretBytes[BN_BYTES];
|
||||||
|
|
||||||
|
|
@ -560,6 +628,8 @@ int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// secureZero the entire struct before freeing to prevent the private key
|
||||||
|
// from lingering in freed memory where it could be read by a later malloc.
|
||||||
void secDhDestroy(SecDhT *dh) {
|
void secDhDestroy(SecDhT *dh) {
|
||||||
if (dh) {
|
if (dh) {
|
||||||
secureZero(dh, sizeof(SecDhT));
|
secureZero(dh, sizeof(SecDhT));
|
||||||
|
|
@ -568,6 +638,11 @@ void secDhDestroy(SecDhT *dh) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Generate a DH keypair: random 256-bit private key, then compute
|
||||||
|
// public = g^private mod p. The private key is only 256 bits (not 1024)
|
||||||
|
// to keep exponentiation fast on 486-class hardware. With Montgomery
|
||||||
|
// multiplication, this takes ~256 squarings + ~128 multiplies, each
|
||||||
|
// operating on 32-word (1024-bit) numbers.
|
||||||
int secDhGenerateKeys(SecDhT *dh) {
|
int secDhGenerateKeys(SecDhT *dh) {
|
||||||
if (!dh) {
|
if (!dh) {
|
||||||
return SEC_ERR_PARAM;
|
return SEC_ERR_PARAM;
|
||||||
|
|
@ -632,6 +707,15 @@ SecCipherT *secCipherCreate(const uint8_t *key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CTR-mode encryption/decryption. The counter is encrypted to produce
|
||||||
|
// an 8-byte keystream block, which is XOR'd with the data. The counter
|
||||||
|
// increments after each block. Because XOR is its own inverse, the same
|
||||||
|
// function handles both encryption and decryption.
|
||||||
|
//
|
||||||
|
// IMPORTANT: the counter is internal state that advances with each call.
|
||||||
|
// The secLink layer ensures that TX and RX use separate cipher instances
|
||||||
|
// with separate counters, and that the same counter value is never reused
|
||||||
|
// with the same key (which would be catastrophic for CTR mode security).
|
||||||
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) {
|
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) {
|
||||||
uint32_t block[2];
|
uint32_t block[2];
|
||||||
uint8_t *keystream;
|
uint8_t *keystream;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,29 @@
|
||||||
// Security library: Diffie-Hellman key exchange + XTEA-CTR cipher
|
// Security library: Diffie-Hellman key exchange + XTEA-CTR cipher
|
||||||
// Targets 486-class hardware with 1024-bit DH (256-bit private exponent)
|
// Targets 486-class hardware with 1024-bit DH (256-bit private exponent)
|
||||||
// and XTEA in CTR mode for symmetric encryption.
|
// and XTEA in CTR mode for symmetric encryption.
|
||||||
|
//
|
||||||
|
// Why XTEA instead of AES/DES:
|
||||||
|
// XTEA requires zero lookup tables, no key schedule, and compiles to
|
||||||
|
// ~20 instructions per round (shifts, adds, XORs). This makes it ideal
|
||||||
|
// for a 486 where cache is tiny (8KB) and AES's 4KB S-boxes would
|
||||||
|
// thrash it. DES is similarly table-heavy and also has a complex key
|
||||||
|
// schedule. XTEA has no library dependencies — the entire cipher fits
|
||||||
|
// in a dozen lines of C. At 32 rounds, XTEA provides 128-bit security
|
||||||
|
// with negligible per-byte cost on even the slowest target hardware.
|
||||||
|
//
|
||||||
|
// Why 1024-bit DH with 256-bit private exponent:
|
||||||
|
// RFC 2409 Group 2 provides interoperability and a well-audited safe
|
||||||
|
// prime. 256-bit private exponents (vs full 1024-bit) reduce the modular
|
||||||
|
// exponentiation from ~1024 squarings+multiplies to ~256, making key
|
||||||
|
// generation feasible on a 486 in seconds rather than minutes. The
|
||||||
|
// security reduction is negligible (Pollard's rho on 256-bit exponent
|
||||||
|
// is ~2^128 operations, same as XTEA's key strength).
|
||||||
|
//
|
||||||
|
// RNG:
|
||||||
|
// XTEA-CTR DRBG seeded from PIT timer jitter and BIOS tick count.
|
||||||
|
// Hardware entropy is weak (~20 bits from PIT), so callers should
|
||||||
|
// supplement with user-generated entropy (keyboard timing, etc.)
|
||||||
|
// before generating DH keys.
|
||||||
|
|
||||||
#ifndef SECURITY_H
|
#ifndef SECURITY_H
|
||||||
#define SECURITY_H
|
#define SECURITY_H
|
||||||
|
|
@ -38,7 +61,11 @@ void secDhDestroy(SecDhT *dh);
|
||||||
int secDhGenerateKeys(SecDhT *dh);
|
int secDhGenerateKeys(SecDhT *dh);
|
||||||
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
|
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
|
||||||
|
|
||||||
// XTEA cipher in CTR mode (encrypt and decrypt are the same operation)
|
// XTEA cipher in CTR mode. CTR mode turns a block cipher into a stream
|
||||||
|
// cipher: encrypt an incrementing counter with the key to produce a
|
||||||
|
// keystream, then XOR with plaintext. Encrypt and decrypt are identical
|
||||||
|
// (XOR is its own inverse). The counter must never repeat with the same
|
||||||
|
// key, which is why secLink derives separate TX/RX keys.
|
||||||
SecCipherT *secCipherCreate(const uint8_t *key);
|
SecCipherT *secCipherCreate(const uint8_t *key);
|
||||||
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
|
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
|
||||||
void secCipherDestroy(SecCipherT *c);
|
void secCipherDestroy(SecCipherT *c);
|
||||||
|
|
|
||||||
13
tasks/demo.c
13
tasks/demo.c
|
|
@ -2,6 +2,11 @@
|
||||||
//
|
//
|
||||||
// Shows priority scheduling, round-robin, pausing/resuming, dynamic
|
// Shows priority scheduling, round-robin, pausing/resuming, dynamic
|
||||||
// priority changes, and slot reuse after task termination.
|
// priority changes, and slot reuse after task termination.
|
||||||
|
//
|
||||||
|
// This is a standalone test harness that runs on the host (Linux x86_64)
|
||||||
|
// or DJGPP target to verify the task switcher without the GUI stack.
|
||||||
|
// Each phase exercises a different scheduler feature and prints output
|
||||||
|
// so you can verify the scheduling order visually.
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include "taskswitch.h"
|
#include "taskswitch.h"
|
||||||
|
|
@ -65,6 +70,8 @@ int main(void) {
|
||||||
|
|
||||||
// Phase 1: all tasks run, but high-priority tasks get more turns per round.
|
// Phase 1: all tasks run, but high-priority tasks get more turns per round.
|
||||||
// Low (priority 0) gets 1 turn, normal (5) gets 6, high (10) gets 11.
|
// Low (priority 0) gets 1 turn, normal (5) gets 6, high (10) gets 11.
|
||||||
|
// In the output you'll see "high" appearing much more frequently than
|
||||||
|
// "low", demonstrating the weighted fair-share scheduling.
|
||||||
printf("--- Phase 1: Priority scheduling ---\n");
|
printf("--- Phase 1: Priority scheduling ---\n");
|
||||||
for (int32_t i = 0; i < 6; i++) {
|
for (int32_t i = 0; i < 6; i++) {
|
||||||
printf("[main] yield %d\n", (int)i);
|
printf("[main] yield %d\n", (int)i);
|
||||||
|
|
@ -102,7 +109,11 @@ int main(void) {
|
||||||
tsYield();
|
tsYield();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: dynamic creation with slot reuse
|
// Phase 5: dynamic creation with slot reuse.
|
||||||
|
// After all tasks from phases 1-4 have terminated, their slots are free.
|
||||||
|
// Creating new tasks should recycle those slots (IDs 1-4) rather than
|
||||||
|
// growing the array. Each "wave" creates 3 tasks, lets them finish,
|
||||||
|
// then creates 3 more — the IDs should repeat across waves.
|
||||||
printf("\n--- Phase 5: Slot reuse ---\n");
|
printf("\n--- Phase 5: Slot reuse ---\n");
|
||||||
printf("[main] active before: %u\n", (unsigned)tsActiveCount());
|
printf("[main] active before: %u\n", (unsigned)tsActiveCount());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,20 @@
|
||||||
//
|
//
|
||||||
// Task storage is a stb_ds dynamic array that grows as needed.
|
// Task storage is a stb_ds dynamic array that grows as needed.
|
||||||
// Terminated task slots are recycled by tsCreate().
|
// Terminated task slots are recycled by tsCreate().
|
||||||
|
//
|
||||||
|
// Why inline asm instead of setjmp/longjmp for context switching:
|
||||||
|
// setjmp/longjmp only save callee-saved registers and don't give us
|
||||||
|
// control over the stack pointer in a portable way. We need to set up
|
||||||
|
// a brand-new stack for each task and jump into a trampoline — setjmp
|
||||||
|
// can't bootstrap a fresh stack. The asm approach also avoids ABI
|
||||||
|
// differences in jmp_buf layout across DJGPP versions.
|
||||||
|
//
|
||||||
|
// Why stb_ds dynamic array instead of a linked list:
|
||||||
|
// Tasks are indexed by integer ID for O(1) lookup (tsGetState, tsKill,
|
||||||
|
// etc.). A linked list would require O(n) traversal for every ID-based
|
||||||
|
// operation. The array also has better cache locality during the
|
||||||
|
// scheduler's linear scan. The downside (holes after termination) is
|
||||||
|
// mitigated by slot recycling in findFreeSlot().
|
||||||
|
|
||||||
#define STB_DS_IMPLEMENTATION
|
#define STB_DS_IMPLEMENTATION
|
||||||
#include "thirdparty/stb_ds.h"
|
#include "thirdparty/stb_ds.h"
|
||||||
|
|
@ -18,6 +32,15 @@
|
||||||
// Internal types
|
// Internal types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Only callee-saved registers need to be in the context struct because
|
||||||
|
// the C calling convention guarantees the caller has already saved
|
||||||
|
// everything else. The compiler treats contextSwitch() as a normal
|
||||||
|
// function call, so caller-saved regs (eax/ecx/edx on i386,
|
||||||
|
// rax/rcx/rdx/r8-r11 on x86_64) are spilled by the compiler before
|
||||||
|
// the call. This minimizes context size and switch overhead.
|
||||||
|
//
|
||||||
|
// Field order is critical: the asm uses hardcoded byte offsets into
|
||||||
|
// this struct. Reordering fields will silently corrupt context switches.
|
||||||
#if defined(__x86_64__)
|
#if defined(__x86_64__)
|
||||||
// Saved CPU context for x86_64 (field order matches asm byte offsets)
|
// Saved CPU context for x86_64 (field order matches asm byte offsets)
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -42,7 +65,11 @@ typedef struct {
|
||||||
} TaskContextT;
|
} TaskContextT;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Task control block
|
// Task control block — one per task slot. The 'allocated' flag tracks
|
||||||
|
// whether the slot is live or recyclable, separate from the state enum,
|
||||||
|
// because we need to distinguish "never used" from "terminated and reaped".
|
||||||
|
// The 'isMain' flag protects task 0 from kill/pause — destroying the
|
||||||
|
// main task would orphan all other tasks with no scheduler to resume them.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char name[TS_NAME_MAX];
|
char name[TS_NAME_MAX];
|
||||||
TaskContextT context;
|
TaskContextT context;
|
||||||
|
|
@ -61,7 +88,12 @@ typedef struct {
|
||||||
// Module state
|
// Module state
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
static TaskBlockT *tasks = NULL; // stb_ds dynamic array
|
// stb_ds dynamic array of task control blocks. Slot 0 is always the main
|
||||||
|
// task. Slots 1..N are app tasks. Terminated slots have allocated=false
|
||||||
|
// and are reused by findFreeSlot() to prevent unbounded growth.
|
||||||
|
static TaskBlockT *tasks = NULL;
|
||||||
|
// Index of the currently executing task. Updated only by tsYield, tsExit,
|
||||||
|
// tsPause (self-pause), and tsRecoverToMain.
|
||||||
static uint32_t currentIdx = 0;
|
static uint32_t currentIdx = 0;
|
||||||
static bool initialized = false;
|
static bool initialized = false;
|
||||||
|
|
||||||
|
|
@ -101,6 +133,24 @@ void tsYield(void);
|
||||||
// callee-saved registers and the stack pointer. The return address is
|
// callee-saved registers and the stack pointer. The return address is
|
||||||
// captured as a local label so that when another task switches back to us,
|
// captured as a local label so that when another task switches back to us,
|
||||||
// execution resumes right after the save point.
|
// execution resumes right after the save point.
|
||||||
|
//
|
||||||
|
// The mechanism:
|
||||||
|
// 1. Save all callee-saved regs + esp/rsp into *save
|
||||||
|
// 2. Capture the address of local label "1:" as the saved EIP/RIP
|
||||||
|
// 3. Load all regs + esp/rsp from *restore
|
||||||
|
// 4. Jump to the restored EIP/RIP (which is label "1:" in the other task)
|
||||||
|
//
|
||||||
|
// For a newly created task, the restored EIP points to taskTrampoline
|
||||||
|
// (set up in tsCreate) rather than label "1:", so the first switch into
|
||||||
|
// a task bootstraps it into its entry function.
|
||||||
|
//
|
||||||
|
// noinline is critical: if the compiler inlines this, the callee-saved
|
||||||
|
// register assumptions break because the enclosing function may use
|
||||||
|
// different register allocation. The asm clobber list tells GCC which
|
||||||
|
// registers we destroy so it spills them before the call.
|
||||||
|
//
|
||||||
|
// The "memory" clobber acts as a compiler fence, ensuring all memory
|
||||||
|
// writes are flushed before the switch and re-read after resumption.
|
||||||
#if defined(__x86_64__)
|
#if defined(__x86_64__)
|
||||||
// x86_64: save rbx, r12-r15, rbp, rsp, rip.
|
// x86_64: save rbx, r12-r15, rbp, rsp, rip.
|
||||||
// Inputs via GCC constraints: %rdi = save ptr, %rsi = restore ptr.
|
// Inputs via GCC constraints: %rdi = save ptr, %rsi = restore ptr.
|
||||||
|
|
@ -114,9 +164,11 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont
|
||||||
"movq %%r15, 32(%%rdi)\n\t"
|
"movq %%r15, 32(%%rdi)\n\t"
|
||||||
"movq %%rbp, 40(%%rdi)\n\t"
|
"movq %%rbp, 40(%%rdi)\n\t"
|
||||||
"movq %%rsp, 48(%%rdi)\n\t"
|
"movq %%rsp, 48(%%rdi)\n\t"
|
||||||
|
// RIP-relative lea captures the resume point address
|
||||||
"leaq 1f(%%rip), %%rax\n\t"
|
"leaq 1f(%%rip), %%rax\n\t"
|
||||||
"movq %%rax, 56(%%rdi)\n\t"
|
"movq %%rax, 56(%%rdi)\n\t"
|
||||||
// Restore new context
|
// Restore new context — once rsp is swapped we're on the other
|
||||||
|
// task's stack. The jmp completes the switch.
|
||||||
"movq 0(%%rsi), %%rbx\n\t"
|
"movq 0(%%rsi), %%rbx\n\t"
|
||||||
"movq 8(%%rsi), %%r12\n\t"
|
"movq 8(%%rsi), %%r12\n\t"
|
||||||
"movq 16(%%rsi), %%r13\n\t"
|
"movq 16(%%rsi), %%r13\n\t"
|
||||||
|
|
@ -126,6 +178,8 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont
|
||||||
"movq 48(%%rsi), %%rsp\n\t"
|
"movq 48(%%rsi), %%rsp\n\t"
|
||||||
"movq 56(%%rsi), %%rax\n\t"
|
"movq 56(%%rsi), %%rax\n\t"
|
||||||
"jmp *%%rax\n\t"
|
"jmp *%%rax\n\t"
|
||||||
|
// Resume point: when someone switches back to us, execution
|
||||||
|
// continues here as if contextSwitch() just returned normally.
|
||||||
"1:\n\t"
|
"1:\n\t"
|
||||||
:
|
:
|
||||||
: "D" (save), "S" (restore)
|
: "D" (save), "S" (restore)
|
||||||
|
|
@ -143,6 +197,7 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont
|
||||||
"movl %%edi, 8(%%eax)\n\t"
|
"movl %%edi, 8(%%eax)\n\t"
|
||||||
"movl %%ebp, 12(%%eax)\n\t"
|
"movl %%ebp, 12(%%eax)\n\t"
|
||||||
"movl %%esp, 16(%%eax)\n\t"
|
"movl %%esp, 16(%%eax)\n\t"
|
||||||
|
// i386 can't do RIP-relative lea, so use an absolute label address
|
||||||
"movl $1f, 20(%%eax)\n\t"
|
"movl $1f, 20(%%eax)\n\t"
|
||||||
// Restore new context
|
// Restore new context
|
||||||
"movl 0(%%edx), %%ebx\n\t"
|
"movl 0(%%edx), %%ebx\n\t"
|
||||||
|
|
@ -163,6 +218,8 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont
|
||||||
|
|
||||||
// Find a free (terminated or unallocated) slot in the task array.
|
// Find a free (terminated or unallocated) slot in the task array.
|
||||||
// Returns the index, or -1 if no free slot exists.
|
// Returns the index, or -1 if no free slot exists.
|
||||||
|
// Starts at 1 because slot 0 is always the main task and cannot be reused.
|
||||||
|
// Linear scan is fine — SHELL_MAX_APPS caps the practical limit at ~32 tasks.
|
||||||
static int32_t findFreeSlot(void) {
|
static int32_t findFreeSlot(void) {
|
||||||
ptrdiff_t count = arrlen(tasks);
|
ptrdiff_t count = arrlen(tasks);
|
||||||
for (ptrdiff_t i = 1; i < count; i++) {
|
for (ptrdiff_t i = 1; i < count; i++) {
|
||||||
|
|
@ -179,6 +236,20 @@ static int32_t findFreeSlot(void) {
|
||||||
// per scheduling turn. When no ready task has credits left, every
|
// per scheduling turn. When no ready task has credits left, every
|
||||||
// ready task is refilled. This guarantees all tasks run while giving
|
// ready task is refilled. This guarantees all tasks run while giving
|
||||||
// higher-priority tasks proportionally more turns.
|
// higher-priority tasks proportionally more turns.
|
||||||
|
//
|
||||||
|
// Algorithm (variant of Linux 2.4's goodness() scheduler):
|
||||||
|
// 1. Scan forward from currentIdx looking for a ready task with credits > 0
|
||||||
|
// 2. If found, decrement its credits and select it
|
||||||
|
// 3. If no task has credits, refill ALL ready tasks (one "epoch")
|
||||||
|
// 4. Scan again after refill
|
||||||
|
//
|
||||||
|
// The round-robin scan starts at (currentIdx + 1) and wraps, ensuring
|
||||||
|
// fairness among tasks with equal priority — no task gets picked twice
|
||||||
|
// in a row unless it's the only ready task.
|
||||||
|
//
|
||||||
|
// If no ready tasks exist at all (everything paused/terminated), return
|
||||||
|
// currentIdx so the caller stays on the current task (always task 0 in
|
||||||
|
// practice, since task 0 is the shell's main loop and never pauses).
|
||||||
static uint32_t scheduleNext(void) {
|
static uint32_t scheduleNext(void) {
|
||||||
uint32_t count = (uint32_t)arrlen(tasks);
|
uint32_t count = (uint32_t)arrlen(tasks);
|
||||||
|
|
||||||
|
|
@ -191,7 +262,7 @@ static uint32_t scheduleNext(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All credits exhausted -- refill every ready task
|
// All credits exhausted — start a new epoch by refilling every ready task
|
||||||
bool anyReady = false;
|
bool anyReady = false;
|
||||||
for (uint32_t i = 0; i < count; i++) {
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
if (tasks[i].allocated && tasks[i].state == TaskStateReady) {
|
if (tasks[i].allocated && tasks[i].state == TaskStateReady) {
|
||||||
|
|
@ -217,8 +288,13 @@ static uint32_t scheduleNext(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Entry point for every new task. Calls the user-supplied function and
|
// Entry point for every new task. The first context switch into a new task
|
||||||
// then terminates the task when it returns.
|
// jumps here (via the EIP/RIP set up in tsCreate). This is a trampoline
|
||||||
|
// rather than calling entry directly because we need to call tsExit() when
|
||||||
|
// the entry function returns — if we just set EIP to the entry function,
|
||||||
|
// it would return to a garbage address (the dummy 0 on the stack).
|
||||||
|
// The trampoline ensures clean task termination even if the app forgets
|
||||||
|
// to call tsExit() explicitly.
|
||||||
static void taskTrampoline(void) {
|
static void taskTrampoline(void) {
|
||||||
TaskBlockT *task = &tasks[currentIdx];
|
TaskBlockT *task = &tasks[currentIdx];
|
||||||
task->entry(task->arg);
|
task->entry(task->arg);
|
||||||
|
|
@ -253,7 +329,9 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
|
||||||
stackSize = TS_DEFAULT_STACK_SIZE;
|
stackSize = TS_DEFAULT_STACK_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse a terminated/free slot, or append a new one
|
// Reuse a terminated/free slot, or append a new one.
|
||||||
|
// Recycling avoids unbounded array growth when apps are repeatedly
|
||||||
|
// launched and terminated over the lifetime of the shell.
|
||||||
int32_t id = findFreeSlot();
|
int32_t id = findFreeSlot();
|
||||||
if (id < 0) {
|
if (id < 0) {
|
||||||
TaskBlockT blank = {0};
|
TaskBlockT blank = {0};
|
||||||
|
|
@ -283,7 +361,13 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
|
||||||
task->isMain = false;
|
task->isMain = false;
|
||||||
task->allocated = true;
|
task->allocated = true;
|
||||||
|
|
||||||
// Set up initial stack (grows downward, 16-byte aligned)
|
// Set up initial stack (grows downward, 16-byte aligned).
|
||||||
|
// The ABI requires 16-byte stack alignment at function entry. We align
|
||||||
|
// the top, then push a dummy return address (0) to simulate a CALL
|
||||||
|
// instruction — this keeps the stack aligned for the trampoline.
|
||||||
|
// The dummy address is never used because taskTrampoline calls tsExit()
|
||||||
|
// which switches away without returning, but it satisfies debuggers
|
||||||
|
// and ABI checkers that expect a return address at the bottom of each frame.
|
||||||
uintptr_t top = (uintptr_t)(task->stack + stackSize);
|
uintptr_t top = (uintptr_t)(task->stack + stackSize);
|
||||||
top &= ~(uintptr_t)0xF;
|
top &= ~(uintptr_t)0xF;
|
||||||
top -= sizeof(uintptr_t);
|
top -= sizeof(uintptr_t);
|
||||||
|
|
@ -316,6 +400,11 @@ uint32_t tsCurrentId(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Self-termination. Frees resources and switches to the next task.
|
||||||
|
// This function never returns — the terminated task's context is abandoned.
|
||||||
|
// We save to tasks[prev].context even though we'll never restore it because
|
||||||
|
// contextSwitch always writes to the save pointer; the data is harmless
|
||||||
|
// and will be overwritten when the slot is recycled.
|
||||||
void tsExit(void) {
|
void tsExit(void) {
|
||||||
if (!initialized || tasks[currentIdx].isMain) {
|
if (!initialized || tasks[currentIdx].isMain) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -323,7 +412,9 @@ void tsExit(void) {
|
||||||
|
|
||||||
tasks[currentIdx].state = TaskStateTerminated;
|
tasks[currentIdx].state = TaskStateTerminated;
|
||||||
|
|
||||||
// Free the stack immediately
|
// Free the stack immediately — safe because we're about to switch
|
||||||
|
// away and never return. The context switch itself doesn't touch
|
||||||
|
// the old stack after swapping ESP/RSP.
|
||||||
free(tasks[currentIdx].stack);
|
free(tasks[currentIdx].stack);
|
||||||
tasks[currentIdx].stack = NULL;
|
tasks[currentIdx].stack = NULL;
|
||||||
tasks[currentIdx].allocated = false;
|
tasks[currentIdx].allocated = false;
|
||||||
|
|
@ -372,6 +463,11 @@ TaskStateE tsGetState(uint32_t taskId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Register the calling context as task 0 (main). No stack is allocated
|
||||||
|
// because the main task uses the process stack. The main task's context
|
||||||
|
// struct is filled in lazily by contextSwitch on the first tsYield() —
|
||||||
|
// until then, the saved EIP/ESP are zero, which is fine because we
|
||||||
|
// never restore task 0 from a cold start.
|
||||||
int32_t tsInit(void) {
|
int32_t tsInit(void) {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return TS_ERR_PARAM;
|
return TS_ERR_PARAM;
|
||||||
|
|
@ -395,6 +491,19 @@ int32_t tsInit(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Forcibly terminate another task. This is safe in a cooperative system
|
||||||
|
// because the target is guaranteed to be suspended at a yield point — it
|
||||||
|
// cannot be in the middle of a critical section. The stack is freed and
|
||||||
|
// the slot is recycled immediately.
|
||||||
|
//
|
||||||
|
// Cannot kill self (use tsExit instead) — killing self would free the
|
||||||
|
// stack we're currently executing on. Cannot kill main (task 0) because
|
||||||
|
// the shell's main loop must always be runnable for crash recovery.
|
||||||
|
//
|
||||||
|
// The shell uses this for two purposes:
|
||||||
|
// 1. shellForceKillApp: "End Task" from the task manager
|
||||||
|
// 2. Crash recovery: after a signal handler longjmps to main, the
|
||||||
|
// crashed task's slot is cleaned up via tsKill
|
||||||
int32_t tsKill(uint32_t taskId) {
|
int32_t tsKill(uint32_t taskId) {
|
||||||
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
if (!initialized || taskId >= (uint32_t)arrlen(tasks)) {
|
||||||
return TS_ERR_PARAM;
|
return TS_ERR_PARAM;
|
||||||
|
|
@ -437,7 +546,10 @@ int32_t tsPause(uint32_t taskId) {
|
||||||
|
|
||||||
tasks[taskId].state = TaskStatePaused;
|
tasks[taskId].state = TaskStatePaused;
|
||||||
|
|
||||||
// If we paused ourselves, yield immediately
|
// If we paused ourselves, must yield immediately — a paused task
|
||||||
|
// won't be selected by scheduleNext, so staying on CPU would deadlock.
|
||||||
|
// If pausing another task, no yield needed; it will simply be skipped
|
||||||
|
// the next time the scheduler scans.
|
||||||
if (taskId == currentIdx) {
|
if (taskId == currentIdx) {
|
||||||
uint32_t next = scheduleNext();
|
uint32_t next = scheduleNext();
|
||||||
if (next != currentIdx) {
|
if (next != currentIdx) {
|
||||||
|
|
@ -454,6 +566,17 @@ int32_t tsPause(uint32_t taskId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Emergency recovery after a crash in an app task. When a signal handler
|
||||||
|
// fires (e.g., SIGSEGV), DJGPP's signal dispatch saves the exception
|
||||||
|
// state and calls our handler. The handler does longjmp back to the
|
||||||
|
// shell's setjmp point in main(), which restores the main task's stack.
|
||||||
|
// However, the task switcher's currentIdx still points to the crashed
|
||||||
|
// app task. This function fixes the bookkeeping so the scheduler treats
|
||||||
|
// task 0 as the running task again.
|
||||||
|
//
|
||||||
|
// The crashed task's slot is NOT freed here — its stack is corrupt and
|
||||||
|
// the caller (shellMain's crash recovery) must call shellForceKillApp
|
||||||
|
// to clean it up properly (destroying windows, closing DXE, etc.).
|
||||||
void tsRecoverToMain(void) {
|
void tsRecoverToMain(void) {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -475,6 +598,9 @@ int32_t tsResume(uint32_t taskId) {
|
||||||
return TS_ERR_STATE;
|
return TS_ERR_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transition from Paused back to Ready and refill credits immediately.
|
||||||
|
// Without the refill, a resumed task might have 0 credits and would have
|
||||||
|
// to wait for the next epoch to run, making resume feel sluggish.
|
||||||
tasks[taskId].state = TaskStateReady;
|
tasks[taskId].state = TaskStateReady;
|
||||||
tasks[taskId].credits = tasks[taskId].priority + 1;
|
tasks[taskId].credits = tasks[taskId].priority + 1;
|
||||||
return TS_OK;
|
return TS_OK;
|
||||||
|
|
@ -515,6 +641,13 @@ void tsShutdown(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The core cooperative yield. Called explicitly by app code (or implicitly
|
||||||
|
// via the shell's idle callback and main loop). If no other task is ready,
|
||||||
|
// returns immediately — no context switch overhead when running solo.
|
||||||
|
//
|
||||||
|
// The state transition: current task moves Running -> Ready (still
|
||||||
|
// schedulable), next task moves Ready -> Running. The previous task will
|
||||||
|
// resume here when someone else yields and the scheduler picks it again.
|
||||||
void tsYield(void) {
|
void tsYield(void) {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -527,6 +660,8 @@ void tsYield(void) {
|
||||||
|
|
||||||
uint32_t prev = currentIdx;
|
uint32_t prev = currentIdx;
|
||||||
|
|
||||||
|
// Only transition to Ready if still Running — a task that paused itself
|
||||||
|
// will already be in Paused state when tsYield is called from tsPause.
|
||||||
if (tasks[prev].state == TaskStateRunning) {
|
if (tasks[prev].state == TaskStateRunning) {
|
||||||
tasks[prev].state = TaskStateReady;
|
tasks[prev].state = TaskStateReady;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,25 @@
|
||||||
// taskswitch.h -- Cooperative task switching library for DJGPP
|
// taskswitch.h -- Cooperative task switching library for DJGPP
|
||||||
//
|
//
|
||||||
|
// Cooperative (non-preemptive) multitasking for DOS protected mode (DJGPP/DPMI).
|
||||||
|
//
|
||||||
|
// Why cooperative instead of preemptive:
|
||||||
|
// 1. DOS is single-threaded — there is no kernel scheduler to preempt us.
|
||||||
|
// 2. DPMI provides no thread or timer-based preemption primitives.
|
||||||
|
// 3. The DVX GUI event model is inherently single-threaded: one compositor,
|
||||||
|
// one input queue, one window stack. Preemption would require locking
|
||||||
|
// around every GUI call, adding latency and complexity for no benefit.
|
||||||
|
// 4. Cooperative switching lets each task yield at safe points, avoiding
|
||||||
|
// the need for synchronization primitives entirely.
|
||||||
|
//
|
||||||
// Credit-based cooperative multitasking with task pausing.
|
// Credit-based cooperative multitasking with task pausing.
|
||||||
// Each task receives (priority + 1) credits per scheduling round.
|
// Each task receives (priority + 1) credits per scheduling round.
|
||||||
// Tasks run round-robin, consuming one credit per turn. When all
|
// Tasks run round-robin, consuming one credit per turn. When all
|
||||||
// credits are spent, every ready task is refilled. Higher-priority
|
// credits are spent, every ready task is refilled. Higher-priority
|
||||||
// tasks run proportionally more often but never starve lower ones.
|
// tasks run proportionally more often but never starve lower ones.
|
||||||
|
//
|
||||||
|
// This is a weighted fair-share scheduler: a priority-10 task gets 11
|
||||||
|
// turns per round while a priority-0 task gets 1, but the low-priority
|
||||||
|
// task is guaranteed to run eventually — no starvation is possible.
|
||||||
|
|
||||||
#ifndef TASKSWITCH_H
|
#ifndef TASKSWITCH_H
|
||||||
#define TASKSWITCH_H
|
#define TASKSWITCH_H
|
||||||
|
|
@ -20,16 +35,22 @@
|
||||||
#define TS_ERR_NOMEM (-4)
|
#define TS_ERR_NOMEM (-4)
|
||||||
#define TS_ERR_STATE (-5)
|
#define TS_ERR_STATE (-5)
|
||||||
|
|
||||||
// Defaults
|
// 32KB default stack — generous for callback-style app tasks that spend
|
||||||
|
// most of their time in the shell's GUI code. Apps needing deeper recursion
|
||||||
|
// or large stack allocations can request more via AppDescriptorT.stackSize.
|
||||||
#define TS_DEFAULT_STACK_SIZE 32768
|
#define TS_DEFAULT_STACK_SIZE 32768
|
||||||
#define TS_NAME_MAX 32
|
#define TS_NAME_MAX 32
|
||||||
|
|
||||||
// Priority levels
|
// Priority levels — the credit count is (priority + 1), so LOW gets 1 turn
|
||||||
|
// per round, NORMAL gets 6, HIGH gets 11. The shell's main task runs at
|
||||||
|
// HIGH to keep UI responsive; app tasks default to NORMAL.
|
||||||
#define TS_PRIORITY_LOW 0
|
#define TS_PRIORITY_LOW 0
|
||||||
#define TS_PRIORITY_NORMAL 5
|
#define TS_PRIORITY_NORMAL 5
|
||||||
#define TS_PRIORITY_HIGH 10
|
#define TS_PRIORITY_HIGH 10
|
||||||
|
|
||||||
// Task states
|
// Task states — only Ready tasks participate in scheduling. The Running
|
||||||
|
// state is cosmetic (marks the currently executing task). Paused tasks
|
||||||
|
// are skipped until explicitly resumed. Terminated slots are recycled.
|
||||||
typedef enum {
|
typedef enum {
|
||||||
TaskStateReady = 0,
|
TaskStateReady = 0,
|
||||||
TaskStateRunning = 1,
|
TaskStateRunning = 1,
|
||||||
|
|
@ -37,11 +58,14 @@ typedef enum {
|
||||||
TaskStateTerminated = 3
|
TaskStateTerminated = 3
|
||||||
} TaskStateE;
|
} TaskStateE;
|
||||||
|
|
||||||
// Task entry function signature
|
// Task entry function signature — the void* arg lets the caller pass
|
||||||
|
// arbitrary context (e.g., a ShellAppT* for the DVX shell's app wrapper)
|
||||||
typedef void (*TaskEntryT)(void *arg);
|
typedef void (*TaskEntryT)(void *arg);
|
||||||
|
|
||||||
// Initialize the task system. The calling context becomes task 0 (main).
|
// Initialize the task system. The calling context becomes task 0 (main).
|
||||||
// The task array grows dynamically as tasks are created.
|
// Task 0 is special: it cannot be killed or paused, and the crash recovery
|
||||||
|
// path (tsRecoverToMain) always returns control here. No separate stack is
|
||||||
|
// allocated for task 0 — it uses the process stack directly.
|
||||||
int32_t tsInit(void);
|
int32_t tsInit(void);
|
||||||
|
|
||||||
// Shut down the task system and free all resources.
|
// Shut down the task system and free all resources.
|
||||||
|
|
@ -49,19 +73,25 @@ void tsShutdown(void);
|
||||||
|
|
||||||
// Create a new task. Pass 0 for stackSize to use TS_DEFAULT_STACK_SIZE.
|
// Create a new task. Pass 0 for stackSize to use TS_DEFAULT_STACK_SIZE.
|
||||||
// Returns the task ID (>= 0) on success, or a negative error code.
|
// Returns the task ID (>= 0) on success, or a negative error code.
|
||||||
|
// Terminated task slots are recycled to avoid unbounded array growth.
|
||||||
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
|
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
|
||||||
|
|
||||||
// Yield CPU to the next eligible ready task (credit-based round-robin).
|
// Yield CPU to the next eligible ready task (credit-based round-robin).
|
||||||
|
// This is the sole mechanism for task switching — every app must call
|
||||||
|
// this (or a GUI function that calls it) periodically, or it will
|
||||||
|
// monopolize the CPU.
|
||||||
void tsYield(void);
|
void tsYield(void);
|
||||||
|
|
||||||
// Pause a task. Cannot pause the main task (id 0).
|
// Pause a task. Cannot pause the main task (id 0).
|
||||||
// If a task pauses itself, an implicit yield occurs.
|
// If a task pauses itself, an implicit yield occurs.
|
||||||
int32_t tsPause(uint32_t taskId);
|
int32_t tsPause(uint32_t taskId);
|
||||||
|
|
||||||
// Resume a paused task.
|
// Resume a paused task. Credits are refilled so the task gets a fair
|
||||||
|
// share of CPU time immediately rather than waiting for the next round.
|
||||||
int32_t tsResume(uint32_t taskId);
|
int32_t tsResume(uint32_t taskId);
|
||||||
|
|
||||||
// Set a task's scheduling priority.
|
// Set a task's scheduling priority. Also refills credits so the change
|
||||||
|
// takes effect immediately rather than at the next credit refill epoch.
|
||||||
int32_t tsSetPriority(uint32_t taskId, int32_t priority);
|
int32_t tsSetPriority(uint32_t taskId, int32_t priority);
|
||||||
|
|
||||||
// Get a task's priority, or TS_ERR_PARAM on invalid id.
|
// Get a task's priority, or TS_ERR_PARAM on invalid id.
|
||||||
|
|
@ -77,14 +107,24 @@ uint32_t tsCurrentId(void);
|
||||||
const char *tsGetName(uint32_t taskId);
|
const char *tsGetName(uint32_t taskId);
|
||||||
|
|
||||||
// Terminate the calling task. Must not be called from the main task.
|
// Terminate the calling task. Must not be called from the main task.
|
||||||
|
// The stack is freed immediately and the slot is marked for reuse.
|
||||||
|
// Internally performs a context switch to the next ready task — this
|
||||||
|
// function never returns to the caller.
|
||||||
void tsExit(void);
|
void tsExit(void);
|
||||||
|
|
||||||
// Forcibly terminate another task. Cannot kill main task (id 0) or self.
|
// Forcibly terminate another task. Cannot kill main task (id 0) or self.
|
||||||
|
// Safe to call only because we're cooperative: the target task is
|
||||||
|
// guaranteed to be suspended at a yield point, so its stack can be freed
|
||||||
|
// without worry about it executing concurrently. In a preemptive system
|
||||||
|
// this would be undefined behavior.
|
||||||
int32_t tsKill(uint32_t taskId);
|
int32_t tsKill(uint32_t taskId);
|
||||||
|
|
||||||
// Crash recovery: force scheduler back to main task (id 0).
|
// Crash recovery: force scheduler back to main task (id 0).
|
||||||
// Call after longjmp from a signal handler that fired in a non-main task.
|
// Call after longjmp from a signal handler that fired in a non-main task.
|
||||||
// The crashed task is NOT cleaned up — call tsKill() afterward.
|
// The crashed task is NOT cleaned up — call tsKill() afterward.
|
||||||
|
// This exists because longjmp unwinds the crashed task's stack but the
|
||||||
|
// scheduler's currentIdx still points to it. We must fix the bookkeeping
|
||||||
|
// before doing anything else.
|
||||||
void tsRecoverToMain(void);
|
void tsRecoverToMain(void);
|
||||||
|
|
||||||
// Get the number of non-terminated tasks.
|
// Get the number of non-terminated tasks.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,23 @@
|
||||||
// termdemo.c — SecLink terminal emulator demo
|
// termdemo.c — SecLink terminal emulator demo
|
||||||
//
|
//
|
||||||
// Uses DVX GUI ANSI terminal widget with SecLink encrypted serial link
|
// A standalone DVX GUI application (NOT a DXE app — this has its own main())
|
||||||
// to provide a BBS terminal over a secured serial connection.
|
// that combines the ANSI terminal widget with the SecLink encrypted serial
|
||||||
|
// stack to create a BBS terminal client.
|
||||||
|
//
|
||||||
|
// Unlike the DXE apps (progman, notepad, clock, dvxdemo) which run inside
|
||||||
|
// the DVX Shell, this is a freestanding program that initializes the GUI
|
||||||
|
// directly and manages its own event loop. It demonstrates how to use the
|
||||||
|
// DVX widget system outside the shell framework.
|
||||||
|
//
|
||||||
|
// Data flow:
|
||||||
|
// User keystrokes -> terminal widget -> commWrite -> secLink (encrypt) -> UART
|
||||||
|
// UART -> secLink (decrypt) -> onRecv -> ring buffer -> commRead -> terminal widget
|
||||||
|
//
|
||||||
|
// The ring buffer decouples the secLink receive callback (which fires during
|
||||||
|
// secLinkPoll) from the terminal widget's read cycle (which fires during
|
||||||
|
// dvxUpdate). This is necessary because the callback can fire at any time
|
||||||
|
// during polling, but the terminal widget expects to read data synchronously
|
||||||
|
// during its paint cycle.
|
||||||
//
|
//
|
||||||
// Usage: termdemo [com_port] [baud_rate]
|
// Usage: termdemo [com_port] [baud_rate]
|
||||||
// com_port — 1-4 (default 1)
|
// com_port — 1-4 (default 1)
|
||||||
|
|
@ -29,14 +45,17 @@
|
||||||
// Types
|
// Types
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Bridges secLink receive callback and terminal widget comm interface
|
// Bridges the secLink receive callback (asynchronous, fires during poll)
|
||||||
|
// with the terminal widget's comm interface (synchronous, called during
|
||||||
|
// widget paint). The ring buffer allows these two timing domains to
|
||||||
|
// communicate without blocking.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
SecLinkT *link;
|
SecLinkT *link;
|
||||||
AppContextT *app;
|
AppContextT *app;
|
||||||
WidgetT *term;
|
WidgetT *term;
|
||||||
uint8_t recvBuf[RECV_BUF_SIZE];
|
uint8_t recvBuf[RECV_BUF_SIZE];
|
||||||
int32_t recvHead;
|
int32_t recvHead; // written by onRecv callback
|
||||||
int32_t recvTail;
|
int32_t recvTail; // read by commRead during widget paint
|
||||||
} TermContextT;
|
} TermContextT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -254,7 +273,10 @@ int main(int argc, char *argv[]) {
|
||||||
wgtAnsiTermSetScrollback(term, 1000);
|
wgtAnsiTermSetScrollback(term, 1000);
|
||||||
tc.term = term;
|
tc.term = term;
|
||||||
|
|
||||||
// Connect terminal widget to secLink via comm interface
|
// The comm interface is the glue between the terminal widget and the
|
||||||
|
// secLink transport. The widget calls commRead during its paint cycle
|
||||||
|
// to get new data to display, and commWrite when the user types to send
|
||||||
|
// keystrokes over the encrypted link.
|
||||||
wgtAnsiTermSetComm(term, &tc, commRead, commWrite);
|
wgtAnsiTermSetComm(term, &tc, commRead, commWrite);
|
||||||
|
|
||||||
// Status bar showing connection info
|
// Status bar showing connection info
|
||||||
|
|
@ -266,11 +288,17 @@ int main(int argc, char *argv[]) {
|
||||||
// Fit window to widget tree
|
// Fit window to widget tree
|
||||||
dvxFitWindow(&ctx, win);
|
dvxFitWindow(&ctx, win);
|
||||||
|
|
||||||
// Poll serial during idle instead of yielding CPU
|
// Register an idle callback so the GUI event loop polls secLink during
|
||||||
|
// idle time (when there are no mouse/keyboard events to process). This
|
||||||
|
// ensures incoming data is processed even when the user isn't interacting
|
||||||
|
// with the terminal. Without this, data would only arrive during the
|
||||||
|
// fixed-rate dvxUpdate calls.
|
||||||
ctx.idleCallback = idlePoll;
|
ctx.idleCallback = idlePoll;
|
||||||
ctx.idleCtx = &tc;
|
ctx.idleCtx = &tc;
|
||||||
|
|
||||||
// Main loop — poll serial, dvxUpdate handles terminal widget automatically
|
// Main loop: poll secLink for incoming data, then dvxUpdate processes
|
||||||
|
// input events, repaints dirty widgets (including the terminal, which
|
||||||
|
// calls commRead to consume the ring buffer), and flushes to the LFB.
|
||||||
while (ctx.running) {
|
while (ctx.running) {
|
||||||
secLinkPoll(tc.link);
|
secLinkPoll(tc.link);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue