Added additional ANSIBBS sequences and made dvxUpate handle terminal widget polling.

This commit is contained in:
Scott Duensing 2026-03-11 20:57:41 -05:00
parent eeb6541af3
commit 324382d758
5 changed files with 314 additions and 109 deletions

View file

@ -24,6 +24,8 @@ static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void initColorScheme(AppContextT *ctx);
static void initMouse(AppContextT *ctx);
static void pollAnsiTermWidgets(AppContextT *ctx);
static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win);
static void pollKeyboard(AppContextT *ctx);
static void pollMouse(AppContextT *ctx);
static void refreshMinimizedIcons(AppContextT *ctx);
@ -536,6 +538,7 @@ bool dvxUpdate(AppContextT *ctx) {
pollMouse(ctx);
pollKeyboard(ctx);
dispatchEvents(ctx);
pollAnsiTermWidgets(ctx);
// Periodically refresh one minimized window thumbnail (staggered)
ctx->frameCount++;
@ -819,6 +822,41 @@ static void initMouse(AppContextT *ctx) {
}
// ============================================================
// pollAnsiTermWidgets — poll and repaint all ANSI term widgets
// ============================================================
static void pollAnsiTermWidgets(AppContextT *ctx) {
for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i];
if (win->widgetRoot) {
pollAnsiTermWidgetsWalk(ctx, win->widgetRoot, win);
}
}
}
// ============================================================
// pollAnsiTermWidgetsWalk — recursive helper
// ============================================================
static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) {
if (w->type == WidgetAnsiTermE) {
wgtAnsiTermPoll(w);
if (wgtAnsiTermRepaint(w) > 0) {
win->contentDirty = true;
dvxInvalidateWindow(ctx, win);
}
}
for (WidgetT *child = w->firstChild; child; child = child->nextSibling) {
pollAnsiTermWidgetsWalk(ctx, child, win);
}
}
// ============================================================
// pollKeyboard
// ============================================================

View file

@ -296,6 +296,7 @@ typedef struct WidgetT {
bool cursorVisible;
bool wrapMode; // auto-wrap at right margin
bool bold; // SGR bold flag (brightens foreground)
bool originMode; // cursor positioning relative to scroll region
bool csiPrivate; // '?' prefix in CSI sequence
uint8_t curAttr; // current text attribute (fg | bg<<4)
uint8_t parseState; // 0=normal, 1=ESC, 2=CSI
@ -303,6 +304,9 @@ typedef struct WidgetT {
int32_t paramCount; // number of CSI params collected
int32_t savedRow; // saved cursor position (SCP)
int32_t savedCol;
// Scrolling region (0-based, inclusive)
int32_t scrollTop; // top row of scroll region
int32_t scrollBot; // bottom row of scroll region
// Scrollback
uint8_t *scrollback; // circular buffer of scrollback lines
int32_t scrollbackMax; // max lines in scrollback buffer

View file

@ -61,7 +61,6 @@ static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 };
static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow);
static void ansiTermDeleteLines(WidgetT *w, int32_t count);
static void ansiTermDirtyAll(WidgetT *w);
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count);
static void ansiTermDirtyRow(WidgetT *w, int32_t row);
static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd);
@ -105,15 +104,6 @@ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) {
}
// ============================================================
// ansiTermDirtyAll
// ============================================================
static void ansiTermDirtyAll(WidgetT *w) {
w->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
}
// ============================================================
// ansiTermDirtyRange
// ============================================================
@ -148,11 +138,11 @@ static void ansiTermDirtyRow(WidgetT *w, int32_t row) {
static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
int32_t cols = w->as.ansiTerm.cols;
int32_t rows = w->as.ansiTerm.rows;
int32_t bot = w->as.ansiTerm.scrollBot;
int32_t row = w->as.ansiTerm.cursorRow;
if (count > rows - row) {
count = rows - row;
if (count > bot - row + 1) {
count = bot - row + 1;
}
if (count <= 0) {
@ -160,20 +150,22 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
}
uint8_t *cells = w->as.ansiTerm.cells;
// Shift lines up
int32_t bytesPerRow = cols * 2;
// Shift lines up within region
if (row + count <= bot) {
memmove(cells + row * bytesPerRow,
cells + (row + count) * bytesPerRow,
(rows - row - count) * bytesPerRow);
(bot - row - count + 1) * bytesPerRow);
}
// Clear the bottom lines
for (int32_t r = rows - count; r < rows; r++) {
// Clear the bottom lines of the region
for (int32_t r = bot - count + 1; r <= bot; r++) {
ansiTermFillCells(w, r * cols, cols);
}
// All rows from cursorRow down are affected
for (int32_t r = row; r < rows; r++) {
// Dirty affected rows
for (int32_t r = row; r <= bot; r++) {
ansiTermDirtyRow(w, r);
}
}
@ -192,6 +184,12 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
int32_t mode = (n >= 1) ? p[0] : 0;
if (cmd == 'h') {
if (mode == 6) {
w->as.ansiTerm.originMode = true;
w->as.ansiTerm.cursorRow = w->as.ansiTerm.scrollTop;
w->as.ansiTerm.cursorCol = 0;
}
if (mode == 7) {
w->as.ansiTerm.wrapMode = true;
}
@ -200,6 +198,12 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
w->as.ansiTerm.cursorVisible = true;
}
} else if (cmd == 'l') {
if (mode == 6) {
w->as.ansiTerm.originMode = false;
w->as.ansiTerm.cursorRow = 0;
w->as.ansiTerm.cursorCol = 0;
}
if (mode == 7) {
w->as.ansiTerm.wrapMode = false;
}
@ -213,13 +217,40 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
}
switch (cmd) {
case '@': // ICH - insert character
{
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
int32_t cols = w->as.ansiTerm.cols;
int32_t row = w->as.ansiTerm.cursorRow;
int32_t col = w->as.ansiTerm.cursorCol;
if (count > cols - col) {
count = cols - col;
}
if (count > 0) {
uint8_t *base = w->as.ansiTerm.cells + row * cols * 2;
memmove(base + (col + count) * 2, base + col * 2, (cols - col - count) * 2);
for (int32_t i = col; i < col + count; i++) {
base[i * 2] = ' ';
base[i * 2 + 1] = w->as.ansiTerm.curAttr;
}
ansiTermDirtyRow(w, row);
}
break;
}
case 'A': // CUU - cursor up
{
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
int32_t minRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollTop : 0;
w->as.ansiTerm.cursorRow -= count;
if (w->as.ansiTerm.cursorRow < 0) {
w->as.ansiTerm.cursorRow = 0;
if (w->as.ansiTerm.cursorRow < minRow) {
w->as.ansiTerm.cursorRow = minRow;
}
break;
@ -228,10 +259,11 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
case 'B': // CUD - cursor down
{
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
int32_t maxRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollBot : w->as.ansiTerm.rows - 1;
w->as.ansiTerm.cursorRow += count;
if (w->as.ansiTerm.cursorRow >= w->as.ansiTerm.rows) {
w->as.ansiTerm.cursorRow = w->as.ansiTerm.rows - 1;
if (w->as.ansiTerm.cursorRow > maxRow) {
w->as.ansiTerm.cursorRow = maxRow;
}
break;
@ -261,12 +293,44 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
break;
}
case 'c': // DA - device attributes
{
if (w->as.ansiTerm.commWrite) {
// Respond as VT100 with advanced video option
const uint8_t reply[] = "\033[?1;2c";
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, reply, 7);
}
break;
}
case 'E': // CNL - cursor next line
{
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
for (int32_t i = 0; i < count; i++) {
ansiTermNewline(w);
}
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;
// Origin mode: row is relative to scroll region
if (w->as.ansiTerm.originMode) {
row += w->as.ansiTerm.scrollTop;
if (row > w->as.ansiTerm.scrollBot) {
row = w->as.ansiTerm.scrollBot;
}
}
if (row < 0) {
row = 0;
}
@ -316,6 +380,32 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
break;
}
case 'P': // DCH - delete character
{
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
int32_t cols = w->as.ansiTerm.cols;
int32_t row = w->as.ansiTerm.cursorRow;
int32_t col = w->as.ansiTerm.cursorCol;
if (count > cols - col) {
count = cols - col;
}
if (count > 0) {
uint8_t *base = w->as.ansiTerm.cells + row * cols * 2;
memmove(base + col * 2, base + (col + count) * 2, (cols - col - count) * 2);
for (int32_t i = cols - count; i < cols; i++) {
base[i * 2] = ' ';
base[i * 2 + 1] = w->as.ansiTerm.curAttr;
}
ansiTermDirtyRow(w, row);
}
break;
}
case 'S': // SU - scroll up
{
int32_t count = (n >= 1 && p[0]) ? p[0] : 1;
@ -338,10 +428,72 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
break;
}
case 'Z': // CBT - back tab
{
int32_t col = w->as.ansiTerm.cursorCol;
if (col > 0) {
col = ((col - 1) / 8) * 8;
}
w->as.ansiTerm.cursorCol = col;
break;
}
case 'm': // SGR - select graphic rendition
ansiTermProcessSgr(w);
break;
case 'n': // DSR - device status report
{
int32_t mode = (n >= 1) ? p[0] : 0;
if (w->as.ansiTerm.commWrite) {
if (mode == 6) {
// CPR — cursor position report: ESC[row;colR (1-based)
char reply[16];
int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(w->as.ansiTerm.cursorRow + 1), (long)(w->as.ansiTerm.cursorCol + 1));
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, (const uint8_t *)reply, len);
} else if (mode == 255) {
// Screen size report
char reply[16];
int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm.rows, (long)w->as.ansiTerm.cols);
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, (const uint8_t *)reply, len);
}
}
break;
}
case 'r': // DECSTBM - set scrolling region
{
int32_t rows = w->as.ansiTerm.rows;
int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0;
int32_t bot = (n >= 2 && p[1]) ? p[1] - 1 : rows - 1;
if (top < 0) {
top = 0;
}
if (bot >= rows) {
bot = rows - 1;
}
if (top < bot) {
w->as.ansiTerm.scrollTop = top;
w->as.ansiTerm.scrollBot = bot;
} else {
// Invalid or reset — restore full screen
w->as.ansiTerm.scrollTop = 0;
w->as.ansiTerm.scrollBot = rows - 1;
}
// Home cursor (relative to region if origin mode)
w->as.ansiTerm.cursorRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollTop : 0;
w->as.ansiTerm.cursorCol = 0;
break;
}
case 's': // SCP - save cursor position
w->as.ansiTerm.savedRow = w->as.ansiTerm.cursorRow;
w->as.ansiTerm.savedCol = w->as.ansiTerm.cursorCol;
@ -475,11 +627,11 @@ static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) {
static void ansiTermInsertLines(WidgetT *w, int32_t count) {
int32_t cols = w->as.ansiTerm.cols;
int32_t rows = w->as.ansiTerm.rows;
int32_t bot = w->as.ansiTerm.scrollBot;
int32_t row = w->as.ansiTerm.cursorRow;
if (count > rows - row) {
count = rows - row;
if (count > bot - row + 1) {
count = bot - row + 1;
}
if (count <= 0) {
@ -487,20 +639,22 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) {
}
uint8_t *cells = w->as.ansiTerm.cells;
// Shift lines down
int32_t bytesPerRow = cols * 2;
// Shift lines down within region
if (row + count <= bot) {
memmove(cells + (row + count) * bytesPerRow,
cells + row * bytesPerRow,
(rows - row - count) * bytesPerRow);
(bot - row - count + 1) * bytesPerRow);
}
// Clear the inserted lines
for (int32_t r = row; r < row + count; r++) {
ansiTermFillCells(w, r * cols, cols);
}
// All rows from cursorRow down are affected
for (int32_t r = row; r < rows; r++) {
// Dirty affected rows
for (int32_t r = row; r <= bot; r++) {
ansiTermDirtyRow(w, r);
}
}
@ -513,11 +667,12 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) {
// Move cursor to next line, scrolling if at the bottom.
static void ansiTermNewline(WidgetT *w) {
w->as.ansiTerm.cursorRow++;
int32_t bot = w->as.ansiTerm.scrollBot;
if (w->as.ansiTerm.cursorRow >= w->as.ansiTerm.rows) {
w->as.ansiTerm.cursorRow = w->as.ansiTerm.rows - 1;
if (w->as.ansiTerm.cursorRow == bot) {
ansiTermScrollUp(w);
} else if (w->as.ansiTerm.cursorRow < w->as.ansiTerm.rows - 1) {
w->as.ansiTerm.cursorRow++;
}
}
@ -547,6 +702,11 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) {
w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1;
}
} else if (ch == '\f') {
// Form feed — clear screen and home cursor
ansiTermEraseDisplay(w, 2);
w->as.ansiTerm.cursorRow = 0;
w->as.ansiTerm.cursorCol = 0;
} else if (ch == '\a') {
// Bell — ignored
} else {
@ -562,6 +722,27 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
w->as.ansiTerm.paramCount = 0;
w->as.ansiTerm.csiPrivate = false;
memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params));
} else if (ch == 'D') {
// IND — scroll up one line
ansiTermScrollUp(w);
w->as.ansiTerm.parseState = PARSE_NORMAL;
} else if (ch == 'M') {
// RI — scroll down one line
ansiTermScrollDown(w);
w->as.ansiTerm.parseState = PARSE_NORMAL;
} else if (ch == 'c') {
// RIS — terminal reset
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.wrapMode = true;
w->as.ansiTerm.originMode = false;
w->as.ansiTerm.cursorVisible = true;
w->as.ansiTerm.scrollTop = 0;
w->as.ansiTerm.scrollBot = w->as.ansiTerm.rows - 1;
ansiTermEraseDisplay(w, 2);
w->as.ansiTerm.parseState = PARSE_NORMAL;
} else {
// Unknown escape — return to normal
w->as.ansiTerm.parseState = PARSE_NORMAL;
@ -635,6 +816,9 @@ static void ansiTermProcessSgr(WidgetT *w) {
uint8_t tmp = fg;
fg = bg;
bg = tmp;
} else if (code == 8) {
// Invisible — foreground same as background
fg = bg & 0x07;
} else if (code == 22) {
// Normal intensity
w->as.ansiTerm.bold = false;
@ -698,17 +882,25 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) {
static void ansiTermScrollDown(WidgetT *w) {
int32_t cols = w->as.ansiTerm.cols;
int32_t rows = w->as.ansiTerm.rows;
int32_t top = w->as.ansiTerm.scrollTop;
int32_t bot = w->as.ansiTerm.scrollBot;
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);
// Shift lines within region down by one
if (bot > top) {
memmove(cells + (top + 1) * bytesPerRow,
cells + top * bytesPerRow,
(bot - top) * bytesPerRow);
}
// Clear the top line
ansiTermFillCells(w, 0, cols);
// Clear the top line of the region
ansiTermFillCells(w, top * cols, cols);
ansiTermDirtyAll(w);
// Dirty affected rows
for (int32_t r = top; r <= bot && r < 32; r++) {
w->as.ansiTerm.dirtyRows |= (1U << r);
}
}
@ -721,28 +913,35 @@ static void ansiTermScrollDown(WidgetT *w) {
static void ansiTermScrollUp(WidgetT *w) {
int32_t cols = w->as.ansiTerm.cols;
int32_t rows = w->as.ansiTerm.rows;
int32_t top = w->as.ansiTerm.scrollTop;
int32_t bot = w->as.ansiTerm.scrollBot;
uint8_t *cells = w->as.ansiTerm.cells;
int32_t bytesPerRow = cols * 2;
// Track whether the view was following live output
// Only push to scrollback when scrolling the full screen
if (top == 0 && bot == w->as.ansiTerm.rows - 1) {
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;
}
}
ansiTermDirtyAll(w);
// Shift lines within region up by one
if (bot > top) {
memmove(cells + top * bytesPerRow,
cells + (top + 1) * bytesPerRow,
(bot - top) * bytesPerRow);
}
// Clear the bottom line of the region
ansiTermFillCells(w, bot * cols, cols);
// Dirty affected rows
for (int32_t r = top; r <= bot && r < 32; r++) {
w->as.ansiTerm.dirtyRows |= (1U << r);
}
}
@ -794,12 +993,15 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
w->as.ansiTerm.cursorVisible = true;
w->as.ansiTerm.wrapMode = true;
w->as.ansiTerm.bold = false;
w->as.ansiTerm.originMode = 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.scrollTop = 0;
w->as.ansiTerm.scrollBot = rows - 1;
w->as.ansiTerm.scrollbackMax = sbMax;
w->as.ansiTerm.scrollbackCount = 0;
w->as.ansiTerm.scrollbackHead = 0;

View file

@ -76,7 +76,6 @@ static int sClientFd = -1;
static int connectToBbs(const char *host, int port);
static int createListenSocket(int port);
static void hexDump(const char *label, const uint8_t *data, int len);
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel);
static void seedRng(void);
static void sigHandler(int sig);
@ -152,28 +151,11 @@ static int createListenSocket(int port) {
}
static void hexDump(const char *label, const uint8_t *data, int len) {
printf("%s (%d bytes):", label, len);
for (int i = 0; i < len && i < 64; i++) {
if (i % 16 == 0) {
printf("\n ");
}
printf("%02X ", data[i]);
}
if (len > 64) {
printf("\n ...");
}
printf("\n");
}
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) {
int bbsFd = *(int *)ctx;
(void)channel;
hexDump("DOS->proxy (decrypted)", data, len);
// Check for ENTER before BBS is connected
if (!sGotEnter) {
for (int i = 0; i < len; i++) {
@ -186,8 +168,6 @@ static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t chann
return;
}
hexDump("DOS->BBS", data, len);
int sent = 0;
while (sent < len) {
ssize_t n = write(bbsFd, data + sent, len - sent);
@ -487,8 +467,6 @@ int main(int argc, char *argv[]) {
int cleanLen = telnetFilter(bbsFd, raw, (int)n, clean);
if (cleanLen > 0) {
hexDump("BBS->DOS", clean, cleanLen);
// Retry with ACK processing if the send window is full
int rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false);
while (rc != SECLINK_SUCCESS && sRunning) {

View file

@ -11,8 +11,6 @@
#include "dvxWidget.h"
#include "secLink.h"
#include "security.h"
#include "../rs232/rs232.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -190,15 +188,6 @@ int main(int argc, char *argv[]) {
TermContextT tc;
memset(&tc, 0, sizeof(tc));
// Test rs232 directly first for diagnostics
printf("Testing rs232Open on COM%d...\n", comPort + 1);
int rsRc = rs232Open(comPort, baudRate, 8, 'N', 1, 0);
printf("rs232Open returned %d\n", rsRc);
if (rsRc == RS232_SUCCESS) {
printf("UART type: %d\n", rs232GetUartType(comPort));
rs232Close(comPort);
}
// Open secLink on the specified COM port
printf("Opening SecLink on COM%d...\n", comPort + 1);
tc.link = secLinkOpen(comPort, baudRate, 8, 'N', 1, 0, onRecv, &tc);
@ -282,15 +271,9 @@ int main(int argc, char *argv[]) {
ctx.idleCallback = idlePoll;
ctx.idleCtx = &tc;
// Main loop — poll serial, render immediately, composite
// Main loop — poll serial, dvxUpdate handles terminal widget automatically
while (ctx.running) {
secLinkPoll(tc.link);
wgtAnsiTermPoll(term);
if (wgtAnsiTermRepaint(term) > 0) {
win->contentDirty = true;
dvxInvalidateWindow(&ctx, win);
}
if (!dvxUpdate(&ctx)) {
break;