Compare commits

...

2 commits

19 changed files with 584 additions and 182 deletions

View file

@ -137,8 +137,6 @@ static void compositeAndFlush(AppContextT *ctx) {
RectT popIsect;
if (rectIntersect(dr, &popRect, &popIsect)) {
setClipRect(d, dr->x, dr->y, dr->w, dr->h);
// Find the window and menu
for (int32_t j = 0; j < ws->count; j++) {
if (ws->windows[j]->id == ctx->popup.windowId) {
@ -202,8 +200,6 @@ static void compositeAndFlush(AppContextT *ctx) {
RectT smIsect;
if (rectIntersect(dr, &smRect, &smIsect)) {
setClipRect(d, dr->x, dr->y, dr->w, dr->h);
BevelStyleT smBevel;
smBevel.highlight = ctx->colors.windowHighlight;
smBevel.shadow = ctx->colors.windowShadow;
@ -465,10 +461,10 @@ static void dispatchEvents(AppContextT *ctx) {
if (mx != ctx->prevMouseX || my != ctx->prevMouseY) {
dirtyCursorArea(ctx, ctx->prevMouseX, ctx->prevMouseY);
dirtyCursorArea(ctx, mx, my);
}
// Update cursor shape based on what the mouse is hovering over
updateCursorShape(ctx);
}
// Handle active drag
if (ctx->stack.dragWindow >= 0) {
@ -1380,9 +1376,16 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win)
static void pollKeyboard(AppContextT *ctx) {
__dpmi_regs r;
// Check if key is available (INT 16h, enhanced function 11h)
while (1) {
// Read shift state once per poll (INT 16h, AH=12h)
memset(&r, 0, sizeof(r));
r.x.ax = 0x1200;
__dpmi_int(0x16, &r);
int32_t shiftFlags = r.x.ax & 0xFF;
bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift
// Process buffered keys
while (1) {
// Check if key is available (INT 16h, enhanced function 11h)
r.x.ax = 0x1100;
__dpmi_int(0x16, &r);
@ -1392,7 +1395,6 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Read the key (INT 16h, enhanced function 10h)
memset(&r, 0, sizeof(r));
r.x.ax = 0x1000;
__dpmi_int(0x16, &r);
@ -1406,13 +1408,6 @@ static void pollKeyboard(AppContextT *ctx) {
ascii = 0;
}
// Read shift state (INT 16h, AH=12h — enhanced shift flags)
memset(&r, 0, sizeof(r));
r.x.ax = 0x1200;
__dpmi_int(0x16, &r);
int32_t shiftFlags = r.x.ax & 0xFF;
bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift
// Alt+Tab / Shift+Alt+Tab — cycle windows
// Alt+Tab: scancode=0xA5, ascii=0x00
if (ascii == 0 && scancode == 0xA5) {

View file

@ -77,12 +77,15 @@ void dirtyListMerge(DirtyListT *dl) {
return;
}
// Single-pass O(N²): 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 for that slot.
for (int32_t i = 0; i < dl->count; i++) {
bool merged = true;
while (merged) {
merged = false;
for (int32_t i = 0; i < dl->count; i++) {
for (int32_t j = i + 1; j < dl->count; j++) {
if (rectsOverlapOrAdjacent(&dl->rects[i], &dl->rects[j], DIRTY_MERGE_GAP)) {
rectUnion(&dl->rects[i], &dl->rects[j], &dl->rects[i]);
@ -104,15 +107,12 @@ void dirtyListMerge(DirtyListT *dl) {
void flushRect(DisplayT *d, const RectT *r) {
int32_t bpp = d->format.bytesPerPixel;
// Caller (compositeAndFlush) already clips to screen bounds
int32_t x = r->x;
int32_t y = r->y;
int32_t w = r->w;
int32_t h = r->h;
if (__builtin_expect(x < 0, 0)) { w += x; x = 0; }
if (__builtin_expect(y < 0, 0)) { h += y; y = 0; }
if (__builtin_expect(x + w > d->width, 0)) { w = d->width - x; }
if (__builtin_expect(y + h > d->height, 0)) { h = d->height - y; }
if (__builtin_expect(w <= 0 || h <= 0, 0)) { return; }
int32_t rowBytes = w * bpp;

View file

@ -266,6 +266,160 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
}
// ============================================================
// drawTextN
// ============================================================
//
// Renders exactly 'count' characters from a buffer in one pass.
// Same idea as drawTermRow but for uniform fg/bg text runs.
// Avoids per-character function call overhead, redundant clip
// calculation, and spanFill startup costs.
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) {
return;
}
int32_t cw = font->charWidth;
int32_t ch = font->charHeight;
int32_t bpp = ops->bytesPerPixel;
int32_t pitch = d->pitch;
// Row-level clip: reject if entirely outside vertically
int32_t clipX1 = d->clipX;
int32_t clipX2 = d->clipX + d->clipW;
int32_t clipY1 = d->clipY;
int32_t clipY2 = d->clipY + d->clipH;
if (y + ch <= clipY1 || y >= clipY2) {
return;
}
int32_t totalW = count * cw;
if (x + totalW <= clipX1 || x >= clipX2) {
return;
}
// Vertical clip for glyph scanlines
int32_t rowStart = 0;
int32_t rowEnd = ch;
if (y < clipY1) { rowStart = clipY1 - y; }
if (y + ch > clipY2) { rowEnd = clipY2 - y; }
// Horizontal clip: find first and last visible column (character index)
int32_t firstChar = 0;
int32_t lastChar = count;
if (x < clipX1) {
firstChar = (clipX1 - x) / cw;
}
if (x + totalW > clipX2) {
lastChar = (clipX2 - x + cw - 1) / cw;
if (lastChar > count) { lastChar = count; }
}
// Per-pixel clip for partially visible edge characters
int32_t edgeColStart = 0;
if (x + firstChar * cw < clipX1) {
edgeColStart = clipX1 - (x + firstChar * cw);
}
if (opaque) {
// Opaque: fill background for the entire visible span once per scanline,
// then overlay foreground glyph pixels
int32_t fillX1 = x + firstChar * cw;
int32_t fillX2 = x + lastChar * cw;
if (fillX1 < clipX1) { fillX1 = clipX1; }
if (fillX2 > clipX2) { fillX2 = clipX2; }
int32_t fillW = fillX2 - fillX1;
if (fillW > 0) {
for (int32_t row = rowStart; row < rowEnd; row++) {
uint8_t *dst = d->backBuf + (y + row) * pitch + fillX1 * bpp;
ops->spanFill(dst, bg, fillW);
}
}
}
// Render glyph foreground pixels
for (int32_t ci = firstChar; ci < lastChar; ci++) {
int32_t cx = x + ci * cw;
int32_t cStart = 0;
int32_t cEnd = cw;
if (ci == firstChar) {
cStart = edgeColStart;
}
if (cx + cw > clipX2) {
cEnd = clipX2 - cx;
}
int32_t idx = (uint8_t)text[ci] - font->firstChar;
const uint8_t *glyph = NULL;
if (idx >= 0 && idx < font->numChars) {
glyph = font->glyphData + idx * ch;
}
if (!glyph) {
continue;
}
if (bpp == 2) {
uint16_t fg16 = (uint16_t)fg;
for (int32_t row = rowStart; row < rowEnd; row++) {
uint8_t bits = glyph[row];
if (bits == 0) { continue; }
uint16_t *dst = (uint16_t *)(d->backBuf + (y + row) * pitch + cx * 2);
for (int32_t p = cStart; p < cEnd; p++) {
if (bits & sGlyphBit[p]) {
dst[p] = fg16;
}
}
}
} else if (bpp == 4) {
for (int32_t row = rowStart; row < rowEnd; row++) {
uint8_t bits = glyph[row];
if (bits == 0) { continue; }
uint32_t *dst = (uint32_t *)(d->backBuf + (y + row) * pitch + cx * 4);
for (int32_t p = cStart; p < cEnd; p++) {
if (bits & sGlyphBit[p]) {
dst[p] = fg;
}
}
}
} else {
uint8_t fg8 = (uint8_t)fg;
for (int32_t row = rowStart; row < rowEnd; row++) {
uint8_t bits = glyph[row];
if (bits == 0) { continue; }
uint8_t *dst = d->backBuf + (y + row) * pitch + cx;
for (int32_t p = cStart; p < cEnd; p++) {
if (bits & sGlyphBit[p]) {
dst[p] = fg8;
}
}
}
}
}
}
// ============================================================
// drawFocusRect
// ============================================================

View file

@ -22,6 +22,11 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
// Draw a null-terminated string
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).
// Much faster than calling drawChar per character: computes clip once,
// fills background in bulk, then overlays glyph foreground pixels.
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
int32_t textWidth(const BitmapFontT *font, const char *text);

View file

@ -576,6 +576,9 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH);
// Mark a widget (and ancestors) for relayout and repaint
void wgtInvalidate(WidgetT *w);
// Repaint only — skip measure/layout (use for visual-only changes)
void wgtInvalidatePaint(WidgetT *w);
// Set/get widget text (label, button, textInput, etc.)
void wgtSetText(WidgetT *w, const char *text);
const char *wgtGetText(const WidgetT *w);

View file

@ -1553,7 +1553,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (ansiTermHasSelection(w)) {
ansiTermCopySelection(w);
ansiTermClearSelection(w);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1563,7 +1563,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
// Ctrl+V: paste from clipboard to terminal
if (key == 0x16 && (mod & KEY_MOD_CTRL)) {
ansiTermPasteToComm(w);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1645,7 +1645,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len);
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
@ -1822,8 +1822,18 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
// Determine if viewing live terminal or scrollback
bool viewingLive = (w->as.ansiTerm.scrollPos == sbCount);
// Render character cells row by row using bulk renderer
// Render character cells row by row using bulk renderer.
// Only repaint rows marked dirty; 0xFFFFFFFF means all rows.
uint32_t dirty = w->as.ansiTerm.dirtyRows;
if (dirty == 0) {
dirty = 0xFFFFFFFF;
}
for (int32_t row = 0; row < rows; row++) {
if (row < 32 && !(dirty & (1U << row))) {
continue;
}
int32_t lineIndex = w->as.ansiTerm.scrollPos + row;
const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
@ -1838,6 +1848,8 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY);
}
w->as.ansiTerm.dirtyRows = 0;
// Draw scrollbar
int32_t sbX = baseX + cols * cellW;
int32_t sbY = baseY;

View file

@ -59,7 +59,7 @@ void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == ' ' || key == 0x0D) {
w->as.button.pressed = true;
sKeyPressedBtn = w;
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
}

View file

@ -709,7 +709,7 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->as.canvas.lastX = cx;
hit->as.canvas.lastY = cy;
wgtInvalidate(hit);
wgtInvalidatePaint(hit);
}

View file

@ -64,7 +64,7 @@ void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->onChange(w);
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
}

View file

@ -154,7 +154,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -167,7 +167,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -194,7 +194,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->onChange(w);
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
}
@ -213,7 +213,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -340,18 +340,25 @@ 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;
}
for (int32_t i = 0; i < len; i++) {
int32_t charIdx = off + i;
uint32_t cfgc = fg;
uint32_t cbgc = bg;
// Draw up to 3 runs: before selection, selection, after selection
int32_t visSelLo = selLo - off;
int32_t visSelHi = selHi - off;
if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) {
cfgc = colors->menuHighlightFg;
cbgc = colors->menuHighlightBg;
if (visSelLo < 0) { visSelLo = 0; }
if (visSelHi > len) { visSelHi = len; }
if (selLo >= 0 && visSelLo < visSelHi) {
if (visSelLo > 0) {
drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, visSelLo, fg, bg, true);
}
drawChar(d, ops, font, textX + i * font->charWidth, textY,
w->as.comboBox.buf[charIdx], cfgc, cbgc, true);
drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, w->as.comboBox.buf + off + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
if (visSelHi < len) {
drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, w->as.comboBox.buf + off + visSelHi, len - visSelHi, fg, bg, true);
}
} else {
drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, len, fg, bg, true);
}
// Draw cursor
@ -426,7 +433,7 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx);
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
}
}

View file

@ -166,7 +166,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
@ -280,6 +280,6 @@ void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx);
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
}
}

View file

@ -192,21 +192,21 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
sDrawingCanvas->as.canvas.lastX = -1;
sDrawingCanvas->as.canvas.lastY = -1;
sDrawingCanvas = NULL;
wgtInvalidate(root);
wgtInvalidatePaint(root);
return;
}
// Handle canvas drawing (mouse move while pressed)
if (sDrawingCanvas && (buttons & 1)) {
widgetCanvasOnMouse(sDrawingCanvas, root, x, y);
wgtInvalidate(root);
wgtInvalidatePaint(root);
return;
}
// Handle slider drag release
if (sDragSlider && !(buttons & 1)) {
sDragSlider = NULL;
wgtInvalidate(root);
wgtInvalidatePaint(root);
return;
}
@ -242,7 +242,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
sDragSlider->onChange(sDragSlider);
}
wgtInvalidate(root);
wgtInvalidatePaint(root);
}
}
@ -272,7 +272,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
}
}
wgtInvalidate(sPressedButton);
wgtInvalidatePaint(sPressedButton);
sPressedButton = NULL;
return;
}
@ -302,7 +302,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
sPressedButton->as.button.pressed = over;
}
wgtInvalidate(sPressedButton);
wgtInvalidatePaint(sPressedButton);
}
return;
@ -376,7 +376,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
}
sOpenPopup = NULL;
wgtInvalidate(root);
wgtInvalidatePaint(root);
// Fall through to normal click handling
}

View file

@ -141,7 +141,7 @@ void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vx;
(void)vy;
w->as.image.pressed = true;
wgtInvalidate(w);
wgtInvalidatePaint(w);
if (w->onClick) {
w->onClick(w);

View file

@ -77,7 +77,7 @@ void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == ' ' || key == 0x0D) {
w->as.imageButton.pressed = true;
sKeyPressedBtn = w;
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
}

View file

@ -156,7 +156,7 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->onChange(w);
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
@ -292,7 +292,7 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
rectFill(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, ibg);
}
drawText(d, ops, font, innerX, iy, w->as.listBox.items[idx], ifg, ibg, idx == w->as.listBox.selectedIdx);
drawText(d, ops, font, innerX, iy, w->as.listBox.items[idx], ifg, ibg, false);
}
// Draw scrollbar

View file

@ -250,6 +250,40 @@ void wgtInvalidate(WidgetT *w) {
}
// ============================================================
// wgtInvalidatePaint
// ============================================================
//
// Lightweight repaint — skips measure/layout/scrollbar management.
// Use when only visual state changed (slider value, cursor blink,
// selection highlight, checkbox toggle) but widget sizes are stable.
void wgtInvalidatePaint(WidgetT *w) {
if (!w || !w->window) {
return;
}
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
// Repaint without measure/layout
RectT fullRect = {0, 0, w->window->contentW, w->window->contentH};
widgetOnPaint(w->window, &fullRect);
w->window->contentDirty = true;
dvxInvalidateWindow(ctx, w->window);
}
// ============================================================
// wgtPaint
// ============================================================

View file

@ -92,7 +92,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
} else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) {
// Down or Right — next radio in group
if (w->parent && w->parent->type == WidgetRadioGroupE) {
@ -115,7 +115,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
next->parent->onChange(next->parent);
}
wgtInvalidate(next);
wgtInvalidatePaint(next);
}
}
} else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) {
@ -139,7 +139,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
prev->parent->onChange(prev->parent);
}
wgtInvalidate(prev);
wgtInvalidatePaint(prev);
}
}
}

View file

@ -119,7 +119,7 @@ void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->onChange(w);
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}

View file

@ -35,6 +35,8 @@ static int32_t textAreaMaxLineLen(const char *buf, int32_t len);
static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col);
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
static int32_t wordBoundaryLeft(const char *buf, int32_t pos);
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
// ============================================================
// Shared clipboard
@ -122,37 +124,35 @@ int32_t wordEnd(const char *buf, int32_t len, int32_t pos) {
// Clear selection on all text widgets except 'except'
// ============================================================
static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) {
if (!root) {
return false;
}
// 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.
static WidgetT *sLastSelectedWidget = NULL;
bool cleared = false;
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
while (top > 0) {
WidgetT *w = stack[--top];
if (w != except) {
static bool clearSelectionOnWidget(WidgetT *w) {
if (w->type == WidgetTextInputE) {
if (w->as.textInput.selStart != w->as.textInput.selEnd) {
cleared = true;
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
return true;
}
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
} else if (w->type == WidgetTextAreaE) {
if (w->as.textArea.selAnchor != w->as.textArea.selCursor) {
cleared = true;
w->as.textArea.selAnchor = -1;
w->as.textArea.selCursor = -1;
return true;
}
w->as.textArea.selAnchor = -1;
w->as.textArea.selCursor = -1;
} else if (w->type == WidgetComboBoxE) {
if (w->as.comboBox.selStart != w->as.comboBox.selEnd) {
cleared = true;
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
return true;
}
w->as.comboBox.selStart = -1;
@ -161,8 +161,13 @@ static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) {
if (w->as.ansiTerm.selStartLine >= 0 &&
(w->as.ansiTerm.selStartLine != w->as.ansiTerm.selEndLine ||
w->as.ansiTerm.selStartCol != w->as.ansiTerm.selEndCol)) {
cleared = true;
w->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
w->as.ansiTerm.selStartLine = -1;
w->as.ansiTerm.selStartCol = -1;
w->as.ansiTerm.selEndLine = -1;
w->as.ansiTerm.selEndCol = -1;
w->as.ansiTerm.selecting = false;
return true;
}
w->as.ansiTerm.selStartLine = -1;
@ -171,16 +176,8 @@ static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) {
w->as.ansiTerm.selEndCol = -1;
w->as.ansiTerm.selecting = false;
}
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (top < 64) {
stack[top++] = c;
}
}
}
return cleared;
return false;
}
@ -189,23 +186,44 @@ void clearOtherSelections(WidgetT *except) {
return;
}
WidgetT *prev = sLastSelectedWidget;
sLastSelectedWidget = except;
if (!prev || prev == except) {
return;
}
// Verify the widget is still alive (its window still in the stack)
WindowT *prevWin = prev->window;
if (!prevWin) {
return;
}
AppContextT *ctx = (AppContextT *)except->window->widgetRoot->userData;
if (!ctx) {
return;
}
for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i];
bool found = false;
if (win && win->widgetRoot) {
if (clearSelectionsInTree(win->widgetRoot, except) && win != except->window) {
RectT fullRect = {0, 0, win->contentW, win->contentH};
widgetOnPaint(win, &fullRect);
win->contentDirty = true;
dvxInvalidateWindow(ctx, win);
for (int32_t i = 0; i < ctx->stack.count; i++) {
if (ctx->stack.windows[i] == prevWin) {
found = true;
break;
}
}
if (!found) {
return;
}
if (clearSelectionOnWidget(prev) && prevWin != except->window) {
RectT fullRect = {0, 0, prevWin->contentW, prevWin->contentH};
widgetOnPaint(prevWin, &fullRect);
prevWin->contentDirty = true;
dvxInvalidateWindow(ctx, prevWin);
}
}
@ -257,6 +275,58 @@ static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor,
}
// ============================================================
// wordBoundaryLeft
// ============================================================
//
// From position pos, skip non-word chars left, then skip word chars left.
// Returns the position at the start of the word (or 0).
static int32_t wordBoundaryLeft(const char *buf, int32_t pos) {
if (pos <= 0) {
return 0;
}
// Skip non-word characters
while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') {
pos--;
}
// Skip word characters
while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) {
pos--;
}
return pos;
}
// ============================================================
// wordBoundaryRight
// ============================================================
//
// From position pos, skip word chars right, then skip non-word chars right.
// Returns the position at the end of the word (or len).
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
if (pos >= len) {
return len;
}
// Skip word characters
while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) {
pos++;
}
// Skip non-word characters
while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') {
pos++;
}
return pos;
}
// ============================================================
// wgtTextArea
// ============================================================
@ -697,7 +767,7 @@ done:
}
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}
@ -948,7 +1018,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
textAreaOffToRowCol(buf, *pLen, pRow, pCol);
w->as.textArea.desiredCol = *pCol;
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -996,7 +1066,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1023,7 +1093,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1067,7 +1137,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1105,7 +1175,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1144,7 +1214,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1181,7 +1251,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1197,7 +1267,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = *pCol;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1213,7 +1283,33 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = *pCol;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
// Ctrl+Left — word left
if (key == (0x73 | 0x100)) {
SEL_BEGIN();
int32_t off = CUR_OFF();
int32_t newOff = wordBoundaryLeft(buf, off);
textAreaOffToRowCol(buf, newOff, pRow, pCol);
w->as.textArea.desiredCol = *pCol;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidatePaint(w);
return;
}
// Ctrl+Right — word right
if (key == (0x74 | 0x100)) {
SEL_BEGIN();
int32_t off = CUR_OFF();
int32_t newOff = wordBoundaryRight(buf, *pLen, off);
textAreaOffToRowCol(buf, newOff, pRow, pCol);
w->as.textArea.desiredCol = *pCol;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidatePaint(w);
return;
}
@ -1229,7 +1325,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1245,7 +1341,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1256,7 +1352,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = 0;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1267,7 +1363,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = *pCol;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1284,7 +1380,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1301,7 +1397,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1313,7 +1409,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = 0;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1324,7 +1420,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = *pCol;
SEL_END();
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1361,7 +1457,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidate(w);
wgtInvalidatePaint(w);
return;
}
@ -1791,35 +1887,70 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
}
int32_t drawY = textY + i * font->charHeight;
for (int32_t j = 0; j < visCols; j++) {
int32_t col = w->as.textArea.scrollCol + j;
int32_t charOff = lineOff + col;
int32_t drawX = textX + j * font->charWidth;
// Visible range within line
int32_t scrollCol = w->as.textArea.scrollCol;
int32_t visStart = scrollCol;
int32_t visEnd = scrollCol + visCols;
int32_t textEnd = lineL; // chars in this line
uint32_t cfgc = fg;
uint32_t cbgc = bg;
// Clamp visible range to actual line content for text drawing
int32_t drawStart = visStart < textEnd ? visStart : textEnd;
int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd;
// Determine selection intersection with this line
int32_t lineSelLo = -1;
int32_t lineSelHi = -1;
// Check selection — past end of line, test the newline byte
// instead of lineOff+col (which aliases into subsequent lines)
bool inSel = false;
if (selLo >= 0) {
if (col < lineL) {
inSel = (charOff >= selLo && charOff < selHi);
} else {
// Selection range in column-space for this line
if (selLo < lineOff + lineL + 1 && selHi > lineOff) {
lineSelLo = selLo - lineOff;
lineSelHi = selHi - lineOff;
if (lineSelLo < 0) { lineSelLo = 0; }
// selHi can extend past line (newline selected)
}
}
if (lineSelLo >= 0 && lineSelLo < lineSelHi) {
// Clamp selection to visible columns for text runs
int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo;
int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd;
if (vSelLo > vSelHi) { vSelLo = vSelHi; }
// Before selection
if (drawStart < vSelLo) {
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, vSelLo - drawStart, fg, bg, true);
}
// Selection (text portion)
if (vSelLo < vSelHi) {
drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
}
// After selection
if (vSelHi < drawEnd) {
drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelHi, drawEnd - vSelHi, fg, bg, true);
}
// Past end of text: fill selected area with highlight bg
int32_t nlOff = lineOff + lineL;
inSel = (nlOff >= selLo && nlOff < selHi);
}
}
bool pastEolSelected = (nlOff >= selLo && nlOff < selHi);
if (inSel) {
cfgc = colors->menuHighlightFg;
cbgc = colors->menuHighlightBg;
}
if (pastEolSelected && drawEnd < visEnd) {
int32_t selPastStart = drawEnd < lineSelLo ? lineSelLo : drawEnd;
int32_t selPastEnd = visEnd;
if (col < lineL) {
drawChar(d, ops, font, drawX, drawY, buf[charOff], cfgc, cbgc, true);
} else if (inSel) {
rectFill(d, ops, drawX, drawY, font->charWidth, font->charHeight, cbgc);
if (selPastStart < visStart) { selPastStart = visStart; }
if (selPastStart < selPastEnd) {
rectFill(d, ops, textX + (selPastStart - scrollCol) * font->charWidth, drawY, (selPastEnd - selPastStart) * font->charWidth, font->charHeight, colors->menuHighlightBg);
}
}
} else {
// No selection on this line — single run
if (drawStart < drawEnd) {
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true);
}
}
@ -2127,24 +2258,39 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
bool isPassword = (w->as.textInput.inputMode == InputPasswordE);
for (int32_t i = 0; i < len; i++) {
int32_t charIdx = off + i;
uint32_t cfgc = fg;
uint32_t cbgc = bg;
if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) {
cfgc = colors->menuHighlightFg;
cbgc = colors->menuHighlightBg;
}
char displayCh = w->as.textInput.buf[charIdx];
// Build display buffer (password masking)
char dispBuf[256];
int32_t dispLen = len > 255 ? 255 : len;
if (isPassword) {
displayCh = '\xF9'; // CP437 bullet
memset(dispBuf, '\xF9', dispLen); // CP437 bullet
} else {
memcpy(dispBuf, w->as.textInput.buf + off, dispLen);
}
drawChar(d, ops, font, textX + i * font->charWidth, textY,
displayCh, cfgc, cbgc, true);
// Draw up to 3 runs: before selection, selection, after selection
int32_t visSelLo = selLo - off;
int32_t visSelHi = selHi - off;
if (visSelLo < 0) { visSelLo = 0; }
if (visSelHi > dispLen) { visSelHi = dispLen; }
if (selLo >= 0 && visSelLo < visSelHi) {
// Before selection
if (visSelLo > 0) {
drawTextN(d, ops, font, textX, textY, dispBuf, visSelLo, fg, bg, true);
}
// Selection
drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, dispBuf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
// After selection
if (visSelHi < dispLen) {
drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, dispBuf + visSelHi, dispLen - visSelHi, fg, bg, true);
}
} else {
// No selection — single run
drawTextN(d, ops, font, textX, textY, dispBuf, dispLen, fg, bg, true);
}
// Draw cursor
@ -2427,6 +2573,52 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
(*pCursor)++;
}
}
} else if (key == (0x73 | 0x100)) {
// Ctrl+Left — word left
int32_t newPos = wordBoundaryLeft(buf, *pCursor);
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
*pCursor = newPos;
*pSelEnd = newPos;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
*pCursor = newPos;
}
} else if (key == (0x74 | 0x100)) {
// Ctrl+Right — word right
int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor);
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
*pCursor = newPos;
*pSelEnd = newPos;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
*pCursor = newPos;
}
} else if (key == (0x47 | 0x100)) {
// Home
if (shift && pSelStart && pSelEnd) {
@ -2520,5 +2712,5 @@ adjustScroll:
}
}
wgtInvalidate(w);
wgtInvalidatePaint(w);
}