From e4b44d08c116170e4b481ecb8c29e99b34156c61 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 17 Mar 2026 23:59:16 -0500 Subject: [PATCH] Text editing cursors now blink. --- dvx/dvxApp.c | 1 + dvx/dvxWidget.h | 5 +++++ dvx/widgets/widgetComboBox.c | 4 ++-- dvx/widgets/widgetInternal.h | 1 + dvx/widgets/widgetSpinner.c | 4 ++-- dvx/widgets/widgetTextInput.c | 39 +++++++++++++++++++++++++++++++---- 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 3e96822..f12e709 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1638,6 +1638,7 @@ bool dvxUpdate(AppContextT *ctx) { dispatchEvents(ctx); updateTooltip(ctx); pollAnsiTermWidgets(ctx); + wgtUpdateCursorBlink(); ctx->frameCount++; diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 0b78731..b2b0fee 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -876,6 +876,11 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH); // function call. struct AppContextT *wgtGetContext(const WidgetT *w); +// Update text cursor blink state. Call once per frame from dvxUpdate. +// Toggles the cursor visibility at 250ms intervals, matching the ANSI +// terminal cursor rate. +void wgtUpdateCursorBlink(void); + // 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/ diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 67778e4..ee7c935 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -404,8 +404,8 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, len, fg, bg, true); } - // Draw cursor - if (w->focused && w->enabled && !w->as.comboBox.open) { + // Draw cursor (blinks at same rate as terminal cursor) + if (w->focused && w->enabled && !w->as.comboBox.open && sCursorBlinkOn) { int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth; if (cursorX >= w->x + TEXT_INPUT_PAD && diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index f489ad6..687f0d7 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -173,6 +173,7 @@ static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const // is that these are truly global — but since the GUI is single-threaded // and only one mouse can exist, this is safe. +extern bool sCursorBlinkOn; // text cursor blink phase (toggled by wgtUpdateCursorBlink) extern bool sDebugLayout; extern WidgetT *sClosedPopup; // popup that was just closed (prevents immediate reopen) extern WidgetT *sFocusedWidget; // currently focused widget across all windows diff --git a/dvx/widgets/widgetSpinner.c b/dvx/widgets/widgetSpinner.c index 317cc85..e169b60 100644 --- a/dvx/widgets/widgetSpinner.c +++ b/dvx/widgets/widgetSpinner.c @@ -436,8 +436,8 @@ void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm drawTextN(d, ops, font, textX, textY, &w->as.spinner.buf[off], len, fg, bg, true); } - // Cursor - if (w->focused) { + // Cursor (blinks at same rate as terminal cursor) + if (w->focused && sCursorBlinkOn) { int32_t curX = textX + (w->as.spinner.cursorPos - off) * font->charWidth; if (curX >= w->x + SPINNER_BORDER && curX < btnX - SPINNER_PAD) { diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index 247dc4e..6765b61 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -71,6 +71,8 @@ #define TEXTAREA_MIN_COLS 20 #define CLIPBOARD_MAX 4096 #define DBLCLICK_TICKS (CLOCKS_PER_SEC / 2) +// Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c) +#define CURSOR_BLINK_MS 250 // ============================================================ // Prototypes @@ -104,6 +106,15 @@ static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); static char sClipboard[CLIPBOARD_MAX]; static int32_t sClipboardLen = 0; +// ============================================================ +// Cursor blink state +// ============================================================ +// Shared across all text-editing widgets (TextInput, TextArea, +// ComboBox, Spinner). Matches the ANSI terminal's 250ms rate. + +bool sCursorBlinkOn = true; +static clock_t sCursorBlinkTime = 0; + void clipboardCopy(const char *text, int32_t len) { if (!text || len <= 0) { @@ -129,6 +140,26 @@ const char *clipboardGet(int32_t *outLen) { } +// ============================================================ +// Cursor blink +// ============================================================ + +void wgtUpdateCursorBlink(void) { + clock_t now = clock(); + clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000; + + if ((now - sCursorBlinkTime) >= interval) { + sCursorBlinkTime = now; + sCursorBlinkOn = !sCursorBlinkOn; + + // Invalidate the focused widget so its cursor redraws + if (sFocusedWidget) { + wgtInvalidatePaint(sFocusedWidget); + } + } +} + + // ============================================================ // Multi-click tracking // ============================================================ @@ -2105,8 +2136,8 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } } - // Draw cursor - if (w->focused) { + // Draw cursor (blinks at same rate as terminal cursor) + if (w->focused && sCursorBlinkOn) { int32_t curDrawCol = w->as.textArea.cursorCol - w->as.textArea.scrollCol; int32_t curDrawRow = w->as.textArea.cursorRow - w->as.textArea.scrollRow; @@ -2457,8 +2488,8 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi drawTextN(d, ops, font, textX, textY, dispBuf, dispLen, fg, bg, true); } - // Draw cursor - if (w->focused && w->enabled) { + // Draw cursor (blinks at same rate as terminal cursor) + if (w->focused && w->enabled && sCursorBlinkOn) { int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth; if (cursorX >= w->x + TEXT_INPUT_PAD &&