Added ANSIBBS widget.

This commit is contained in:
Scott Duensing 2026-03-10 19:42:33 -05:00
parent 856cc194b2
commit 2e45e4b14d
9 changed files with 1302 additions and 4 deletions

View file

@ -7,7 +7,7 @@ Motif-style beveled chrome, dirty-rectangle compositing, draggable and
resizable windows, dropdown menus, scrollbars, and a declarative widget/layout resizable windows, dropdown menus, scrollbars, and a declarative widget/layout
system with buttons, checkboxes, radios, text inputs, dropdowns, combo boxes, system with buttons, checkboxes, radios, text inputs, dropdowns, combo boxes,
sliders, progress bars, tab controls, tree views, toolbars, status bars, 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 ## 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 color. All operations clip to the canvas bounds. Colors are in display
pixel format (use `packColor()` to create them). 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 ### Spacing and dividers
```c ```c

View file

@ -13,7 +13,8 @@ LIBDIR = ../lib
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c 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/widgetLayout.c \
widgets/widgetEvent.c \ widgets/widgetEvent.c \
widgets/widgetOps.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 file dependencies
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h 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)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS) $(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetEvent.o: widgets/widgetEvent.c $(WIDGET_DEPS) $(WOBJDIR)/widgetEvent.o: widgets/widgetEvent.c $(WIDGET_DEPS)

View file

@ -63,7 +63,8 @@ typedef enum {
WidgetTreeItemE, WidgetTreeItemE,
WidgetImageE, WidgetImageE,
WidgetImageButtonE, WidgetImageButtonE,
WidgetCanvasE WidgetCanvasE,
WidgetAnsiTermE
} WidgetTypeE; } WidgetTypeE;
// ============================================================ // ============================================================
@ -283,6 +284,34 @@ typedef struct WidgetT {
int32_t lastX; int32_t lastX;
int32_t lastY; int32_t lastY;
} canvas; } 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; } as;
} WidgetT; } 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); 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); 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 // Operations
// ============================================================ // ============================================================

1145
dvx/widgets/widgetAnsiTerm.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -97,6 +97,9 @@ void widgetDestroyChildren(WidgetT *w) {
free(child->as.canvas.data); free(child->as.canvas.data);
} else if (child->type == WidgetImageButtonE) { } else if (child->type == WidgetImageButtonE) {
free(child->as.imageButton.data); 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 // Clear popup/drag references if they point to destroyed widgets

View file

@ -121,7 +121,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
while (top > 0) { while (top > 0) {
WidgetT *w = stack[--top]; 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; focus = w;
break; break;
} }
@ -137,6 +137,13 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return; return;
} }
// Handle ANSI terminal key input
if (focus->type == WidgetAnsiTermE) {
widgetAnsiTermOnKey(focus, key);
wgtInvalidate(focus);
return;
}
// Handle text input for TextInput and ComboBox // Handle text input for TextInput and ComboBox
char *buf = NULL; char *buf = NULL;
int32_t bufSize = 0; 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); widgetTreeViewOnMouse(hit, root, vx, vy);
} }
if (hit->type == WidgetAnsiTermE && hit->enabled) {
AppContextT *actx = (AppContextT *)root->userData;
widgetAnsiTermOnMouse(hit, vx, vy, &actx->font);
}
wgtInvalidate(root); wgtInvalidate(root);
} }

View file

@ -97,6 +97,7 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const
// Per-widget paint functions // 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 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 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); 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 // Per-widget calcMinSize functions
// ============================================================ // ============================================================
void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetCanvasCalcMinSize(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 // 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 widgetButtonOnMouse(WidgetT *hit);
void widgetImageButtonOnMouse(WidgetT *hit); void widgetImageButtonOnMouse(WidgetT *hit);
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy); void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy);

View file

@ -136,6 +136,9 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
case WidgetSliderE: case WidgetSliderE:
widgetSliderCalcMinSize(w, font); widgetSliderCalcMinSize(w, font);
break; break;
case WidgetAnsiTermE:
widgetAnsiTermCalcMinSize(w, font);
break;
case WidgetSeparatorE: case WidgetSeparatorE:
if (w->as.separator.vertical) { if (w->as.separator.vertical) {
w->calcMinW = SEPARATOR_THICKNESS; w->calcMinW = SEPARATOR_THICKNESS;

View file

@ -137,6 +137,10 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
} }
return; // handles its own children return; // handles its own children
case WidgetAnsiTermE:
widgetAnsiTermPaint(w, d, ops, font, colors);
break;
default: default:
break; break;
} }
@ -209,6 +213,9 @@ void wgtDestroy(WidgetT *w) {
free(w->as.image.data); free(w->as.image.data);
} else if (w->type == WidgetCanvasE) { } else if (w->type == WidgetCanvasE) {
free(w->as.canvas.data); free(w->as.canvas.data);
} else if (w->type == WidgetAnsiTermE) {
free(w->as.ansiTerm.cells);
free(w->as.ansiTerm.scrollback);
} }
// Clear static references // Clear static references