From 2e45e4b14db91de4b7ccc94ab50a71395e340647 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 10 Mar 2026 19:42:33 -0500 Subject: [PATCH] Added ANSIBBS widget. --- README.md | 73 ++- dvx/Makefile | 4 +- dvx/dvxWidget.h | 53 +- dvx/widgets/widgetAnsiTerm.c | 1145 ++++++++++++++++++++++++++++++++++ dvx/widgets/widgetCore.c | 3 + dvx/widgets/widgetEvent.c | 14 +- dvx/widgets/widgetInternal.h | 4 + dvx/widgets/widgetLayout.c | 3 + dvx/widgets/widgetOps.c | 7 + 9 files changed, 1302 insertions(+), 4 deletions(-) create mode 100644 dvx/widgets/widgetAnsiTerm.c diff --git a/README.md b/README.md index f6e11a9..d2313c9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Motif-style beveled chrome, dirty-rectangle compositing, draggable and resizable windows, dropdown menus, scrollbars, and a declarative widget/layout system with buttons, checkboxes, radios, text inputs, dropdowns, combo boxes, sliders, progress bars, tab controls, tree views, toolbars, status bars, -images, image buttons, and drawable canvases. +images, image buttons, drawable canvases, and an ANSI BBS terminal emulator. ## Building @@ -628,6 +628,77 @@ solid, and `FillCircle` fills a circle -- all using the current pen color. All operations clip to the canvas bounds. Colors are in display pixel format (use `packColor()` to create them). +### ANSI Terminal + +```c +WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows); +``` +ANSI BBS terminal emulator widget. Displays a character grid (default +80x25 if cols/rows are 0) with full ANSI escape sequence support and a +16-color CGA palette. The terminal has a 2px sunken bevel border and a +vertical scrollbar for scrollback history. + +ANSI escape sequences supported: + +| Sequence | Description | +|----------|-------------| +| `ESC[H` / `ESC[f` | Cursor position (CUP/HVP) | +| `ESC[A/B/C/D` | Cursor up/down/forward/back | +| `ESC[J` | Erase display (0=to end, 1=to start, 2=all) | +| `ESC[K` | Erase line (0=to end, 1=to start, 2=all) | +| `ESC[m` | SGR: colors 30-37/40-47, bright 90-97/100-107, bold(1), blink(5), reverse(7), reset(0) | +| `ESC[s` / `ESC[u` | Save / restore cursor position | +| `ESC[S` / `ESC[T` | Scroll up / down | +| `ESC[L` / `ESC[M` | Insert / delete lines | +| `ESC[?7h/l` | Enable / disable auto-wrap | +| `ESC[?25h/l` | Show / hide cursor | + +Control characters: CR, LF, BS, TAB, BEL (ignored). + +```c +void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); +``` +Feed data through the ANSI parser. Use this to display `.ANS` files or +inject content without a communications link. + +```c +void wgtAnsiTermClear(WidgetT *w); +``` +Clear the screen, push all visible lines to scrollback, and reset the +cursor to the home position. + +```c +void wgtAnsiTermSetComm(WidgetT *w, void *ctx, + int32_t (*readFn)(void *, uint8_t *, int32_t), + int32_t (*writeFn)(void *, const uint8_t *, int32_t)); +``` +Set the communications interface. `readFn` should return bytes read +(0 if none available). `writeFn` sends bytes. Pass NULL function +pointers for a disconnected / display-only terminal. + +When connected, keyboard input is translated to ANSI sequences and sent +via `writeFn` (arrows become `ESC[A`..`ESC[D`, etc.). + +```c +int32_t wgtAnsiTermPoll(WidgetT *w); +``` +Poll the comm interface for incoming data and process it through the +ANSI parser. Returns the number of bytes read. Call this from your +event loop or idle handler, then `wgtInvalidate()` if the return +value is nonzero. + +```c +void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); +``` +Set the scrollback buffer size (default 500 lines). Clears any existing +scrollback. Lines scroll into the buffer when they leave the top of the +screen or when the screen is cleared (`ESC[2J` / `wgtAnsiTermClear`). + +A vertical scrollbar appears automatically when there is scrollback +content. Click the arrow buttons for single-line scrolling, or the +trough for page scrolling. The view auto-follows live output unless the +user has scrolled back. + ### Spacing and dividers ```c diff --git a/dvx/Makefile b/dvx/Makefile index a4bb022..d93e2ed 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -13,7 +13,8 @@ LIBDIR = ../lib SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c -WSRCS = widgets/widgetCore.c \ +WSRCS = widgets/widgetAnsiTerm.c \ + widgets/widgetCore.c \ widgets/widgetLayout.c \ widgets/widgetEvent.c \ widgets/widgetOps.c \ @@ -76,6 +77,7 @@ $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvx # Widget file dependencies WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h +$(WOBJDIR)/widgetAnsiTerm.o: widgets/widgetAnsiTerm.c $(WIDGET_DEPS) $(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS) $(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS) $(WOBJDIR)/widgetEvent.o: widgets/widgetEvent.c $(WIDGET_DEPS) diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 0cadcff..4ff3f1c 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -63,7 +63,8 @@ typedef enum { WidgetTreeItemE, WidgetImageE, WidgetImageButtonE, - WidgetCanvasE + WidgetCanvasE, + WidgetAnsiTermE } WidgetTypeE; // ============================================================ @@ -283,6 +284,34 @@ typedef struct WidgetT { int32_t lastX; int32_t lastY; } canvas; + + struct { + uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes + int32_t cols; // columns (default 80) + int32_t rows; // rows (default 25) + int32_t cursorRow; // 0-based cursor row + int32_t cursorCol; // 0-based cursor column + bool cursorVisible; + bool wrapMode; // auto-wrap at right margin + bool bold; // SGR bold flag (brightens foreground) + bool csiPrivate; // '?' prefix in CSI sequence + uint8_t curAttr; // current text attribute (fg | bg<<4) + uint8_t parseState; // 0=normal, 1=ESC, 2=CSI + int32_t params[8]; // CSI parameter accumulator + int32_t paramCount; // number of CSI params collected + int32_t savedRow; // saved cursor position (SCP) + int32_t savedCol; + // Scrollback + uint8_t *scrollback; // circular buffer of scrollback lines + int32_t scrollbackMax; // max lines in scrollback buffer + int32_t scrollbackCount; // current number of lines stored + int32_t scrollbackHead; // write position (circular index) + int32_t scrollPos; // view position (scrollbackCount = live) + // Communications interface (all NULL = disconnected) + void *commCtx; + int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); + int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); + } ansiTerm; } as; } WidgetT; @@ -444,6 +473,28 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color); uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y); +// ============================================================ +// ANSI Terminal +// ============================================================ + +// Create an ANSI terminal widget (0 for cols/rows = 80x25 default) +WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows); + +// Write data through the ANSI parser (for loading .ANS files or feeding data without a connection) +void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); + +// Clear the terminal screen and reset cursor to home +void wgtAnsiTermClear(WidgetT *w); + +// Set the communications interface (NULL function pointers = disconnected) +void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)); + +// Set the scrollback buffer size in lines (default 500). Clears existing scrollback. +void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); + +// Poll the comm interface for incoming data and process it. Returns bytes processed. +int32_t wgtAnsiTermPoll(WidgetT *w); + // ============================================================ // Operations // ============================================================ diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c new file mode 100644 index 0000000..b2e30ff --- /dev/null +++ b/dvx/widgets/widgetAnsiTerm.c @@ -0,0 +1,1145 @@ +// widgetAnsiTerm.c — ANSI BBS terminal emulator widget + +#include "widgetInternal.h" + + +// ============================================================ +// Constants +// ============================================================ + +#define ANSI_BORDER 2 +#define ANSI_MAX_PARAMS 8 +#define ANSI_SB_W 14 +#define ANSI_DEFAULT_SCROLLBACK 500 + +#define PARSE_NORMAL 0 +#define PARSE_ESC 1 +#define PARSE_CSI 2 + +// Default attribute: light gray on black +#define ANSI_DEFAULT_ATTR 0x07 + +// ============================================================ +// CGA palette (RGB values for 16 standard colors) +// ============================================================ + +static const uint8_t sCgaPalette[16][3] = { + { 0, 0, 0}, // 0: black + { 0, 0, 170}, // 1: blue + { 0, 170, 0}, // 2: green + { 0, 170, 170}, // 3: cyan + {170, 0, 0}, // 4: red + {170, 0, 170}, // 5: magenta + {170, 85, 0}, // 6: brown + {170, 170, 170}, // 7: light gray + { 85, 85, 85}, // 8: dark gray + { 85, 85, 255}, // 9: bright blue + { 85, 255, 85}, // 10: bright green + { 85, 255, 255}, // 11: bright cyan + {255, 85, 85}, // 12: bright red + {255, 85, 255}, // 13: bright magenta + {255, 255, 85}, // 14: bright yellow + {255, 255, 255}, // 15: bright white +}; + +// ANSI SGR color index to CGA color index +static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 }; + + +// ============================================================ +// Prototypes +// ============================================================ + +static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow); +static void ansiTermDeleteLines(WidgetT *w, int32_t count); +static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd); +static void ansiTermEraseDisplay(WidgetT *w, int32_t mode); +static void ansiTermEraseLine(WidgetT *w, int32_t mode); +static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count); +static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex); +static void ansiTermInsertLines(WidgetT *w, int32_t count); +static void ansiTermNewline(WidgetT *w); +static void ansiTermProcessByte(WidgetT *w, uint8_t ch); +static void ansiTermProcessSgr(WidgetT *w); +static void ansiTermPutChar(WidgetT *w, uint8_t ch); +static void ansiTermScrollDown(WidgetT *w); +static void ansiTermScrollUp(WidgetT *w); + + +// ============================================================ +// ansiTermAddToScrollback +// ============================================================ +// +// Copy a screen row into the scrollback circular buffer. + +static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { + if (!w->as.ansiTerm.scrollback || w->as.ansiTerm.scrollbackMax <= 0) { + return; + } + + int32_t cols = w->as.ansiTerm.cols; + int32_t bytesPerRow = cols * 2; + int32_t head = w->as.ansiTerm.scrollbackHead; + + memcpy(w->as.ansiTerm.scrollback + head * bytesPerRow, + w->as.ansiTerm.cells + screenRow * bytesPerRow, + bytesPerRow); + + w->as.ansiTerm.scrollbackHead = (head + 1) % w->as.ansiTerm.scrollbackMax; + + if (w->as.ansiTerm.scrollbackCount < w->as.ansiTerm.scrollbackMax) { + w->as.ansiTerm.scrollbackCount++; + } +} + + +// ============================================================ +// ansiTermDeleteLines +// ============================================================ + +static void ansiTermDeleteLines(WidgetT *w, int32_t count) { + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + int32_t row = w->as.ansiTerm.cursorRow; + + if (count > rows - row) { + count = rows - row; + } + + if (count <= 0) { + return; + } + + uint8_t *cells = w->as.ansiTerm.cells; + + // Shift lines up + int32_t bytesPerRow = cols * 2; + memmove(cells + row * bytesPerRow, + cells + (row + count) * bytesPerRow, + (rows - row - count) * bytesPerRow); + + // Clear the bottom lines + for (int32_t r = rows - count; r < rows; r++) { + ansiTermFillCells(w, r * cols, cols); + } +} + + +// ============================================================ +// ansiTermDispatchCsi +// ============================================================ + +static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { + int32_t *p = w->as.ansiTerm.params; + int32_t n = w->as.ansiTerm.paramCount; + + // DEC private modes (ESC[?...) + if (w->as.ansiTerm.csiPrivate) { + int32_t mode = (n >= 1) ? p[0] : 0; + + if (cmd == 'h') { + if (mode == 7) { + w->as.ansiTerm.wrapMode = true; + } + + if (mode == 25) { + w->as.ansiTerm.cursorVisible = true; + } + } else if (cmd == 'l') { + if (mode == 7) { + w->as.ansiTerm.wrapMode = false; + } + + if (mode == 25) { + w->as.ansiTerm.cursorVisible = false; + } + } + + return; + } + + switch (cmd) { + case 'A': // CUU - cursor up + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + w->as.ansiTerm.cursorRow -= count; + + if (w->as.ansiTerm.cursorRow < 0) { + w->as.ansiTerm.cursorRow = 0; + } + + break; + } + + case 'B': // CUD - cursor down + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + w->as.ansiTerm.cursorRow += count; + + if (w->as.ansiTerm.cursorRow >= w->as.ansiTerm.rows) { + w->as.ansiTerm.cursorRow = w->as.ansiTerm.rows - 1; + } + + break; + } + + case 'C': // CUF - cursor forward + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + w->as.ansiTerm.cursorCol += count; + + if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) { + w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1; + } + + break; + } + + case 'D': // CUB - cursor back + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + w->as.ansiTerm.cursorCol -= count; + + if (w->as.ansiTerm.cursorCol < 0) { + w->as.ansiTerm.cursorCol = 0; + } + + break; + } + + case 'H': // CUP - cursor position + case 'f': // HVP - same + { + int32_t row = (n >= 1 && p[0]) ? p[0] - 1 : 0; + int32_t col = (n >= 2 && p[1]) ? p[1] - 1 : 0; + + if (row < 0) { + row = 0; + } + + if (row >= w->as.ansiTerm.rows) { + row = w->as.ansiTerm.rows - 1; + } + + if (col < 0) { + col = 0; + } + + if (col >= w->as.ansiTerm.cols) { + col = w->as.ansiTerm.cols - 1; + } + + w->as.ansiTerm.cursorRow = row; + w->as.ansiTerm.cursorCol = col; + break; + } + + case 'J': // ED - erase display + { + int32_t mode = (n >= 1) ? p[0] : 0; + ansiTermEraseDisplay(w, mode); + break; + } + + case 'K': // EL - erase line + { + int32_t mode = (n >= 1) ? p[0] : 0; + ansiTermEraseLine(w, mode); + break; + } + + case 'L': // IL - insert lines + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + ansiTermInsertLines(w, count); + break; + } + + case 'M': // DL - delete lines + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + ansiTermDeleteLines(w, count); + break; + } + + case 'S': // SU - scroll up + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + + for (int32_t i = 0; i < count; i++) { + ansiTermScrollUp(w); + } + + break; + } + + case 'T': // SD - scroll down + { + int32_t count = (n >= 1 && p[0]) ? p[0] : 1; + + for (int32_t i = 0; i < count; i++) { + ansiTermScrollDown(w); + } + + break; + } + + case 'm': // SGR - select graphic rendition + ansiTermProcessSgr(w); + break; + + case 's': // SCP - save cursor position + w->as.ansiTerm.savedRow = w->as.ansiTerm.cursorRow; + w->as.ansiTerm.savedCol = w->as.ansiTerm.cursorCol; + break; + + case 'u': // RCP - restore cursor position + w->as.ansiTerm.cursorRow = w->as.ansiTerm.savedRow; + w->as.ansiTerm.cursorCol = w->as.ansiTerm.savedCol; + break; + + default: + break; + } +} + + +// ============================================================ +// ansiTermEraseDisplay +// ============================================================ + +static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + int32_t cur = w->as.ansiTerm.cursorRow * cols + w->as.ansiTerm.cursorCol; + + if (mode == 0) { + // Erase from cursor to end of screen + ansiTermFillCells(w, cur, cols * rows - cur); + } else if (mode == 1) { + // Erase from start to cursor + ansiTermFillCells(w, 0, cur + 1); + } else if (mode == 2) { + // Erase entire screen — push all lines to scrollback first + bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + + for (int32_t r = 0; r < rows; r++) { + ansiTermAddToScrollback(w, r); + } + + if (wasAtBottom) { + w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; + } + + ansiTermFillCells(w, 0, cols * rows); + } +} + + +// ============================================================ +// ansiTermEraseLine +// ============================================================ + +static void ansiTermEraseLine(WidgetT *w, int32_t mode) { + int32_t cols = w->as.ansiTerm.cols; + int32_t row = w->as.ansiTerm.cursorRow; + int32_t col = w->as.ansiTerm.cursorCol; + int32_t base = row * cols; + + if (mode == 0) { + // Erase from cursor to end of line + ansiTermFillCells(w, base + col, cols - col); + } else if (mode == 1) { + // Erase from start of line to cursor + ansiTermFillCells(w, base, col + 1); + } else if (mode == 2) { + // Erase entire line + ansiTermFillCells(w, base, cols); + } +} + + +// ============================================================ +// ansiTermFillCells +// ============================================================ +// +// Fill a range of cells with space + current attribute. + +static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { + uint8_t *cells = w->as.ansiTerm.cells; + uint8_t attr = w->as.ansiTerm.curAttr; + int32_t total = w->as.ansiTerm.cols * w->as.ansiTerm.rows; + + if (start < 0) { + count += start; + start = 0; + } + + if (start + count > total) { + count = total - start; + } + + for (int32_t i = 0; i < count; i++) { + cells[(start + i) * 2] = ' '; + cells[(start + i) * 2 + 1] = attr; + } +} + + +// ============================================================ +// ansiTermGetLine +// ============================================================ +// +// Get a pointer to the cell data for a given line index in the +// combined scrollback+screen view. +// lineIndex < scrollbackCount → scrollback line +// lineIndex >= scrollbackCount → screen line + +static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { + int32_t cols = w->as.ansiTerm.cols; + int32_t bytesPerRow = cols * 2; + int32_t sbCount = w->as.ansiTerm.scrollbackCount; + + if (lineIndex < sbCount) { + // Scrollback line (circular buffer) + int32_t sbMax = w->as.ansiTerm.scrollbackMax; + int32_t actual = (w->as.ansiTerm.scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; + return w->as.ansiTerm.scrollback + actual * bytesPerRow; + } + + // Screen line + int32_t screenRow = lineIndex - sbCount; + return w->as.ansiTerm.cells + screenRow * bytesPerRow; +} + + +// ============================================================ +// ansiTermInsertLines +// ============================================================ + +static void ansiTermInsertLines(WidgetT *w, int32_t count) { + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + int32_t row = w->as.ansiTerm.cursorRow; + + if (count > rows - row) { + count = rows - row; + } + + if (count <= 0) { + return; + } + + uint8_t *cells = w->as.ansiTerm.cells; + + // Shift lines down + int32_t bytesPerRow = cols * 2; + memmove(cells + (row + count) * bytesPerRow, + cells + row * bytesPerRow, + (rows - row - count) * bytesPerRow); + + // Clear the inserted lines + for (int32_t r = row; r < row + count; r++) { + ansiTermFillCells(w, r * cols, cols); + } +} + + +// ============================================================ +// ansiTermNewline +// ============================================================ +// +// Move cursor to next line, scrolling if at the bottom. + +static void ansiTermNewline(WidgetT *w) { + w->as.ansiTerm.cursorRow++; + + if (w->as.ansiTerm.cursorRow >= w->as.ansiTerm.rows) { + w->as.ansiTerm.cursorRow = w->as.ansiTerm.rows - 1; + ansiTermScrollUp(w); + } +} + + +// ============================================================ +// ansiTermProcessByte +// ============================================================ +// +// Feed one byte through the ANSI parser state machine. + +static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { + switch (w->as.ansiTerm.parseState) { + case PARSE_NORMAL: + if (ch == 0x1B) { + w->as.ansiTerm.parseState = PARSE_ESC; + } else if (ch == '\r') { + w->as.ansiTerm.cursorCol = 0; + } else if (ch == '\n') { + ansiTermNewline(w); + } else if (ch == '\b') { + if (w->as.ansiTerm.cursorCol > 0) { + w->as.ansiTerm.cursorCol--; + } + } else if (ch == '\t') { + w->as.ansiTerm.cursorCol = (w->as.ansiTerm.cursorCol + 8) & ~7; + + if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) { + w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1; + } + } else if (ch == '\a') { + // Bell — ignored + } else if (ch >= 32 || ch >= 128) { + ansiTermPutChar(w, ch); + } + break; + + case PARSE_ESC: + if (ch == '[') { + w->as.ansiTerm.parseState = PARSE_CSI; + w->as.ansiTerm.paramCount = 0; + w->as.ansiTerm.csiPrivate = false; + memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params)); + } else { + // Unknown escape — return to normal + w->as.ansiTerm.parseState = PARSE_NORMAL; + } + break; + + case PARSE_CSI: + if (ch == '?') { + w->as.ansiTerm.csiPrivate = true; + } else if (ch >= '0' && ch <= '9') { + if (w->as.ansiTerm.paramCount == 0) { + w->as.ansiTerm.paramCount = 1; + } + + int32_t idx = w->as.ansiTerm.paramCount - 1; + + if (idx < ANSI_MAX_PARAMS) { + w->as.ansiTerm.params[idx] = w->as.ansiTerm.params[idx] * 10 + (ch - '0'); + } + } else if (ch == ';') { + if (w->as.ansiTerm.paramCount < ANSI_MAX_PARAMS) { + w->as.ansiTerm.paramCount++; + } + } else if (ch >= 0x40 && ch <= 0x7E) { + // Final byte — dispatch the CSI sequence + ansiTermDispatchCsi(w, ch); + w->as.ansiTerm.parseState = PARSE_NORMAL; + } else { + // Unexpected byte — abort sequence + w->as.ansiTerm.parseState = PARSE_NORMAL; + } + break; + } +} + + +// ============================================================ +// ansiTermProcessSgr +// ============================================================ +// +// Handle SGR (Select Graphic Rendition) escape sequence. + +static void ansiTermProcessSgr(WidgetT *w) { + if (w->as.ansiTerm.paramCount == 0) { + // ESC[m with no params = reset + w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm.bold = false; + return; + } + + for (int32_t i = 0; i < w->as.ansiTerm.paramCount; i++) { + int32_t code = w->as.ansiTerm.params[i]; + uint8_t fg = w->as.ansiTerm.curAttr & 0x0F; + uint8_t bg = (w->as.ansiTerm.curAttr >> 4) & 0x0F; + + if (code == 0) { + fg = 7; + bg = 0; + w->as.ansiTerm.bold = false; + } else if (code == 1) { + w->as.ansiTerm.bold = true; + fg |= 8; + } else if (code == 5) { + // Blink — use bright background + bg |= 8; + } else if (code == 7) { + // Reverse video + uint8_t tmp = fg; + fg = bg; + bg = tmp; + } else if (code == 22) { + // Normal intensity + w->as.ansiTerm.bold = false; + fg &= 7; + } else if (code >= 30 && code <= 37) { + fg = (uint8_t)sAnsiToCga[code - 30]; + + if (w->as.ansiTerm.bold) { + fg |= 8; + } + } else if (code >= 40 && code <= 47) { + bg = (uint8_t)sAnsiToCga[code - 40]; + } else if (code >= 90 && code <= 97) { + // Bright foreground + fg = (uint8_t)(sAnsiToCga[code - 90] | 8); + } else if (code >= 100 && code <= 107) { + // Bright background + bg = (uint8_t)(sAnsiToCga[code - 100] | 8); + } + + w->as.ansiTerm.curAttr = (uint8_t)((bg << 4) | fg); + } +} + + +// ============================================================ +// ansiTermPutChar +// ============================================================ +// +// Place a character at the cursor and advance. + +static void ansiTermPutChar(WidgetT *w, uint8_t ch) { + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + int32_t row = w->as.ansiTerm.cursorRow; + int32_t col = w->as.ansiTerm.cursorCol; + + if (row >= 0 && row < rows && col >= 0 && col < cols) { + int32_t idx = (row * cols + col) * 2; + w->as.ansiTerm.cells[idx] = ch; + w->as.ansiTerm.cells[idx + 1] = w->as.ansiTerm.curAttr; + } + + w->as.ansiTerm.cursorCol++; + + if (w->as.ansiTerm.cursorCol >= cols) { + if (w->as.ansiTerm.wrapMode) { + w->as.ansiTerm.cursorCol = 0; + ansiTermNewline(w); + } else { + w->as.ansiTerm.cursorCol = cols - 1; + } + } +} + + +// ============================================================ +// ansiTermScrollDown +// ============================================================ + +static void ansiTermScrollDown(WidgetT *w) { + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + uint8_t *cells = w->as.ansiTerm.cells; + int32_t bytesPerRow = cols * 2; + + // Shift all lines down by one + memmove(cells + bytesPerRow, cells, (rows - 1) * bytesPerRow); + + // Clear the top line + ansiTermFillCells(w, 0, cols); +} + + +// ============================================================ +// ansiTermScrollUp +// ============================================================ +// +// Scroll the screen up by one line. The top line is pushed into +// the scrollback buffer before being discarded. + +static void ansiTermScrollUp(WidgetT *w) { + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + uint8_t *cells = w->as.ansiTerm.cells; + int32_t bytesPerRow = cols * 2; + + // Track whether the view was following live output + bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + + // Push top line to scrollback + ansiTermAddToScrollback(w, 0); + + // Shift all lines up by one + memmove(cells, cells + bytesPerRow, (rows - 1) * bytesPerRow); + + // Clear the bottom line + ansiTermFillCells(w, (rows - 1) * cols, cols); + + // Keep view at bottom if it was following live output + if (wasAtBottom) { + w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; + } +} + + +// ============================================================ +// wgtAnsiTerm +// ============================================================ + +WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { + if (!parent) { + return NULL; + } + + if (cols <= 0) { + cols = 80; + } + + if (rows <= 0) { + rows = 25; + } + + WidgetT *w = widgetAlloc(parent, WidgetAnsiTermE); + + if (!w) { + return NULL; + } + + int32_t cellCount = cols * rows; + w->as.ansiTerm.cells = (uint8_t *)malloc(cellCount * 2); + + if (!w->as.ansiTerm.cells) { + free(w); + return NULL; + } + + // Allocate scrollback buffer + int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; + w->as.ansiTerm.scrollback = (uint8_t *)malloc(sbMax * cols * 2); + + if (!w->as.ansiTerm.scrollback) { + free(w->as.ansiTerm.cells); + free(w); + return NULL; + } + + w->as.ansiTerm.cols = cols; + w->as.ansiTerm.rows = rows; + w->as.ansiTerm.cursorRow = 0; + w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm.cursorVisible = true; + w->as.ansiTerm.wrapMode = true; + w->as.ansiTerm.bold = false; + w->as.ansiTerm.csiPrivate = false; + w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm.paramCount = 0; + w->as.ansiTerm.savedRow = 0; + w->as.ansiTerm.savedCol = 0; + w->as.ansiTerm.scrollbackMax = sbMax; + w->as.ansiTerm.scrollbackCount = 0; + w->as.ansiTerm.scrollbackHead = 0; + w->as.ansiTerm.scrollPos = 0; + w->as.ansiTerm.commCtx = NULL; + w->as.ansiTerm.commRead = NULL; + w->as.ansiTerm.commWrite = NULL; + + memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params)); + + // Initialize all cells to space with default attribute + for (int32_t i = 0; i < cellCount; i++) { + w->as.ansiTerm.cells[i * 2] = ' '; + w->as.ansiTerm.cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; + } + + return w; +} + + +// ============================================================ +// wgtAnsiTermClear +// ============================================================ + +void wgtAnsiTermClear(WidgetT *w) { + if (!w || w->type != WidgetAnsiTermE) { + return; + } + + int32_t rows = w->as.ansiTerm.rows; + int32_t cols = w->as.ansiTerm.cols; + + // Push all visible lines to scrollback + bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + + for (int32_t r = 0; r < rows; r++) { + ansiTermAddToScrollback(w, r); + } + + if (wasAtBottom) { + w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; + } + + // Clear the screen + int32_t cellCount = cols * rows; + + for (int32_t i = 0; i < cellCount; i++) { + w->as.ansiTerm.cells[i * 2] = ' '; + w->as.ansiTerm.cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; + } + + w->as.ansiTerm.cursorRow = 0; + w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm.bold = false; + w->as.ansiTerm.parseState = PARSE_NORMAL; +} + + +// ============================================================ +// wgtAnsiTermSetScrollback +// ============================================================ + +void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { + if (!w || w->type != WidgetAnsiTermE || maxLines <= 0) { + return; + } + + int32_t cols = w->as.ansiTerm.cols; + uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); + + if (!newBuf) { + return; + } + + free(w->as.ansiTerm.scrollback); + w->as.ansiTerm.scrollback = newBuf; + w->as.ansiTerm.scrollbackMax = maxLines; + w->as.ansiTerm.scrollbackCount = 0; + w->as.ansiTerm.scrollbackHead = 0; + w->as.ansiTerm.scrollPos = 0; +} + + +// ============================================================ +// wgtAnsiTermPoll +// ============================================================ + +int32_t wgtAnsiTermPoll(WidgetT *w) { + if (!w || w->type != WidgetAnsiTermE || !w->as.ansiTerm.commRead) { + return 0; + } + + uint8_t buf[256]; + int32_t n = w->as.ansiTerm.commRead(w->as.ansiTerm.commCtx, buf, (int32_t)sizeof(buf)); + + if (n > 0) { + wgtAnsiTermWrite(w, buf, n); + } + + return n > 0 ? n : 0; +} + + +// ============================================================ +// wgtAnsiTermSetComm +// ============================================================ + +void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { + if (!w || w->type != WidgetAnsiTermE) { + return; + } + + w->as.ansiTerm.commCtx = ctx; + w->as.ansiTerm.commRead = readFn; + w->as.ansiTerm.commWrite = writeFn; +} + + +// ============================================================ +// wgtAnsiTermWrite +// ============================================================ + +void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { + if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) { + return; + } + + for (int32_t i = 0; i < len; i++) { + ansiTermProcessByte(w, data[i]); + } +} + + +// ============================================================ +// widgetAnsiTermCalcMinSize +// ============================================================ + +void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { + w->calcMinW = w->as.ansiTerm.cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; + w->calcMinH = w->as.ansiTerm.rows * font->charHeight + ANSI_BORDER * 2; +} + + +// ============================================================ +// widgetAnsiTermOnKey +// ============================================================ +// +// Translate keyboard input to ANSI escape sequences and send +// via the comm interface. Does nothing if commWrite is NULL. + +void widgetAnsiTermOnKey(WidgetT *w, int32_t key) { + if (!w->as.ansiTerm.commWrite) { + return; + } + + uint8_t buf[8]; + int32_t len = 0; + + if (key >= 32 && key < 127) { + // Printable ASCII + buf[0] = (uint8_t)key; + len = 1; + } else if (key == 13 || key == 10) { + // Enter + buf[0] = '\r'; + len = 1; + } else if (key == 8) { + // Backspace + buf[0] = 0x08; + len = 1; + } else if (key == (0x48 | 0x100)) { + // Up arrow → ESC[A + buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; + len = 3; + } else if (key == (0x50 | 0x100)) { + // Down arrow → ESC[B + buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; + len = 3; + } else if (key == (0x4D | 0x100)) { + // Right arrow → ESC[C + buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; + len = 3; + } else if (key == (0x4B | 0x100)) { + // Left arrow → ESC[D + buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; + len = 3; + } else if (key == (0x47 | 0x100)) { + // Home → ESC[H + buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; + len = 3; + } else if (key == (0x4F | 0x100)) { + // End → ESC[F + buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; + len = 3; + } else if (key == (0x49 | 0x100)) { + // PgUp → ESC[5~ + buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; + len = 4; + } else if (key == (0x51 | 0x100)) { + // PgDn → ESC[6~ + buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; + len = 4; + } else if (key == (0x53 | 0x100)) { + // Delete → ESC[3~ + buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; + len = 4; + } + + if (len > 0) { + w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len); + } +} + + +// ============================================================ +// widgetAnsiTermOnMouse +// ============================================================ +// +// Handle mouse clicks: scrollbar interaction and focus. + +void widgetAnsiTermOnMouse(WidgetT *hit, int32_t vx, int32_t vy, const BitmapFontT *font) { + hit->focused = true; + + int32_t sbCount = hit->as.ansiTerm.scrollbackCount; + + if (sbCount == 0) { + return; + } + + // Scrollbar geometry + int32_t cols = hit->as.ansiTerm.cols; + int32_t rows = hit->as.ansiTerm.rows; + int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; + int32_t sbY = hit->y + ANSI_BORDER; + int32_t sbH = rows * font->charHeight; + int32_t arrowH = ANSI_SB_W; + + if (vx < sbX || vx >= sbX + ANSI_SB_W) { + return; + } + + int32_t maxScroll = sbCount; + + if (vy >= sbY && vy < sbY + arrowH) { + // Up arrow + hit->as.ansiTerm.scrollPos--; + } else if (vy >= sbY + sbH - arrowH && vy < sbY + sbH) { + // Down arrow + hit->as.ansiTerm.scrollPos++; + } else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { + // Track area — compute thumb position to determine page direction + int32_t trackY = sbY + arrowH; + int32_t trackH = sbH - arrowH * 2; + + if (trackH <= 0) { + return; + } + + int32_t totalLines = sbCount + rows; + int32_t thumbH = (rows * trackH) / totalLines; + + if (thumbH < 8) { + thumbH = 8; + } + + int32_t thumbRange = trackH - thumbH; + int32_t thumbY = trackY; + + if (maxScroll > 0 && thumbRange > 0) { + thumbY = trackY + (hit->as.ansiTerm.scrollPos * thumbRange) / maxScroll; + } + + if (vy < thumbY) { + // Page up + hit->as.ansiTerm.scrollPos -= rows; + } else if (vy >= thumbY + thumbH) { + // Page down + hit->as.ansiTerm.scrollPos += rows; + } + } + + // Clamp + if (hit->as.ansiTerm.scrollPos < 0) { + hit->as.ansiTerm.scrollPos = 0; + } + + if (hit->as.ansiTerm.scrollPos > maxScroll) { + hit->as.ansiTerm.scrollPos = maxScroll; + } +} + + +// ============================================================ +// widgetAnsiTermPaint +// ============================================================ + +void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + // Draw sunken bevel border + BevelStyleT bevel; + bevel.highlight = colors->windowShadow; + bevel.shadow = colors->windowHighlight; + bevel.face = 0; + bevel.width = ANSI_BORDER; + drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); + + // Build the 16-color packed palette for this display format + uint32_t palette[16]; + + for (int32_t i = 0; i < 16; i++) { + palette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); + } + + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + int32_t cellW = font->charWidth; + int32_t cellH = font->charHeight; + int32_t baseX = w->x + ANSI_BORDER; + int32_t baseY = w->y + ANSI_BORDER; + int32_t sbCount = w->as.ansiTerm.scrollbackCount; + + // Determine if viewing live terminal or scrollback + bool viewingLive = (w->as.ansiTerm.scrollPos == sbCount); + + // Render character cells + for (int32_t row = 0; row < rows; row++) { + int32_t lineIndex = w->as.ansiTerm.scrollPos + row; + const uint8_t *lineData = ansiTermGetLine(w, lineIndex); + + for (int32_t col = 0; col < cols; col++) { + uint8_t ch = lineData[col * 2]; + uint8_t attr = lineData[col * 2 + 1]; + + uint32_t fg = palette[attr & 0x0F]; + uint32_t bg = palette[(attr >> 4) & 0x0F]; + + // Draw cursor as inverse block (only when viewing live terminal) + if (viewingLive && w->as.ansiTerm.cursorVisible && w->focused && + row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { + uint32_t tmp = fg; + fg = bg; + bg = tmp; + } + + int32_t cx = baseX + col * cellW; + int32_t cy = baseY + row * cellH; + + drawChar(d, ops, font, cx, cy, (char)ch, fg, bg, true); + } + } + + // Draw scrollbar + int32_t sbX = baseX + cols * cellW; + int32_t sbY = baseY; + int32_t sbW = ANSI_SB_W; + int32_t sbH = rows * cellH; + int32_t arrowH = ANSI_SB_W; + + if (sbCount == 0) { + // No scrollback — fill scrollbar area with trough color + rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough); + return; + } + + // Track background + int32_t trackY = sbY + arrowH; + int32_t trackH = sbH - arrowH * 2; + rectFill(d, ops, sbX, trackY, sbW, trackH, colors->scrollbarTrough); + + // Up arrow button + BevelStyleT btnBevel; + btnBevel.highlight = colors->windowHighlight; + btnBevel.shadow = colors->windowShadow; + btnBevel.face = colors->scrollbarBg; + btnBevel.width = 1; + drawBevel(d, ops, sbX, sbY, sbW, arrowH, &btnBevel); + + // Up arrow glyph (small triangle) + int32_t arrowMidX = sbX + sbW / 2; + int32_t arrowMidY = sbY + arrowH / 2; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, arrowMidX - i, arrowMidY - 1 + i, i * 2 + 1, colors->scrollbarFg); + } + + // Down arrow button + drawBevel(d, ops, sbX, sbY + sbH - arrowH, sbW, arrowH, &btnBevel); + + arrowMidY = sbY + sbH - arrowH + arrowH / 2; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, arrowMidX - i, arrowMidY + 1 - i, i * 2 + 1, colors->scrollbarFg); + } + + // Thumb + if (trackH > 0) { + int32_t totalLines = sbCount + rows; + int32_t thumbH = (rows * trackH) / totalLines; + + if (thumbH < 8) { + thumbH = 8; + } + + int32_t thumbRange = trackH - thumbH; + int32_t maxScroll = sbCount; + int32_t thumbY = trackY; + + if (maxScroll > 0 && thumbRange > 0) { + thumbY = trackY + (w->as.ansiTerm.scrollPos * thumbRange) / maxScroll; + } + + drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); + } +} diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 0cfaa3d..051829a 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -97,6 +97,9 @@ void widgetDestroyChildren(WidgetT *w) { free(child->as.canvas.data); } else if (child->type == WidgetImageButtonE) { free(child->as.imageButton.data); + } else if (child->type == WidgetAnsiTermE) { + free(child->as.ansiTerm.cells); + free(child->as.ansiTerm.scrollback); } // Clear popup/drag references if they point to destroyed widgets diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 1bb5a54..15610a1 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -121,7 +121,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { while (top > 0) { WidgetT *w = stack[--top]; - if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE)) { + if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetAnsiTermE)) { focus = w; break; } @@ -137,6 +137,13 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { return; } + // Handle ANSI terminal key input + if (focus->type == WidgetAnsiTermE) { + widgetAnsiTermOnKey(focus, key); + wgtInvalidate(focus); + return; + } + // Handle text input for TextInput and ComboBox char *buf = NULL; int32_t bufSize = 0; @@ -544,6 +551,11 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { widgetTreeViewOnMouse(hit, root, vx, vy); } + if (hit->type == WidgetAnsiTermE && hit->enabled) { + AppContextT *actx = (AppContextT *)root->userData; + widgetAnsiTermOnMouse(hit, vx, vy, &actx->font); + } + wgtInvalidate(root); } diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index fe438ae..eaf78fe 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -97,6 +97,7 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const // Per-widget paint functions // ============================================================ +void widgetAnsiTermPaint(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); void widgetImageButtonPaint(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); @@ -122,6 +123,7 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Per-widget calcMinSize functions // ============================================================ +void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font); @@ -149,6 +151,8 @@ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font); // Per-widget mouse functions // ============================================================ +void widgetAnsiTermOnMouse(WidgetT *hit, int32_t vx, int32_t vy, const BitmapFontT *font); +void widgetAnsiTermOnKey(WidgetT *w, int32_t key); void widgetButtonOnMouse(WidgetT *hit); void widgetImageButtonOnMouse(WidgetT *hit); void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy); diff --git a/dvx/widgets/widgetLayout.c b/dvx/widgets/widgetLayout.c index 05a6799..225c06a 100644 --- a/dvx/widgets/widgetLayout.c +++ b/dvx/widgets/widgetLayout.c @@ -136,6 +136,9 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) { case WidgetSliderE: widgetSliderCalcMinSize(w, font); break; + case WidgetAnsiTermE: + widgetAnsiTermCalcMinSize(w, font); + break; case WidgetSeparatorE: if (w->as.separator.vertical) { w->calcMinW = SEPARATOR_THICKNESS; diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index 47bac28..d9bc78b 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -137,6 +137,10 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo } return; // handles its own children + case WidgetAnsiTermE: + widgetAnsiTermPaint(w, d, ops, font, colors); + break; + default: break; } @@ -209,6 +213,9 @@ void wgtDestroy(WidgetT *w) { free(w->as.image.data); } else if (w->type == WidgetCanvasE) { free(w->as.canvas.data); + } else if (w->type == WidgetAnsiTermE) { + free(w->as.ansiTerm.cells); + free(w->as.ansiTerm.scrollback); } // Clear static references