More optimizations. Word-at-a-time keyboard selection added.
This commit is contained in:
parent
e489f54ef0
commit
97530513b8
4 changed files with 466 additions and 108 deletions
154
dvx/dvxDraw.c
154
dvx/dvxDraw.c
|
|
@ -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
|
// drawFocusRect
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
|
||||||
// Draw a null-terminated string
|
// 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);
|
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
|
// Measure text width in pixels
|
||||||
int32_t textWidth(const BitmapFontT *font, const char *text);
|
int32_t textWidth(const BitmapFontT *font, const char *text);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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++) {
|
// Draw up to 3 runs: before selection, selection, after selection
|
||||||
int32_t charIdx = off + i;
|
int32_t visSelLo = selLo - off;
|
||||||
uint32_t cfgc = fg;
|
int32_t visSelHi = selHi - off;
|
||||||
uint32_t cbgc = bg;
|
|
||||||
|
|
||||||
if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) {
|
if (visSelLo < 0) { visSelLo = 0; }
|
||||||
cfgc = colors->menuHighlightFg;
|
if (visSelHi > len) { visSelHi = len; }
|
||||||
cbgc = colors->menuHighlightBg;
|
|
||||||
|
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,
|
drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, w->as.comboBox.buf + off + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
|
||||||
w->as.comboBox.buf[charIdx], cfgc, cbgc, 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
|
// Draw cursor
|
||||||
|
|
|
||||||
|
|
@ -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 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 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 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
|
// 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'
|
// Clear selection on all text widgets except 'except'
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) {
|
// Track the widget that last had an active selection so we can
|
||||||
if (!root) {
|
// clear it in O(1) instead of walking every widget in every window.
|
||||||
return false;
|
static WidgetT *sLastSelectedWidget = NULL;
|
||||||
}
|
|
||||||
|
|
||||||
bool cleared = false;
|
|
||||||
WidgetT *stack[64];
|
|
||||||
int32_t top = 0;
|
|
||||||
stack[top++] = root;
|
|
||||||
|
|
||||||
while (top > 0) {
|
static bool clearSelectionOnWidget(WidgetT *w) {
|
||||||
WidgetT *w = stack[--top];
|
|
||||||
|
|
||||||
if (w != except) {
|
|
||||||
if (w->type == WidgetTextInputE) {
|
if (w->type == WidgetTextInputE) {
|
||||||
if (w->as.textInput.selStart != w->as.textInput.selEnd) {
|
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.selStart = -1;
|
||||||
w->as.textInput.selEnd = -1;
|
w->as.textInput.selEnd = -1;
|
||||||
} else if (w->type == WidgetTextAreaE) {
|
} else if (w->type == WidgetTextAreaE) {
|
||||||
if (w->as.textArea.selAnchor != w->as.textArea.selCursor) {
|
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.selAnchor = -1;
|
||||||
w->as.textArea.selCursor = -1;
|
w->as.textArea.selCursor = -1;
|
||||||
} else if (w->type == WidgetComboBoxE) {
|
} else if (w->type == WidgetComboBoxE) {
|
||||||
if (w->as.comboBox.selStart != w->as.comboBox.selEnd) {
|
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;
|
w->as.comboBox.selStart = -1;
|
||||||
|
|
@ -161,8 +161,13 @@ static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) {
|
||||||
if (w->as.ansiTerm.selStartLine >= 0 &&
|
if (w->as.ansiTerm.selStartLine >= 0 &&
|
||||||
(w->as.ansiTerm.selStartLine != w->as.ansiTerm.selEndLine ||
|
(w->as.ansiTerm.selStartLine != w->as.ansiTerm.selEndLine ||
|
||||||
w->as.ansiTerm.selStartCol != w->as.ansiTerm.selEndCol)) {
|
w->as.ansiTerm.selStartCol != w->as.ansiTerm.selEndCol)) {
|
||||||
cleared = true;
|
|
||||||
w->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
|
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;
|
w->as.ansiTerm.selStartLine = -1;
|
||||||
|
|
@ -171,16 +176,8 @@ static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) {
|
||||||
w->as.ansiTerm.selEndCol = -1;
|
w->as.ansiTerm.selEndCol = -1;
|
||||||
w->as.ansiTerm.selecting = false;
|
w->as.ansiTerm.selecting = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
return false;
|
||||||
if (top < 64) {
|
|
||||||
stack[top++] = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleared;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -189,23 +186,44 @@ void clearOtherSelections(WidgetT *except) {
|
||||||
return;
|
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;
|
AppContextT *ctx = (AppContextT *)except->window->widgetRoot->userData;
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
bool found = false;
|
||||||
WindowT *win = ctx->stack.windows[i];
|
|
||||||
|
|
||||||
if (win && win->widgetRoot) {
|
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
||||||
if (clearSelectionsInTree(win->widgetRoot, except) && win != except->window) {
|
if (ctx->stack.windows[i] == prevWin) {
|
||||||
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
found = true;
|
||||||
widgetOnPaint(win, &fullRect);
|
break;
|
||||||
win->contentDirty = true;
|
|
||||||
dvxInvalidateWindow(ctx, win);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// wgtTextArea
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1217,6 +1287,32 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Up arrow
|
// Up arrow
|
||||||
if (key == (0x48 | 0x100)) {
|
if (key == (0x48 | 0x100)) {
|
||||||
SEL_BEGIN();
|
SEL_BEGIN();
|
||||||
|
|
@ -1791,35 +1887,70 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
}
|
}
|
||||||
int32_t drawY = textY + i * font->charHeight;
|
int32_t drawY = textY + i * font->charHeight;
|
||||||
|
|
||||||
for (int32_t j = 0; j < visCols; j++) {
|
// Visible range within line
|
||||||
int32_t col = w->as.textArea.scrollCol + j;
|
int32_t scrollCol = w->as.textArea.scrollCol;
|
||||||
int32_t charOff = lineOff + col;
|
int32_t visStart = scrollCol;
|
||||||
int32_t drawX = textX + j * font->charWidth;
|
int32_t visEnd = scrollCol + visCols;
|
||||||
|
int32_t textEnd = lineL; // chars in this line
|
||||||
|
|
||||||
uint32_t cfgc = fg;
|
// Clamp visible range to actual line content for text drawing
|
||||||
uint32_t cbgc = bg;
|
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 (selLo >= 0) {
|
||||||
if (col < lineL) {
|
// Selection range in column-space for this line
|
||||||
inSel = (charOff >= selLo && charOff < selHi);
|
if (selLo < lineOff + lineL + 1 && selHi > lineOff) {
|
||||||
} else {
|
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;
|
int32_t nlOff = lineOff + lineL;
|
||||||
inSel = (nlOff >= selLo && nlOff < selHi);
|
bool pastEolSelected = (nlOff >= selLo && nlOff < selHi);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inSel) {
|
if (pastEolSelected && drawEnd < visEnd) {
|
||||||
cfgc = colors->menuHighlightFg;
|
int32_t selPastStart = drawEnd < lineSelLo ? lineSelLo : drawEnd;
|
||||||
cbgc = colors->menuHighlightBg;
|
int32_t selPastEnd = visEnd;
|
||||||
}
|
|
||||||
|
|
||||||
if (col < lineL) {
|
if (selPastStart < visStart) { selPastStart = visStart; }
|
||||||
drawChar(d, ops, font, drawX, drawY, buf[charOff], cfgc, cbgc, true);
|
|
||||||
} else if (inSel) {
|
if (selPastStart < selPastEnd) {
|
||||||
rectFill(d, ops, drawX, drawY, font->charWidth, font->charHeight, cbgc);
|
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);
|
bool isPassword = (w->as.textInput.inputMode == InputPasswordE);
|
||||||
|
|
||||||
for (int32_t i = 0; i < len; i++) {
|
// Build display buffer (password masking)
|
||||||
int32_t charIdx = off + i;
|
char dispBuf[256];
|
||||||
uint32_t cfgc = fg;
|
int32_t dispLen = len > 255 ? 255 : len;
|
||||||
uint32_t cbgc = bg;
|
|
||||||
|
|
||||||
if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) {
|
|
||||||
cfgc = colors->menuHighlightFg;
|
|
||||||
cbgc = colors->menuHighlightBg;
|
|
||||||
}
|
|
||||||
|
|
||||||
char displayCh = w->as.textInput.buf[charIdx];
|
|
||||||
|
|
||||||
if (isPassword) {
|
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,
|
// Draw up to 3 runs: before selection, selection, after selection
|
||||||
displayCh, cfgc, cbgc, true);
|
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
|
// Draw cursor
|
||||||
|
|
@ -2427,6 +2573,52 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
|
||||||
(*pCursor)++;
|
(*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)) {
|
} else if (key == (0x47 | 0x100)) {
|
||||||
// Home
|
// Home
|
||||||
if (shift && pSelStart && pSelEnd) {
|
if (shift && pSelStart && pSelEnd) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue