Code cleanup, some optimizations. Cut/Copy/Paste seems to be working as expected.

This commit is contained in:
Scott Duensing 2026-03-14 20:14:55 -05:00
parent 05bcfb4a4c
commit 54d9180d3e
17 changed files with 438 additions and 180 deletions

View file

@ -321,7 +321,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
if (target) {
switch (target->type) {
case WidgetButtonE:
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = target;
target->focused = true;
target->as.button.pressed = true;
sKeyPressedBtn = target;
@ -329,19 +330,22 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
return true;
case WidgetCheckboxE:
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
widgetCheckboxOnMouse(target, win->widgetRoot, 0, 0);
sFocusedWidget = target;
wgtInvalidate(target);
return true;
case WidgetRadioE:
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
widgetRadioOnMouse(target, win->widgetRoot, 0, 0);
sFocusedWidget = target;
wgtInvalidate(target);
return true;
case WidgetImageButtonE:
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = target;
target->focused = true;
target->as.imageButton.pressed = true;
sKeyPressedBtn = target;
@ -385,7 +389,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
target->as.dropdown.open = true;
target->as.dropdown.hoverIdx = target->as.dropdown.selectedIdx;
sOpenPopup = target;
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = target;
target->focused = true;
wgtInvalidate(win->widgetRoot);
return true;
@ -394,7 +399,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
target->as.comboBox.open = true;
target->as.comboBox.hoverIdx = target->as.comboBox.selectedIdx;
sOpenPopup = target;
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = target;
target->focused = true;
wgtInvalidate(win->widgetRoot);
return true;
@ -406,7 +412,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target);
if (next) {
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = next;
next->focused = true;
// Open dropdown/combobox if that's the focused target
@ -429,7 +436,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
default:
// For focusable widgets, just focus them
if (widgetIsFocusable(target->type)) {
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = target;
target->focused = true;
wgtInvalidate(win->widgetRoot);
return true;
@ -1956,7 +1964,10 @@ static void pollKeyboard(AppContextT *ctx) {
if (next) {
sOpenPopup = NULL;
widgetClearFocus(win->widgetRoot);
if (sFocusedWidget) {
sFocusedWidget->focused = false;
}
sFocusedWidget = next;
next->focused = true;
// Scroll the widget into view if needed

View file

@ -138,20 +138,22 @@ void flushRect(DisplayT *d, const RectT *r) {
}
} else {
// Partial scanlines — copy row by row with rep movsd
for (int32_t i = 0; i < h; i++) {
int32_t dwords = rowBytes >> 2;
int32_t remainder = rowBytes & 3;
for (int32_t i = 0; i < h; i++) {
int32_t dc = dwords;
uint8_t *s = src;
uint8_t *dd = dst;
__asm__ __volatile__ (
"rep movsl"
: "+D"(dd), "+S"(s), "+c"(dwords)
: "+D"(dd), "+S"(s), "+c"(dc)
:
: "memory"
);
// Trailing bytes (dd and s already advanced by rep movsl)
if (__builtin_expect(remainder > 0, 0)) {
while (remainder-- > 0) {
int32_t rem = remainder;
while (rem-- > 0) {
*dd++ = *s++;
}
}

View file

@ -18,6 +18,10 @@ static void spanFill8(uint8_t *dst, uint32_t color, int32_t count);
static void spanFill16(uint8_t *dst, uint32_t color, int32_t count);
static void spanFill32(uint8_t *dst, uint32_t color, int32_t count);
// Bit lookup tables — avoids per-pixel shift on 486 (40+ cycle savings per shift)
static const uint8_t sGlyphBit[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
static const uint16_t sMaskBit[16] = {0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, 0x0004, 0x0002, 0x0001};
// ============================================================
// accelParse
@ -204,20 +208,20 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
if (bpp == 2) {
uint16_t fg16 = (uint16_t)fg;
for (int32_t col = colStart; col < colEnd; col++) {
if (bits & (0x80 >> col)) {
if (bits & sGlyphBit[col]) {
*(uint16_t *)(dst + col * 2) = fg16;
}
}
} else if (bpp == 4) {
for (int32_t col = colStart; col < colEnd; col++) {
if (bits & (0x80 >> col)) {
if (bits & sGlyphBit[col]) {
*(uint32_t *)(dst + col * 4) = fg;
}
}
} else {
uint8_t fg8 = (uint8_t)fg;
for (int32_t col = colStart; col < colEnd; col++) {
if (bits & (0x80 >> col)) {
if (bits & sGlyphBit[col]) {
dst[col] = fg8;
}
}
@ -237,20 +241,20 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
if (bpp == 2) {
uint16_t fg16 = (uint16_t)fg;
for (int32_t col = colStart; col < colEnd; col++) {
if (bits & (0x80 >> col)) {
if (bits & sGlyphBit[col]) {
*(uint16_t *)(dst + col * 2) = fg16;
}
}
} else if (bpp == 4) {
for (int32_t col = colStart; col < colEnd; col++) {
if (bits & (0x80 >> col)) {
if (bits & sGlyphBit[col]) {
*(uint32_t *)(dst + col * 4) = fg;
}
}
} else {
uint8_t fg8 = (uint8_t)fg;
for (int32_t col = colStart; col < colEnd; col++) {
if (bits & (0x80 >> col)) {
if (bits & sGlyphBit[col]) {
dst[col] = fg8;
}
}
@ -386,17 +390,19 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
return;
}
// Pre-compute column mask once (loop-invariant)
uint16_t colMask = 0;
for (int32_t col = colStart; col < colEnd; col++) {
colMask |= sMaskBit[col];
}
for (int32_t row = rowStart; row < rowEnd; row++) {
uint16_t mask = andMask[row];
uint16_t data = xorData[row];
// Skip fully transparent rows
uint16_t colMask = 0;
for (int32_t col = colStart; col < colEnd; col++) {
colMask |= (0x8000 >> col);
}
if ((mask & colMask) == colMask) {
continue; // all visible columns are transparent
continue;
}
int32_t py = y + row;
@ -406,14 +412,14 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
uint16_t fg16 = (uint16_t)fgColor;
uint16_t bg16 = (uint16_t)bgColor;
for (int32_t col = colStart; col < colEnd; col++) {
uint16_t bit = 0x8000 >> col;
uint16_t bit = sMaskBit[col];
if (!(mask & bit)) {
*(uint16_t *)(dst + col * 2) = (data & bit) ? fg16 : bg16;
}
}
} else if (bpp == 4) {
for (int32_t col = colStart; col < colEnd; col++) {
uint16_t bit = 0x8000 >> col;
uint16_t bit = sMaskBit[col];
if (!(mask & bit)) {
*(uint32_t *)(dst + col * 4) = (data & bit) ? fgColor : bgColor;
}
@ -422,7 +428,7 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
uint8_t fg8 = (uint8_t)fgColor;
uint8_t bg8 = (uint8_t)bgColor;
for (int32_t col = colStart; col < colEnd; col++) {
uint16_t bit = 0x8000 >> col;
uint16_t bit = sMaskBit[col];
if (!(mask & bit)) {
dst[col] = (data & bit) ? fg8 : bg8;
}
@ -540,7 +546,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
uint8_t bits = glyph ? glyph[row] : 0;
for (int32_t p = cStart; p < cEnd; p++) {
dst[p] = (bits & (0x80 >> p)) ? fg16 : bg16;
dst[p] = (bits & sGlyphBit[p]) ? fg16 : bg16;
}
}
} else if (bpp == 4) {
@ -549,7 +555,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
uint8_t bits = glyph ? glyph[row] : 0;
for (int32_t p = cStart; p < cEnd; p++) {
dst[p] = (bits & (0x80 >> p)) ? fg : bg;
dst[p] = (bits & sGlyphBit[p]) ? fg : bg;
}
}
} else {
@ -561,7 +567,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
uint8_t bits = glyph ? glyph[row] : 0;
for (int32_t p = cStart; p < cEnd; p++) {
dst[p] = (bits & (0x80 >> p)) ? fg8 : bg8;
dst[p] = (bits & sGlyphBit[p]) ? fg8 : bg8;
}
}
}

View file

@ -215,6 +215,8 @@ typedef struct WidgetT {
char *undoBuf;
int32_t undoLen;
int32_t undoCursor; // byte offset at time of snapshot
int32_t cachedLines; // cached line count (-1 = dirty)
int32_t cachedMaxLL; // cached max line length (-1 = dirty)
} textArea;
struct {
@ -222,6 +224,7 @@ typedef struct WidgetT {
int32_t itemCount;
int32_t selectedIdx;
int32_t scrollPos;
int32_t maxItemLen; // cached max strlen of items
} listBox;
struct {
@ -241,6 +244,7 @@ typedef struct WidgetT {
bool open;
int32_t hoverIdx;
int32_t scrollPos;
int32_t maxItemLen; // cached max strlen of items
} dropdown;
struct {
@ -260,6 +264,7 @@ typedef struct WidgetT {
bool open;
int32_t hoverIdx;
int32_t listScrollPos;
int32_t maxItemLen; // cached max strlen of items
} comboBox;
struct {
@ -314,6 +319,7 @@ typedef struct WidgetT {
int32_t canvasW;
int32_t canvasH;
int32_t canvasPitch;
int32_t canvasBpp; // cached bytes per pixel (avoids pitch/w division)
uint32_t penColor;
int32_t penSize;
int32_t lastX;

View file

@ -286,9 +286,17 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW
srcXTab[dx] = ((colStart + dx) * srcW) / dstW * bpp;
}
// Pre-compute source Y lookup table (one division per row instead of per row)
int32_t srcYTab[ICON_SIZE];
int32_t visibleRows = rowEnd - rowStart;
for (int32_t dy = 0; dy < visibleRows; dy++) {
srcYTab[dy] = ((rowStart + dy) * srcH) / dstH;
}
// Blit with pre-computed lookups — no per-pixel divisions or clip checks
for (int32_t dy = rowStart; dy < rowEnd; dy++) {
int32_t sy = (dy * srcH) / dstH;
int32_t sy = srcYTab[dy - rowStart];
uint8_t *dstRow = d->backBuf + (dstY + dy) * d->pitch + (dstX + colStart) * bpp;
const uint8_t *srcRow = src + sy * srcPitch;

View file

@ -52,7 +52,7 @@ static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
// Draw a filled circle of diameter penSize at (cx, cy) in canvas coords.
static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t bpp = w->as.canvas.canvasBpp;
int32_t pitch = w->as.canvas.canvasPitch;
uint8_t *data = w->as.canvas.data;
int32_t cw = w->as.canvas.canvasW;
@ -71,7 +71,7 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
return;
}
// Filled circle via bounding box + radius check
// Filled circle via per-row horizontal span
int32_t r2 = rad * rad;
for (int32_t dy = -rad; dy <= rad; dy++) {
@ -81,17 +81,41 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) {
continue;
}
for (int32_t dx = -rad; dx <= rad; dx++) {
int32_t px = cx + dx;
// Compute horizontal half-span: dx² <= r² - dy²
int32_t dy2 = dy * dy;
int32_t rem = r2 - dy2;
int32_t hspan = 0;
if (px < 0 || px >= cw) {
continue;
// Integer sqrt via Newton's method
if (rem > 0) {
hspan = rad;
for (int32_t i = 0; i < 8; i++) {
hspan = (hspan + rem / hspan) / 2;
}
if (dx * dx + dy * dy <= r2) {
uint8_t *dst = data + py * pitch + px * bpp;
if (hspan * hspan > rem) {
hspan--;
}
}
int32_t x0 = cx - hspan;
int32_t x1 = cx + hspan;
if (x0 < 0) {
x0 = 0;
}
if (x1 >= cw) {
x1 = cw - 1;
}
if (x0 <= x1) {
uint8_t *dst = data + py * pitch + x0 * bpp;
for (int32_t px = x0; px <= x1; px++) {
canvasPutPixel(dst, color, bpp);
dst += bpp;
}
}
}
@ -196,15 +220,13 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
return NULL;
}
// Fill with white
// Fill with white using span fill for performance
uint32_t white = packColor(d, 255, 255, 255);
BlitOpsT canvasOps;
drawInit(&canvasOps, d);
for (int32_t y = 0; y < h; y++) {
for (int32_t x = 0; x < w; x++) {
uint8_t *dst = data + y * pitch + x * bpp;
canvasPutPixel(dst, white, bpp);
}
canvasOps.spanFill(data + y * pitch, white, w);
}
WidgetT *wgt = widgetAlloc(parent, WidgetCanvasE);
@ -214,6 +236,7 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
wgt->as.canvas.canvasW = w;
wgt->as.canvas.canvasH = h;
wgt->as.canvas.canvasPitch = pitch;
wgt->as.canvas.canvasBpp = bpp;
wgt->as.canvas.penColor = packColor(d, 0, 0, 0);
wgt->as.canvas.penSize = 2;
wgt->as.canvas.lastX = -1;
@ -235,17 +258,22 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) {
return;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
// Find BlitOps for span fill
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
int32_t pitch = w->as.canvas.canvasPitch;
int32_t cw = w->as.canvas.canvasW;
int32_t ch = w->as.canvas.canvasH;
for (int32_t y = 0; y < ch; y++) {
for (int32_t x = 0; x < cw; x++) {
uint8_t *dst = w->as.canvas.data + y * pitch + x * bpp;
canvasPutPixel(dst, color, bpp);
}
ctx->blitOps.spanFill(w->as.canvas.data + y * pitch, color, cw);
}
}
@ -280,7 +308,7 @@ void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t
return;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t bpp = w->as.canvas.canvasBpp;
int32_t pitch = w->as.canvas.canvasPitch;
uint8_t *data = w->as.canvas.data;
int32_t cw = w->as.canvas.canvasW;
@ -346,7 +374,7 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) {
return;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t bpp = w->as.canvas.canvasBpp;
int32_t pitch = w->as.canvas.canvasPitch;
uint8_t *data = w->as.canvas.data;
int32_t cw = w->as.canvas.canvasW;
@ -361,17 +389,41 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) {
continue;
}
for (int32_t dx = -radius; dx <= radius; dx++) {
int32_t px = cx + dx;
// Compute horizontal half-span: dx² <= r² - dy²
int32_t dy2 = dy * dy;
int32_t rem = r2 - dy2;
int32_t hspan = 0;
if (px < 0 || px >= cw) {
continue;
// Integer sqrt via Newton's method
if (rem > 0) {
hspan = radius;
for (int32_t i = 0; i < 8; i++) {
hspan = (hspan + rem / hspan) / 2;
}
if (dx * dx + dy * dy <= r2) {
uint8_t *dst = data + py * pitch + px * bpp;
if (hspan * hspan > rem) {
hspan--;
}
}
int32_t x0 = cx - hspan;
int32_t x1 = cx + hspan;
if (x0 < 0) {
x0 = 0;
}
if (x1 >= cw) {
x1 = cw - 1;
}
if (x0 <= x1) {
uint8_t *dst = data + py * pitch + x0 * bpp;
for (int32_t px = x0; px <= x1; px++) {
canvasPutPixel(dst, color, bpp);
dst += bpp;
}
}
}
@ -393,7 +445,7 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t
return;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t bpp = w->as.canvas.canvasBpp;
int32_t pitch = w->as.canvas.canvasPitch;
uint8_t *data = w->as.canvas.data;
int32_t cw = w->as.canvas.canvasW;
@ -405,12 +457,28 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t
int32_t y0 = y < 0 ? 0 : y;
int32_t x1 = x + width > cw ? cw : x + width;
int32_t y1 = y + height > ch ? ch : y + height;
int32_t fillW = x1 - x0;
if (fillW <= 0) {
return;
}
// Find BlitOps for span fill
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (ctx) {
for (int32_t py = y0; py < y1; py++) {
ctx->blitOps.spanFill(data + py * pitch + x0 * bpp, color, fillW);
}
} else {
for (int32_t py = y0; py < y1; py++) {
for (int32_t px = x0; px < x1; px++) {
uint8_t *dst = data + py * pitch + px * bpp;
canvasPutPixel(dst, color, bpp);
canvasPutPixel(data + py * pitch + px * bpp, color, bpp);
}
}
}
}
@ -429,7 +497,7 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) {
return 0;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t bpp = w->as.canvas.canvasBpp;
const uint8_t *src = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp;
return canvasGetPixel(src, bpp);
@ -495,6 +563,7 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
w->as.canvas.canvasW = imgW;
w->as.canvas.canvasH = imgH;
w->as.canvas.canvasPitch = pitch;
w->as.canvas.canvasBpp = bpp;
return 0;
}
@ -587,7 +656,7 @@ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) {
return;
}
int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW;
int32_t bpp = w->as.canvas.canvasBpp;
uint8_t *dst = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp;
canvasPutPixel(dst, color, bpp);

View file

@ -118,10 +118,10 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
// Draw label
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, w->as.checkbox.text);
drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false);
if (w->focused) {
int32_t labelW = textWidthAccel(font, w->as.checkbox.text);
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
}
}

View file

@ -13,13 +13,18 @@ WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
if (w) {
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
w->as.comboBox.buf = (char *)malloc(bufSize);
w->as.comboBox.undoBuf = (char *)malloc(bufSize);
w->as.comboBox.bufSize = bufSize;
if (w->as.comboBox.buf) {
if (!w->as.comboBox.buf || !w->as.comboBox.undoBuf) {
free(w->as.comboBox.buf);
free(w->as.comboBox.undoBuf);
w->as.comboBox.buf = NULL;
w->as.comboBox.undoBuf = NULL;
} else {
w->as.comboBox.buf[0] = '\0';
}
w->as.comboBox.undoBuf = (char *)malloc(bufSize);
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
w->as.comboBox.selectedIdx = -1;
@ -55,6 +60,19 @@ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) {
w->as.comboBox.items = items;
w->as.comboBox.itemCount = count;
// Cache max item strlen to avoid recomputing in calcMinSize
int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) {
int32_t slen = (int32_t)strlen(items[i]);
if (slen > maxLen) {
maxLen = slen;
}
}
w->as.comboBox.maxItemLen = maxLen;
if (w->as.comboBox.selectedIdx >= count) {
w->as.comboBox.selectedIdx = -1;
}
@ -90,14 +108,11 @@ void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
// ============================================================
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t maxItemW = font->charWidth * 8;
int32_t maxItemW = w->as.comboBox.maxItemLen * font->charWidth;
int32_t minW = font->charWidth * 8;
for (int32_t i = 0; i < w->as.comboBox.itemCount; i++) {
int32_t iw = (int32_t)strlen(w->as.comboBox.items[i]) * font->charWidth;
if (iw > maxItemW) {
maxItemW = iw;
}
if (maxItemW < minW) {
maxItemW = minW;
}
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;

View file

@ -7,6 +7,7 @@
// ============================================================
bool sDebugLayout = false;
WidgetT *sFocusedWidget = NULL;
WidgetT *sOpenPopup = NULL;
WidgetT *sPressedButton = NULL;
WidgetT *sDragSlider = NULL;
@ -108,7 +109,11 @@ void widgetDestroyChildren(WidgetT *w) {
child->wclass->destroy(child);
}
// Clear popup/drag references if they point to destroyed widgets
// Clear static references if they point to destroyed widgets
if (sFocusedWidget == child) {
sFocusedWidget = NULL;
}
if (sOpenPopup == child) {
sOpenPopup = NULL;
}

View file

@ -44,6 +44,19 @@ void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) {
w->as.dropdown.items = items;
w->as.dropdown.itemCount = count;
// Cache max item strlen to avoid recomputing in calcMinSize
int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) {
int32_t slen = (int32_t)strlen(items[i]);
if (slen > maxLen) {
maxLen = slen;
}
}
w->as.dropdown.maxItemLen = maxLen;
if (w->as.dropdown.selectedIdx >= count) {
w->as.dropdown.selectedIdx = -1;
}
@ -82,14 +95,11 @@ const char *widgetDropdownGetText(const WidgetT *w) {
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// Width: widest item + button width + border
int32_t maxItemW = font->charWidth * 8;
int32_t maxItemW = w->as.dropdown.maxItemLen * font->charWidth;
int32_t minW = font->charWidth * 8;
for (int32_t i = 0; i < w->as.dropdown.itemCount; i++) {
int32_t iw = (int32_t)strlen(w->as.dropdown.items[i]) * font->charWidth;
if (iw > maxItemW) {
maxItemW = iw;
}
if (maxItemW < minW) {
maxItemW = minW;
}
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;

View file

@ -114,29 +114,10 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return;
}
// Find the focused widget
WidgetT *focus = NULL;
// Use cached focus pointer — O(1) instead of O(n) tree walk
WidgetT *focus = sFocusedWidget;
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
while (top > 0) {
WidgetT *w = stack[--top];
if (w->focused && widgetIsFocusable(w->type)) {
focus = w;
break;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible && top < 64) {
stack[top++] = c;
}
}
}
if (!focus) {
if (!focus || !focus->focused || focus->window != win) {
return;
}
@ -183,7 +164,26 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
int32_t vx = x + scrollX;
int32_t vy = y + scrollY;
widgetTextDragUpdate(sDragTextSelect, root, vx, vy);
if (sDragTextSelect->type == WidgetAnsiTermE) {
// Fast path: repaint only dirty terminal rows into the
// content buffer, then dirty just that screen stripe.
int32_t dirtyY = 0;
int32_t dirtyH = 0;
if (wgtAnsiTermRepaint(sDragTextSelect, &dirtyY, &dirtyH) > 0) {
AppContextT *ctx = (AppContextT *)root->userData;
int32_t scrollY2 = win->vScroll ? win->vScroll->value : 0;
int32_t rectX = win->x + win->contentX;
int32_t rectY = win->y + win->contentY + dirtyY - scrollY2;
int32_t rectW = win->contentW;
win->contentDirty = true;
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
}
} else {
wgtInvalidate(root);
}
return;
}
@ -396,20 +396,10 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
return;
}
// Clear focus from all widgets, set focus on clicked widget
WidgetT *fstack[64];
int32_t ftop = 0;
fstack[ftop++] = root;
while (ftop > 0) {
WidgetT *w = fstack[--ftop];
w->focused = false;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (ftop < 64) {
fstack[ftop++] = c;
}
}
// Clear focus from previously focused widget (O(1) instead of tree walk)
if (sFocusedWidget) {
sFocusedWidget->focused = false;
sFocusedWidget = NULL;
}
// Dispatch to per-widget mouse handler via vtable
@ -417,6 +407,11 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
hit->wclass->onMouse(hit, root, vx, vy);
}
// Cache the newly focused widget
if (hit->focused) {
sFocusedWidget = hit;
}
wgtInvalidate(root);
}
@ -454,17 +449,20 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
// Clear background
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg);
// Apply scroll offset — layout at virtual size, positioned at -scroll
// Apply scroll offset and re-layout at virtual size
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t layoutW = DVX_MAX(win->contentW, root->calcMinW);
int32_t layoutH = DVX_MAX(win->contentH, root->calcMinH);
// Only re-layout if root position or size actually changed
if (root->x != -scrollX || root->y != -scrollY || root->w != layoutW || root->h != layoutH) {
root->x = -scrollX;
root->y = -scrollY;
root->w = layoutW;
root->h = layoutH;
widgetLayoutChildren(root, &ctx->font);
}
// Paint widget tree (clip rect limits drawing to visible area)
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);

View file

@ -87,6 +87,7 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
extern bool sDebugLayout;
extern WidgetT *sClosedPopup;
extern WidgetT *sFocusedWidget;
extern WidgetT *sKeyPressedBtn;
extern WidgetT *sOpenPopup;
extern WidgetT *sPressedButton;

View file

@ -48,6 +48,19 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
w->as.listBox.items = items;
w->as.listBox.itemCount = count;
// Cache max item strlen to avoid recomputing in calcMinSize
int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) {
int32_t slen = (int32_t)strlen(items[i]);
if (slen > maxLen) {
maxLen = slen;
}
}
w->as.listBox.maxItemLen = maxLen;
if (w->as.listBox.selectedIdx >= count) {
w->as.listBox.selectedIdx = count > 0 ? 0 : -1;
}
@ -76,14 +89,11 @@ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
// ============================================================
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t maxItemW = font->charWidth * 8;
int32_t maxItemW = w->as.listBox.maxItemLen * font->charWidth;
int32_t minW = font->charWidth * 8;
for (int32_t i = 0; i < w->as.listBox.itemCount; i++) {
int32_t iw = (int32_t)strlen(w->as.listBox.items[i]) * font->charWidth;
if (iw > maxItemW) {
maxItemW = iw;
}
if (maxItemW < minW) {
maxItemW = minW;
}
w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + LISTBOX_SB_W;

View file

@ -124,6 +124,10 @@ void wgtDestroy(WidgetT *w) {
}
// Clear static references
if (sFocusedWidget == w) {
sFocusedWidget = NULL;
}
if (sOpenPopup == w) {
sOpenPopup = NULL;
}

View file

@ -108,6 +108,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (next) {
w->focused = false;
next->focused = true;
sFocusedWidget = next;
next->parent->as.radioGroup.selectedIdx = next->as.radio.index;
if (next->parent->onChange) {
@ -131,6 +132,7 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (prev) {
w->focused = false;
prev->focused = true;
sFocusedWidget = prev;
prev->parent->as.radioGroup.selectedIdx = prev->as.radio.index;
if (prev->parent->onChange) {
@ -190,10 +192,10 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, w->as.radio.text);
drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
if (w->focused) {
int32_t labelW = textWidthAccel(font, w->as.radio.text);
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
}
}

View file

@ -25,7 +25,10 @@ static int32_t maskPrevSlot(const char *mask, int32_t pos);
static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod);
static int32_t textAreaCountLines(const char *buf, int32_t len);
static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col);
static inline void textAreaDirtyCache(WidgetT *w);
static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols);
static int32_t textAreaGetLineCount(WidgetT *w);
static int32_t textAreaGetMaxLineLen(WidgetT *w);
static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row);
static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row);
static int32_t textAreaMaxLineLen(const char *buf, int32_t len);
@ -42,6 +45,10 @@ static int32_t sClipboardLen = 0;
void clipboardCopy(const char *text, int32_t len) {
if (!text || len <= 0) {
return;
}
if (len > CLIPBOARD_MAX - 1) {
len = CLIPBOARD_MAX - 1;
}
@ -260,16 +267,23 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
if (w) {
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
w->as.textArea.buf = (char *)malloc(bufSize);
w->as.textArea.undoBuf = (char *)malloc(bufSize);
w->as.textArea.bufSize = bufSize;
if (w->as.textArea.buf) {
if (!w->as.textArea.buf || !w->as.textArea.undoBuf) {
free(w->as.textArea.buf);
free(w->as.textArea.undoBuf);
w->as.textArea.buf = NULL;
w->as.textArea.undoBuf = NULL;
} else {
w->as.textArea.buf[0] = '\0';
}
w->as.textArea.undoBuf = (char *)malloc(bufSize);
w->as.textArea.selAnchor = -1;
w->as.textArea.selCursor = -1;
w->as.textArea.desiredCol = 0;
w->as.textArea.cachedLines = -1;
w->as.textArea.cachedMaxLL = -1;
w->weight = 100;
}
@ -419,14 +433,22 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
// Ctrl+C — copy formatted text
if (key == 3) {
int32_t selLo = -1;
int32_t selHi = -1;
if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) {
selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd;
selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart;
int32_t selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd;
int32_t selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart;
if (selLo < 0) {
selLo = 0;
}
if (selHi > maskLen) {
selHi = maskLen;
}
if (selHi > selLo) {
clipboardCopy(buf + selLo, selHi - selLo);
}
}
return;
}
@ -463,7 +485,7 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
if (changed) {
*pCur = slotPos;
*pCur = slotPos <= maskLen ? slotPos : maskLen;
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
@ -478,12 +500,18 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
// Ctrl+X — copy and clear selected slots
if (key == 24) {
int32_t selLo = -1;
int32_t selHi = -1;
if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) {
selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd;
selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart;
int32_t selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd;
int32_t selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart;
if (selLo < 0) {
selLo = 0;
}
if (selHi > maskLen) {
selHi = maskLen;
}
clipboardCopy(buf + selLo, selHi - selLo);
if (w->as.textInput.undoBuf) {
@ -510,14 +538,15 @@ static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
// Ctrl+Z — undo
if (key == 26 && w->as.textInput.undoBuf) {
char tmpBuf[CLIPBOARD_MAX];
char tmpBuf[256];
int32_t tmpLen = maskLen + 1 < (int32_t)sizeof(tmpBuf) ? maskLen + 1 : (int32_t)sizeof(tmpBuf);
int32_t tmpCursor = *pCur;
memcpy(tmpBuf, buf, maskLen + 1);
memcpy(tmpBuf, buf, tmpLen);
memcpy(buf, w->as.textInput.undoBuf, maskLen + 1);
*pCur = w->as.textInput.undoCursor < maskLen ? w->as.textInput.undoCursor : maskLen;
memcpy(w->as.textInput.undoBuf, tmpBuf, maskLen + 1);
memcpy(w->as.textInput.undoBuf, tmpBuf, tmpLen);
w->as.textInput.undoCursor = tmpCursor;
w->as.textInput.selStart = -1;
@ -689,6 +718,15 @@ static int32_t textAreaCountLines(const char *buf, int32_t len) {
}
static int32_t textAreaGetLineCount(WidgetT *w) {
if (w->as.textArea.cachedLines < 0) {
w->as.textArea.cachedLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len);
}
return w->as.textArea.cachedLines;
}
static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) {
(void)len;
int32_t off = 0;
@ -742,6 +780,21 @@ static int32_t textAreaMaxLineLen(const char *buf, int32_t len) {
}
static int32_t textAreaGetMaxLineLen(WidgetT *w) {
if (w->as.textArea.cachedMaxLL < 0) {
w->as.textArea.cachedMaxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len);
}
return w->as.textArea.cachedMaxLL;
}
static inline void textAreaDirtyCache(WidgetT *w) {
w->as.textArea.cachedLines = -1;
w->as.textArea.cachedMaxLL = -1;
}
static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col) {
int32_t start = textAreaLineStart(buf, len, row);
int32_t lineL = textAreaLineLen(buf, len, row);
@ -844,7 +897,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
const BitmapFontT *font = &ctx->font;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = textAreaMaxLineLen(buf, *pLen);
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
@ -857,7 +910,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
visCols = 1;
}
int32_t totalLines = textAreaCountLines(buf, *pLen);
int32_t totalLines = textAreaGetLineCount(w);
// Helper macros for cursor offset
#define CUR_OFF() textAreaCursorToOff(buf, *pLen, *pRow, *pCol)
@ -877,6 +930,17 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
#define SEL_LO() (*pSA < *pSC ? *pSA : *pSC)
#define SEL_HI() (*pSA < *pSC ? *pSC : *pSA)
// Clamp selection to buffer bounds
if (HAS_SEL()) {
if (*pSA > *pLen) {
*pSA = *pLen;
}
if (*pSC > *pLen) {
*pSC = *pLen;
}
}
// Ctrl+A — select all
if (key == 1) {
*pSA = 0;
@ -927,6 +991,8 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->onChange) {
w->onChange(w);
}
textAreaDirtyCache(w);
}
textAreaEnsureVisible(w, visRows, visCols);
@ -952,6 +1018,8 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->onChange) {
w->onChange(w);
}
textAreaDirtyCache(w);
}
textAreaEnsureVisible(w, visRows, visCols);
@ -994,6 +1062,8 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->onChange) {
w->onChange(w);
}
textAreaDirtyCache(w);
}
textAreaEnsureVisible(w, visRows, visCols);
@ -1030,6 +1100,8 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->onChange) {
w->onChange(w);
}
textAreaDirtyCache(w);
}
textAreaEnsureVisible(w, visRows, visCols);
@ -1049,6 +1121,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pSA = -1;
*pSC = -1;
w->as.textArea.desiredCol = *pCol;
textAreaDirtyCache(w);
if (w->onChange) {
w->onChange(w);
@ -1062,6 +1135,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
(*pLen)--;
textAreaOffToRowCol(buf, off - 1, pRow, pCol);
w->as.textArea.desiredCol = *pCol;
textAreaDirtyCache(w);
if (w->onChange) {
w->onChange(w);
@ -1086,6 +1160,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
*pSA = -1;
*pSC = -1;
w->as.textArea.desiredCol = *pCol;
textAreaDirtyCache(w);
if (w->onChange) {
w->onChange(w);
@ -1097,6 +1172,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
textEditSaveUndo(buf, *pLen, off, w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize);
memmove(buf + off, buf + off + 1, *pLen - off);
(*pLen)--;
textAreaDirtyCache(w);
if (w->onChange) {
w->onChange(w);
@ -1277,6 +1353,8 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
w->as.textArea.desiredCol = *pCol;
}
textAreaDirtyCache(w);
if (w->onChange) {
w->onChange(w);
}
@ -1311,7 +1389,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
int32_t innerY = w->y + TEXTAREA_BORDER;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len);
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
@ -1529,11 +1607,11 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
int32_t innerY = w->y + TEXTAREA_BORDER;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len);
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
int32_t totalLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len);
int32_t totalLines = textAreaGetLineCount(w);
if (visRows < 1) {
visRows = 1;
@ -1656,11 +1734,11 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t len = w->as.textArea.len;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = textAreaMaxLineLen(buf, len);
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
int32_t totalLines = textAreaCountLines(buf, len);
int32_t totalLines = textAreaGetLineCount(w);
bool needVSb = (totalLines > visRows);
// Sunken border
@ -1694,9 +1772,10 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
selHi = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selCursor : w->as.textArea.selAnchor;
}
// Draw lines
// Draw lines — compute first visible line offset once, then advance incrementally
int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD;
int32_t textY = w->y + TEXTAREA_BORDER;
int32_t lineOff = textAreaLineStart(buf, len, w->as.textArea.scrollRow);
for (int32_t i = 0; i < visRows; i++) {
int32_t row = w->as.textArea.scrollRow + i;
@ -1705,8 +1784,11 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
break;
}
int32_t lineOff = textAreaLineStart(buf, len, row);
int32_t lineL = textAreaLineLen(buf, len, row);
// Compute line length by scanning from lineOff (not from buffer start)
int32_t lineL = 0;
while (lineOff + lineL < len && buf[lineOff + lineL] != '\n') {
lineL++;
}
int32_t drawY = textY + i * font->charHeight;
for (int32_t j = 0; j < visCols; j++) {
@ -1740,6 +1822,12 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
rectFill(d, ops, drawX, drawY, font->charWidth, font->charHeight, cbgc);
}
}
// Advance lineOff to the next line
lineOff += lineL;
if (lineOff < len && buf[lineOff] == '\n') {
lineOff++;
}
}
// Draw cursor
@ -1881,6 +1969,8 @@ void widgetTextAreaSetText(WidgetT *w, const char *text) {
w->as.textArea.desiredCol = 0;
w->as.textArea.selAnchor = -1;
w->as.textArea.selCursor = -1;
w->as.textArea.cachedLines = -1;
w->as.textArea.cachedMaxLL = -1;
}
}
@ -2097,6 +2187,23 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1;
int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1;
// Clamp selection to buffer bounds
if (hasSel) {
if (selLo < 0) {
selLo = 0;
}
if (selHi > *pLen) {
selHi = *pLen;
}
if (selLo >= selHi) {
hasSel = false;
selLo = -1;
selHi = -1;
}
}
// Ctrl+A — select all
if (key == 1 && pSelStart && pSelEnd) {
*pSelStart = 0;

View file

@ -14,10 +14,11 @@ LIBDIR = ../lib
SRCS = demo.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(BINDIR)/demo.exe
BMPS = $(wildcard *.bmp)
.PHONY: all clean lib
all: lib $(TARGET)
all: lib $(TARGET) $(addprefix $(BINDIR)/,$(BMPS))
lib:
$(MAKE) -C ../dvx
@ -37,6 +38,9 @@ $(OBJDIR):
$(BINDIR):
mkdir -p $(BINDIR)
$(BINDIR)/%.bmp: %.bmp | $(BINDIR)
cp $< $@
# Dependencies
$(OBJDIR)/demo.o: demo.c ../dvx/dvxApp.h ../dvx/dvxWidget.h