diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 05a5aab..9c00903 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -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 diff --git a/dvx/dvxComp.c b/dvx/dvxComp.c index a8bbb3e..9998b69 100644 --- a/dvx/dvxComp.c +++ b/dvx/dvxComp.c @@ -138,20 +138,22 @@ void flushRect(DisplayT *d, const RectT *r) { } } else { // Partial scanlines — copy row by row with rep movsd + int32_t dwords = rowBytes >> 2; + int32_t remainder = rowBytes & 3; for (int32_t i = 0; i < h; i++) { - int32_t dwords = rowBytes >> 2; - int32_t remainder = rowBytes & 3; - uint8_t *s = src; - uint8_t *dd = dst; + 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++; } } diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index 893dd29..af7d3d8 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -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; } } } diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 80f591d..3682425 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -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; diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index 184312e..6663abc 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -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; diff --git a/dvx/widgets/widgetCanvas.c b/dvx/widgets/widgetCanvas.c index 6ce4d2e..3db5fc9 100644 --- a/dvx/widgets/widgetCanvas.c +++ b/dvx/widgets/widgetCanvas.c @@ -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; - for (int32_t py = y0; py < y1; py++) { - for (int32_t px = x0; px < x1; px++) { - uint8_t *dst = data + py * pitch + px * bpp; + if (fillW <= 0) { + return; + } - canvasPutPixel(dst, color, bpp); + // 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++) { + 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); diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index 9f20d3f..2f32a7b 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -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); } } diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 361ff65..481cd6f 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -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; diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 8ba7ef6..aec52ad 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -6,10 +6,11 @@ // Global state for drag and popup tracking // ============================================================ -bool sDebugLayout = false; -WidgetT *sOpenPopup = NULL; -WidgetT *sPressedButton = NULL; -WidgetT *sDragSlider = NULL; +bool sDebugLayout = false; +WidgetT *sFocusedWidget = NULL; +WidgetT *sOpenPopup = NULL; +WidgetT *sPressedButton = NULL; +WidgetT *sDragSlider = NULL; WidgetT *sDrawingCanvas = NULL; WidgetT *sDragTextSelect = NULL; int32_t sDragOffset = 0; @@ -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; } diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index 0dcdb4e..6c535a9 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -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; diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 68030be..8da293a 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -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); - wgtInvalidate(root); + + 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); - root->x = -scrollX; - root->y = -scrollY; - root->w = layoutW; - root->h = layoutH; - widgetLayoutChildren(root, &ctx->font); + // 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); diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index ba90575..525300b 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -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; diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index 6d662ad..82834b3 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -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; diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index cc4b0c9..9a9c35d 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -124,6 +124,10 @@ void wgtDestroy(WidgetT *w) { } // Clear static references + if (sFocusedWidget == w) { + sFocusedWidget = NULL; + } + if (sOpenPopup == w) { sOpenPopup = NULL; } diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c index 1d125f6..f3af09d 100644 --- a/dvx/widgets/widgetRadio.c +++ b/dvx/widgets/widgetRadio.c @@ -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); } } diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index eba89e8..c976876 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -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.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,13 +433,21 @@ 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; - clipboardCopy(buf + selLo, selHi - selLo); + 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,9 +1784,12 @@ 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); - int32_t drawY = textY + i * font->charHeight; + // 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++) { int32_t col = w->as.textArea.scrollCol + 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 @@ -1873,14 +1961,16 @@ void widgetTextAreaSetText(WidgetT *w, const char *text) { if (w->as.textArea.buf) { strncpy(w->as.textArea.buf, text, w->as.textArea.bufSize - 1); w->as.textArea.buf[w->as.textArea.bufSize - 1] = '\0'; - w->as.textArea.len = (int32_t)strlen(w->as.textArea.buf); - w->as.textArea.cursorRow = 0; - w->as.textArea.cursorCol = 0; - w->as.textArea.scrollRow = 0; - w->as.textArea.scrollCol = 0; + w->as.textArea.len = (int32_t)strlen(w->as.textArea.buf); + w->as.textArea.cursorRow = 0; + w->as.textArea.cursorCol = 0; + w->as.textArea.scrollRow = 0; + w->as.textArea.scrollCol = 0; w->as.textArea.desiredCol = 0; - w->as.textArea.selAnchor = -1; - w->as.textArea.selCursor = -1; + 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; diff --git a/dvxdemo/Makefile b/dvxdemo/Makefile index b9c4d23..29b52ce 100644 --- a/dvxdemo/Makefile +++ b/dvxdemo/Makefile @@ -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