diff --git a/dvx/Makefile b/dvx/Makefile index 31ecc04..74f4566 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -16,6 +16,7 @@ SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp WSRCS = widgets/widgetAnsiTerm.c \ widgets/widgetClass.c \ widgets/widgetCore.c \ + widgets/widgetScrollbar.c \ widgets/widgetLayout.c \ widgets/widgetEvent.c \ widgets/widgetOps.c \ @@ -112,6 +113,7 @@ $(WOBJDIR)/widgetStatusBar.o: widgets/widgetStatusBar.c $(WIDGET_DEPS) $(WOBJDIR)/widgetTabControl.o: widgets/widgetTabControl.c $(WIDGET_DEPS) $(WOBJDIR)/widgetTextInput.o: widgets/widgetTextInput.c $(WIDGET_DEPS) $(WOBJDIR)/widgetToolbar.o: widgets/widgetToolbar.c $(WIDGET_DEPS) +$(WOBJDIR)/widgetScrollbar.o: widgets/widgetScrollbar.c $(WIDGET_DEPS) $(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS) clean: diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index e1bfa32..9954469 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -53,6 +53,7 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win); static void pollKeyboard(AppContextT *ctx); static void pollMouse(AppContextT *ctx); static void refreshMinimizedIcons(AppContextT *ctx); +static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); static void updateCursorShape(AppContextT *ctx); static void updateTooltip(AppContextT *ctx); @@ -178,19 +179,11 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT); AccelTableT *table = win->accelTable; + // Match against pre-normalized keys (Item 6) for (int32_t i = 0; i < table->count; i++) { AccelEntryT *e = &table->entries[i]; - // Uppercase the entry key too - int32_t entryKey = e->key; - - if (entryKey >= 'a' && entryKey <= 'z') { - entryKey = entryKey - 32; - } - - int32_t entryMods = e->modifiers & (ACCEL_CTRL | ACCEL_ALT); - - if (entryKey == matchKey && entryMods == requiredMods) { + if (e->normKey == matchKey && e->normMods == requiredMods) { win->onMenu(win, e->cmdId); return true; } @@ -313,6 +306,18 @@ static void compositeAndFlush(AppContextT *ctx) { dirtyListMerge(dl); + // Pre-filter visible, non-minimized windows once (Item 7) + int32_t visibleIdx[MAX_WINDOWS]; + int32_t visibleCount = 0; + + for (int32_t j = 0; j < ws->count; j++) { + WindowT *win = ws->windows[j]; + + if (win->visible && !win->minimized) { + visibleIdx[visibleCount++] = j; + } + } + for (int32_t i = 0; i < dl->count; i++) { RectT *dr = &dl->rects[i]; @@ -332,13 +337,9 @@ static void compositeAndFlush(AppContextT *ctx) { // 2. Draw minimized window icons (under all windows) wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr); - // 3. Walk window stack bottom-to-top - for (int32_t j = 0; j < ws->count; j++) { - WindowT *win = ws->windows[j]; - - if (!win->visible || win->minimized) { - continue; - } + // 3. Walk pre-filtered visible window list bottom-to-top + for (int32_t vi = 0; vi < visibleCount; vi++) { + WindowT *win = ws->windows[visibleIdx[vi]]; // Check if window intersects this dirty rect RectT winRect = {win->x, win->y, win->w, win->h}; @@ -660,7 +661,7 @@ static void dispatchEvents(AppContextT *ctx) { // Hover tracking int32_t relY = my - ctx->sysMenu.popupY - 2; - int32_t itemIdx = relY / ctx->font.charHeight; + int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0; if (itemIdx >= 0 && itemIdx < ctx->sysMenu.itemCount && ctx->sysMenu.items[itemIdx].separator) { itemIdx = -1; @@ -700,7 +701,7 @@ static void dispatchEvents(AppContextT *ctx) { if (inCurrent) { // Hover tracking in current level int32_t relY = my - ctx->popup.popupY - 2; - int32_t itemIdx = relY / ctx->font.charHeight; + int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0; if (itemIdx < 0) { itemIdx = 0; @@ -780,7 +781,7 @@ static void dispatchEvents(AppContextT *ctx) { // Now current level IS this parent — update hover int32_t relY = my - ctx->popup.popupY - 2; - int32_t itemIdx = relY / ctx->font.charHeight; + int32_t itemIdx = (relY >= 0) ? (int32_t)((uint32_t)relY * ctx->charHeightRecip >> 16) : 0; if (itemIdx < 0) { itemIdx = 0; @@ -1063,10 +1064,19 @@ void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmd return; } + // Pre-normalize key for fast matching (Item 6) + int32_t normKey = key; + + if (normKey >= 'a' && normKey <= 'z') { + normKey = normKey - 32; + } + AccelEntryT *e = &table->entries[table->count++]; e->key = key; e->modifiers = modifiers; e->cmdId = cmdId; + e->normKey = normKey; + e->normMods = modifiers & (ACCEL_CTRL | ACCEL_ALT); } @@ -1121,34 +1131,7 @@ void dvxCascadeWindows(AppContextT *ctx) { continue; } - // Un-maximize if needed - if (win->maximized) { - win->maximized = false; - } - - // Dirty old position - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - - win->x = offsetX; - win->y = offsetY; - win->w = winW; - win->h = winH; - - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); - - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->contentDirty = true; - } - - // Dirty new position - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + repositionWindow(ctx, win, offsetX, offsetY, winW, winH); offsetX += step; offsetY += step; @@ -1311,11 +1294,12 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ // Initialize mouse initMouse(ctx); - ctx->running = true; - ctx->lastIconClickId = -1; - ctx->lastIconClickTime = 0; - ctx->lastCloseClickId = -1; + ctx->running = true; + ctx->lastIconClickId = -1; + ctx->lastIconClickTime = 0; + ctx->lastCloseClickId = -1; ctx->lastCloseClickTime = 0; + ctx->charHeightRecip = ((uint32_t)0x10000 + (uint32_t)ctx->font.charHeight - 1) / (uint32_t)ctx->font.charHeight; // Dirty the entire screen for first paint dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height); @@ -1573,12 +1557,6 @@ void dvxTileWindows(AppContextT *ctx) { continue; } - if (win->maximized) { - win->maximized = false; - } - - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - int32_t row = slot / cols; int32_t col = slot % cols; @@ -1587,25 +1565,7 @@ void dvxTileWindows(AppContextT *ctx) { int32_t rowCols = (remaining < cols) ? remaining : cols; int32_t cellW = screenW / rowCols; - win->x = col * cellW; - win->y = row * tileH; - win->w = cellW; - win->h = tileH; - - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); - - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->contentDirty = true; - } - - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + repositionWindow(ctx, win, col * cellW, row * tileH, cellW, tileH); slot++; } @@ -1653,31 +1613,7 @@ void dvxTileWindowsH(AppContextT *ctx) { continue; } - if (win->maximized) { - win->maximized = false; - } - - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - - win->x = slot * tileW; - win->y = 0; - win->w = tileW; - win->h = screenH; - - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); - - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->contentDirty = true; - } - - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + repositionWindow(ctx, win, slot * tileW, 0, tileW, screenH); slot++; } @@ -1725,31 +1661,7 @@ void dvxTileWindowsV(AppContextT *ctx) { continue; } - if (win->maximized) { - win->maximized = false; - } - - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); - - win->x = 0; - win->y = slot * tileH; - win->w = screenW; - win->h = tileH; - - wmUpdateContentRect(win); - wmReallocContentBuf(win, &ctx->display); - - if (win->onResize) { - win->onResize(win, win->contentW, win->contentH); - } - - if (win->onPaint) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - win->onPaint(win, &fullRect); - win->contentDirty = true; - } - - dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + repositionWindow(ctx, win, 0, slot * tileH, screenW, tileH); slot++; } @@ -3038,6 +2950,42 @@ static void refreshMinimizedIcons(AppContextT *ctx) { } +// ============================================================ +// repositionWindow — move/resize a window, dirty old & new, fire callbacks +// ============================================================ + +static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h) { + // Dirty old position + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); + + // Un-maximize if needed + if (win->maximized) { + win->maximized = false; + } + + win->x = x; + win->y = y; + win->w = w; + win->h = h; + + wmUpdateContentRect(win); + wmReallocContentBuf(win, &ctx->display); + + if (win->onResize) { + win->onResize(win, win->contentW, win->contentH); + } + + if (win->onPaint) { + RectT fullRect = {0, 0, win->contentW, win->contentH}; + win->onPaint(win, &fullRect); + win->contentDirty = true; + } + + // Dirty new position + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); +} + + // ============================================================ // updateCursorShape // ============================================================ diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 43a6bd5..fc698ce 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -52,6 +52,7 @@ typedef struct AppContextT { int32_t tooltipY; int32_t tooltipW; // size (pre-computed) int32_t tooltipH; + uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight } AppContextT; // Initialize the application (VESA mode, input, etc.) diff --git a/dvx/dvxComp.c b/dvx/dvxComp.c index 9185c6f..13ccff9 100644 --- a/dvx/dvxComp.c +++ b/dvx/dvxComp.c @@ -77,13 +77,15 @@ void dirtyListMerge(DirtyListT *dl) { return; } - // Single-pass O(N²): for each rect, try to merge it into an - // earlier rect. When a merge succeeds the merged rect may now - // overlap others, so restart the inner scan for that slot. + // O(N²) with bounded restarts: for each rect, try to merge it + // into an earlier rect. When a merge succeeds the merged rect + // may now overlap others, so restart the inner scan — but cap + // restarts per slot to avoid O(N³) pathological cascades. for (int32_t i = 0; i < dl->count; i++) { - bool merged = true; + int32_t restarts = 0; + bool merged = true; - while (merged) { + while (merged && restarts < 3) { merged = false; for (int32_t j = i + 1; j < dl->count; j++) { @@ -95,6 +97,8 @@ void dirtyListMerge(DirtyListT *dl) { merged = true; } } + + restarts++; } } } diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index 35afb88..13a4e14 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -185,77 +185,151 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 if (x < clipX1) { colStart = clipX1 - x; } if (x + cw > clipX2) { colEnd = clipX2 - x; } + // Unclipped fast path: full 8-pixel character cell with direct bit + // tests eliminates loop overhead and sGlyphBit[] lookups (Item 4) + bool unclipped = (colStart == 0 && colEnd == cw); + if (opaque) { // Opaque mode: fill entire cell with bg, then overwrite fg pixels - // This avoids a branch per pixel for the common case - for (int32_t row = rowStart; row < rowEnd; row++) { - int32_t py = y + row; - uint8_t *dst = d->backBuf + py * pitch + (x + colStart) * bpp; - int32_t span = colEnd - colStart; + if (unclipped && bpp == 4) { + for (int32_t row = rowStart; row < rowEnd; row++) { + uint32_t *dst32 = (uint32_t *)(d->backBuf + (y + row) * pitch + x * 4); + uint8_t bits = glyph[row]; - // Fill row with background - ops->spanFill(dst, bg, span); - - // Overwrite foreground pixels - uint8_t bits = glyph[row]; - if (bits == 0) { - continue; // entirely background row — already filled + dst32[0] = (bits & 0x80) ? fg : bg; + dst32[1] = (bits & 0x40) ? fg : bg; + dst32[2] = (bits & 0x20) ? fg : bg; + dst32[3] = (bits & 0x10) ? fg : bg; + dst32[4] = (bits & 0x08) ? fg : bg; + dst32[5] = (bits & 0x04) ? fg : bg; + dst32[6] = (bits & 0x02) ? fg : bg; + dst32[7] = (bits & 0x01) ? fg : bg; } + } else if (unclipped && bpp == 2) { + uint16_t fg16 = (uint16_t)fg; + uint16_t bg16 = (uint16_t)bg; - // Shift bits to account for colStart - dst = d->backBuf + py * pitch + x * bpp; + for (int32_t row = rowStart; row < rowEnd; row++) { + uint16_t *dst16 = (uint16_t *)(d->backBuf + (y + row) * pitch + x * 2); + uint8_t bits = glyph[row]; - if (bpp == 2) { - uint16_t fg16 = (uint16_t)fg; - for (int32_t col = colStart; col < colEnd; col++) { - if (bits & sGlyphBit[col]) { - *(uint16_t *)(dst + col * 2) = fg16; - } + dst16[0] = (bits & 0x80) ? fg16 : bg16; + dst16[1] = (bits & 0x40) ? fg16 : bg16; + dst16[2] = (bits & 0x20) ? fg16 : bg16; + dst16[3] = (bits & 0x10) ? fg16 : bg16; + dst16[4] = (bits & 0x08) ? fg16 : bg16; + dst16[5] = (bits & 0x04) ? fg16 : bg16; + dst16[6] = (bits & 0x02) ? fg16 : bg16; + dst16[7] = (bits & 0x01) ? fg16 : bg16; + } + } else { + // Clipped path or 8bpp: spanFill bg then overwrite fg + for (int32_t row = rowStart; row < rowEnd; row++) { + int32_t py = y + row; + uint8_t *dst = d->backBuf + py * pitch + (x + colStart) * bpp; + + ops->spanFill(dst, bg, colEnd - colStart); + + uint8_t bits = glyph[row]; + if (bits == 0) { + continue; } - } else if (bpp == 4) { - for (int32_t col = colStart; col < colEnd; col++) { - if (bits & sGlyphBit[col]) { - *(uint32_t *)(dst + col * 4) = fg; + + dst = d->backBuf + py * pitch + x * bpp; + + if (bpp == 2) { + uint16_t fg16 = (uint16_t)fg; + for (int32_t col = colStart; col < colEnd; col++) { + if (bits & sGlyphBit[col]) { + *(uint16_t *)(dst + col * 2) = fg16; + } } - } - } else { - uint8_t fg8 = (uint8_t)fg; - for (int32_t col = colStart; col < colEnd; col++) { - if (bits & sGlyphBit[col]) { - dst[col] = fg8; + } else if (bpp == 4) { + for (int32_t col = colStart; col < colEnd; 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 & sGlyphBit[col]) { + dst[col] = fg8; + } } } } } } else { // Transparent mode: only write foreground pixels - for (int32_t row = rowStart; row < rowEnd; row++) { - uint8_t bits = glyph[row]; - if (bits == 0) { - continue; + if (unclipped && bpp == 4) { + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t bits = glyph[row]; + if (bits == 0) { + continue; + } + + uint32_t *dst32 = (uint32_t *)(d->backBuf + (y + row) * pitch + x * 4); + + if (bits & 0x80) { dst32[0] = fg; } + if (bits & 0x40) { dst32[1] = fg; } + if (bits & 0x20) { dst32[2] = fg; } + if (bits & 0x10) { dst32[3] = fg; } + if (bits & 0x08) { dst32[4] = fg; } + if (bits & 0x04) { dst32[5] = fg; } + if (bits & 0x02) { dst32[6] = fg; } + if (bits & 0x01) { dst32[7] = fg; } } + } else if (unclipped && bpp == 2) { + uint16_t fg16 = (uint16_t)fg; - int32_t py = y + row; - uint8_t *dst = d->backBuf + py * pitch + x * bpp; + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t bits = glyph[row]; + if (bits == 0) { + continue; + } - if (bpp == 2) { - uint16_t fg16 = (uint16_t)fg; - for (int32_t col = colStart; col < colEnd; col++) { - if (bits & sGlyphBit[col]) { - *(uint16_t *)(dst + col * 2) = fg16; - } + uint16_t *dst16 = (uint16_t *)(d->backBuf + (y + row) * pitch + x * 2); + + if (bits & 0x80) { dst16[0] = fg16; } + if (bits & 0x40) { dst16[1] = fg16; } + if (bits & 0x20) { dst16[2] = fg16; } + if (bits & 0x10) { dst16[3] = fg16; } + if (bits & 0x08) { dst16[4] = fg16; } + if (bits & 0x04) { dst16[5] = fg16; } + if (bits & 0x02) { dst16[6] = fg16; } + if (bits & 0x01) { dst16[7] = fg16; } + } + } else { + // Clipped path or 8bpp + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t bits = glyph[row]; + if (bits == 0) { + continue; } - } else if (bpp == 4) { - for (int32_t col = colStart; col < colEnd; col++) { - if (bits & sGlyphBit[col]) { - *(uint32_t *)(dst + col * 4) = fg; + + int32_t py = y + row; + uint8_t *dst = d->backBuf + py * pitch + x * bpp; + + if (bpp == 2) { + uint16_t fg16 = (uint16_t)fg; + for (int32_t col = colStart; col < colEnd; col++) { + if (bits & sGlyphBit[col]) { + *(uint16_t *)(dst + col * 2) = fg16; + } } - } - } else { - uint8_t fg8 = (uint8_t)fg; - for (int32_t col = colStart; col < colEnd; col++) { - if (bits & sGlyphBit[col]) { - dst[col] = fg8; + } else if (bpp == 4) { + for (int32_t col = colStart; col < colEnd; 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 & sGlyphBit[col]) { + dst[col] = fg8; + } } } } @@ -916,12 +990,29 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, return; } - uint8_t *row = d->backBuf + y * d->pitch + x * d->format.bytesPerPixel; + int32_t bpp = d->format.bytesPerPixel; + uint8_t *row = d->backBuf + y * d->pitch + x * bpp; int32_t pitch = d->pitch; - for (int32_t i = 0; i < h; i++) { - ops->spanFill(row, color, w); - row += pitch; + // Inline rep stosl for 32bpp — avoids function pointer call overhead + // per scanline which is significant for the many 1px fills (Item 10) + if (__builtin_expect(bpp == 4, 1)) { + for (int32_t i = 0; i < h; i++) { + int32_t count = w; + uint8_t *dst = row; + __asm__ __volatile__ ( + "rep stosl" + : "+D"(dst), "+c"(count) + : "a"(color) + : "memory" + ); + row += pitch; + } + } else { + for (int32_t i = 0; i < h; i++) { + ops->spanFill(row, color, w); + row += pitch; + } } } @@ -1009,10 +1100,12 @@ static void spanFill8(uint8_t *dst, uint32_t color, int32_t count) { uint8_t c = (uint8_t)color; uint32_t dword = (uint32_t)c | ((uint32_t)c << 8) | ((uint32_t)c << 16) | ((uint32_t)c << 24); - // Align to 4 bytes - while (((uintptr_t)dst & 3) && count > 0) { - *dst++ = c; - count--; + // Align to 4 bytes — skip if already aligned (Item 11) + if (__builtin_expect((uintptr_t)dst & 3, 0)) { + while (((uintptr_t)dst & 3) && count > 0) { + *dst++ = c; + count--; + } } if (count >= 4) { diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 07d1fbc..9223894 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -184,6 +184,7 @@ struct MenuT { typedef struct { MenuT menus[MAX_MENUS]; int32_t menuCount; + bool positionsDirty; // true = barX/barW need recomputation } MenuBarT; // ============================================================ @@ -243,6 +244,8 @@ typedef struct { int32_t key; // key code: ASCII char or KEY_Fxx for extended int32_t modifiers; // ACCEL_CTRL, ACCEL_SHIFT, ACCEL_ALT (OR'd together) int32_t cmdId; // command ID passed to onMenu + int32_t normKey; // pre-normalized key (uppercased) for fast matching + int32_t normMods; // pre-masked modifiers (CTRL|ALT only) for fast matching } AccelEntryT; typedef struct { diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 7c114b3..875b7d4 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -132,6 +132,83 @@ typedef enum { InputMaskedE // format mask (e.g. "(###) ###-####") } InputModeE; +// ============================================================ +// Large widget data (separately allocated to reduce WidgetT size) +// ============================================================ + +typedef struct { + uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes + int32_t cols; // columns (default 80) + int32_t rows; // rows (default 25) + int32_t cursorRow; // 0-based cursor row + int32_t cursorCol; // 0-based cursor column + bool cursorVisible; + bool wrapMode; // auto-wrap at right margin + bool bold; // SGR bold flag (brightens foreground) + bool originMode; // cursor positioning relative to scroll region + bool csiPrivate; // '?' prefix in CSI sequence + uint8_t curAttr; // current text attribute (fg | bg<<4) + uint8_t parseState; // 0=normal, 1=ESC, 2=CSI + int32_t params[8]; // CSI parameter accumulator + int32_t paramCount; // number of CSI params collected + int32_t savedRow; // saved cursor position (SCP) + int32_t savedCol; + // Scrolling region (0-based, inclusive) + int32_t scrollTop; // top row of scroll region + int32_t scrollBot; // bottom row of scroll region + // Scrollback + uint8_t *scrollback; // circular buffer of scrollback lines + int32_t scrollbackMax; // max lines in scrollback buffer + int32_t scrollbackCount; // current number of lines stored + int32_t scrollbackHead; // write position (circular index) + int32_t scrollPos; // view position (scrollbackCount = live) + // Blink support + bool blinkVisible; // current blink phase (true = text visible) + clock_t blinkTime; // timestamp of last blink toggle + // Cursor blink + bool cursorOn; // current cursor blink phase + clock_t cursorTime; // timestamp of last cursor toggle + // Dirty tracking for fast repaint + uint32_t dirtyRows; // bitmask of rows needing repaint + int32_t lastCursorRow; // cursor row at last repaint + int32_t lastCursorCol; // cursor col at last repaint + // Cached packed palette (avoids packColor per repaint) + uint32_t packedPalette[16]; + bool paletteValid; + // Selection (line indices in scrollback+screen space) + int32_t selStartLine; + int32_t selStartCol; + int32_t selEndLine; + int32_t selEndCol; + bool selecting; + // Communications interface (all NULL = disconnected) + void *commCtx; + int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); + int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); +} AnsiTermDataT; + +typedef struct { + const ListViewColT *cols; + int32_t colCount; + const char **cellData; + int32_t rowCount; + int32_t selectedIdx; + int32_t scrollPos; + int32_t scrollPosH; + int32_t sortCol; + ListViewSortE sortDir; + int32_t resolvedColW[LISTVIEW_MAX_COLS]; + int32_t totalColW; + int32_t *sortIndex; + bool multiSelect; + int32_t anchorIdx; + uint8_t *selBits; + bool reorderable; + int32_t dragIdx; + int32_t dropIdx; + void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); +} ListViewDataT; + // ============================================================ // Widget structure // ============================================================ @@ -142,6 +219,7 @@ typedef struct WidgetT { WidgetTypeE type; const struct WidgetClassT *wclass; char name[MAX_WIDGET_NAME]; + uint32_t nameHash; // djb2 hash of name, 0 if unnamed // Tree linkage struct WidgetT *parent; @@ -372,78 +450,9 @@ typedef struct WidgetT { int32_t lastY; } canvas; - struct { - uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes - int32_t cols; // columns (default 80) - int32_t rows; // rows (default 25) - int32_t cursorRow; // 0-based cursor row - int32_t cursorCol; // 0-based cursor column - bool cursorVisible; - bool wrapMode; // auto-wrap at right margin - bool bold; // SGR bold flag (brightens foreground) - bool originMode; // cursor positioning relative to scroll region - bool csiPrivate; // '?' prefix in CSI sequence - uint8_t curAttr; // current text attribute (fg | bg<<4) - uint8_t parseState; // 0=normal, 1=ESC, 2=CSI - int32_t params[8]; // CSI parameter accumulator - int32_t paramCount; // number of CSI params collected - int32_t savedRow; // saved cursor position (SCP) - int32_t savedCol; - // Scrolling region (0-based, inclusive) - int32_t scrollTop; // top row of scroll region - int32_t scrollBot; // bottom row of scroll region - // Scrollback - uint8_t *scrollback; // circular buffer of scrollback lines - int32_t scrollbackMax; // max lines in scrollback buffer - int32_t scrollbackCount; // current number of lines stored - int32_t scrollbackHead; // write position (circular index) - int32_t scrollPos; // view position (scrollbackCount = live) - // Blink support - bool blinkVisible; // current blink phase (true = text visible) - clock_t blinkTime; // timestamp of last blink toggle - // Cursor blink - bool cursorOn; // current cursor blink phase - clock_t cursorTime; // timestamp of last cursor toggle - // Dirty tracking for fast repaint - uint32_t dirtyRows; // bitmask of rows needing repaint - int32_t lastCursorRow; // cursor row at last repaint - int32_t lastCursorCol; // cursor col at last repaint - // Cached packed palette (avoids packColor per repaint) - uint32_t packedPalette[16]; - bool paletteValid; - // Selection (line indices in scrollback+screen space) - int32_t selStartLine; - int32_t selStartCol; - int32_t selEndLine; - int32_t selEndCol; - bool selecting; - // Communications interface (all NULL = disconnected) - void *commCtx; - int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); - int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); - } ansiTerm; + AnsiTermDataT *ansiTerm; - struct { - const ListViewColT *cols; - int32_t colCount; - const char **cellData; - int32_t rowCount; - int32_t selectedIdx; - int32_t scrollPos; - int32_t scrollPosH; - int32_t sortCol; - ListViewSortE sortDir; - int32_t resolvedColW[LISTVIEW_MAX_COLS]; - int32_t totalColW; - int32_t *sortIndex; - bool multiSelect; - int32_t anchorIdx; - uint8_t *selBits; - bool reorderable; - int32_t dragIdx; - int32_t dropIdx; - void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); - } listView; + ListViewDataT *listView; struct { int32_t value; @@ -733,6 +742,9 @@ void wgtSetEnabled(WidgetT *w, bool enabled); // Show/hide a widget void wgtSetVisible(WidgetT *w, bool visible); +// Set a widget's name (for lookup via wgtFind) +void wgtSetName(WidgetT *w, const char *name); + // Find a widget by name (searches the subtree rooted at root) WidgetT *wgtFind(WidgetT *root, const char *name); @@ -766,7 +778,7 @@ static inline void wgtSetTooltip(WidgetT *w, const char *text) { w->tooltip = te // ============================================================ // Draw borders around layout containers in ugly colors -void wgtSetDebugLayout(bool enabled); +void wgtSetDebugLayout(struct AppContextT *ctx, bool enabled); // ============================================================ // Layout (called internally; available for manual trigger) diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index a0237f1..e766a45 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -64,6 +64,11 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { return; } + // Skip recomputation if positions are already up to date (Item 5) + if (!win->menuBar->positionsDirty) { + return; + } + int32_t x = CHROME_TOTAL_SIDE; for (int32_t i = 0; i < win->menuBar->menuCount; i++) { @@ -74,6 +79,8 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { menu->barW = labelW; x += labelW; } + + win->menuBar->positionsDirty = false; } @@ -284,20 +291,23 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW return; } - // Pre-compute source X lookup table (one division per column instead of per pixel) - int32_t srcXTab[ICON_SIZE]; + // Pre-compute source lookup tables using fixed-point reciprocals + // to replace per-entry division with multiply+shift (Item 1). + // Static buffers avoid per-call stack allocation (Item 9). + static int32_t srcXTab[ICON_SIZE]; + static int32_t srcYTab[ICON_SIZE]; int32_t visibleCols = colEnd - colStart; - - for (int32_t dx = 0; dx < visibleCols; dx++) { - 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; + uint32_t recipW = ((uint32_t)srcW << 16) / (uint32_t)dstW; + uint32_t recipH = ((uint32_t)srcH << 16) / (uint32_t)dstH; + + for (int32_t dx = 0; dx < visibleCols; dx++) { + srcXTab[dx] = (int32_t)(((uint32_t)(colStart + dx) * recipW) >> 16) * bpp; + } + for (int32_t dy = 0; dy < visibleRows; dy++) { - srcYTab[dy] = ((rowStart + dy) * srcH) / dstH; + srcYTab[dy] = (int32_t)(((uint32_t)(rowStart + dy) * recipH) >> 16); } // Blit with pre-computed lookups — no per-pixel divisions or clip checks @@ -600,6 +610,7 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) { menu->label[MAX_MENU_LABEL - 1] = '\0'; menu->accelKey = accelParse(label); bar->menuCount++; + bar->positionsDirty = true; return menu; } diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index 4cb5ce2..a047bfd 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -91,22 +91,22 @@ static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t // Copy a screen row into the scrollback circular buffer. static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { - if (!w->as.ansiTerm.scrollback || w->as.ansiTerm.scrollbackMax <= 0) { + if (!w->as.ansiTerm->scrollback || w->as.ansiTerm->scrollbackMax <= 0) { return; } - int32_t cols = w->as.ansiTerm.cols; + int32_t cols = w->as.ansiTerm->cols; int32_t bytesPerRow = cols * 2; - int32_t head = w->as.ansiTerm.scrollbackHead; + int32_t head = w->as.ansiTerm->scrollbackHead; - memcpy(w->as.ansiTerm.scrollback + head * bytesPerRow, - w->as.ansiTerm.cells + screenRow * bytesPerRow, + memcpy(w->as.ansiTerm->scrollback + head * bytesPerRow, + w->as.ansiTerm->cells + screenRow * bytesPerRow, bytesPerRow); - w->as.ansiTerm.scrollbackHead = (head + 1) % w->as.ansiTerm.scrollbackMax; + w->as.ansiTerm->scrollbackHead = (head + 1) % w->as.ansiTerm->scrollbackMax; - if (w->as.ansiTerm.scrollbackCount < w->as.ansiTerm.scrollbackMax) { - w->as.ansiTerm.scrollbackCount++; + if (w->as.ansiTerm->scrollbackCount < w->as.ansiTerm->scrollbackMax) { + w->as.ansiTerm->scrollbackCount++; } } @@ -120,15 +120,15 @@ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { // after a display format change). static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { - if (w->as.ansiTerm.paletteValid) { + if (w->as.ansiTerm->paletteValid) { return; } for (int32_t i = 0; i < 16; i++) { - w->as.ansiTerm.packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); + w->as.ansiTerm->packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); } - w->as.ansiTerm.paletteValid = true; + w->as.ansiTerm->paletteValid = true; } @@ -138,14 +138,14 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { static void ansiTermClearSelection(WidgetT *w) { if (ansiTermHasSelection(w)) { - 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; + 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; } @@ -164,7 +164,7 @@ static void ansiTermCopySelection(WidgetT *w) { int32_t eCol; ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); - int32_t cols = w->as.ansiTerm.cols; + int32_t cols = w->as.ansiTerm->cols; // Build text from selected cells (strip trailing spaces per line) char buf[4096]; @@ -210,12 +210,12 @@ static void ansiTermCopySelection(WidgetT *w) { // Mark rows dirty that are touched by a cell range [startCell, startCell+count). static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { - int32_t cols = w->as.ansiTerm.cols; + int32_t cols = w->as.ansiTerm->cols; int32_t startRow = startCell / cols; int32_t endRow = (startCell + count - 1) / cols; for (int32_t r = startRow; r <= endRow && r < 32; r++) { - w->as.ansiTerm.dirtyRows |= (1U << r); + w->as.ansiTerm->dirtyRows |= (1U << r); } } @@ -226,7 +226,7 @@ static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { static void ansiTermDirtyRow(WidgetT *w, int32_t row) { if (row >= 0 && row < 32) { - w->as.ansiTerm.dirtyRows |= (1U << row); + w->as.ansiTerm->dirtyRows |= (1U << row); } } @@ -236,9 +236,9 @@ static void ansiTermDirtyRow(WidgetT *w, int32_t row) { // ============================================================ static void ansiTermDeleteLines(WidgetT *w, int32_t count) { - int32_t cols = w->as.ansiTerm.cols; - int32_t bot = w->as.ansiTerm.scrollBot; - int32_t row = w->as.ansiTerm.cursorRow; + int32_t cols = w->as.ansiTerm->cols; + int32_t bot = w->as.ansiTerm->scrollBot; + int32_t row = w->as.ansiTerm->cursorRow; if (count > bot - row + 1) { count = bot - row + 1; @@ -248,7 +248,7 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) { return; } - uint8_t *cells = w->as.ansiTerm.cells; + uint8_t *cells = w->as.ansiTerm->cells; int32_t bytesPerRow = cols * 2; // Shift lines up within region @@ -275,40 +275,40 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) { // ============================================================ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { - int32_t *p = w->as.ansiTerm.params; - int32_t n = w->as.ansiTerm.paramCount; + int32_t *p = w->as.ansiTerm->params; + int32_t n = w->as.ansiTerm->paramCount; // DEC private modes (ESC[?...) - if (w->as.ansiTerm.csiPrivate) { + if (w->as.ansiTerm->csiPrivate) { int32_t mode = (n >= 1) ? p[0] : 0; if (cmd == 'h') { if (mode == 6) { - w->as.ansiTerm.originMode = true; - w->as.ansiTerm.cursorRow = w->as.ansiTerm.scrollTop; - w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm->originMode = true; + w->as.ansiTerm->cursorRow = w->as.ansiTerm->scrollTop; + w->as.ansiTerm->cursorCol = 0; } if (mode == 7) { - w->as.ansiTerm.wrapMode = true; + w->as.ansiTerm->wrapMode = true; } if (mode == 25) { - w->as.ansiTerm.cursorVisible = true; + w->as.ansiTerm->cursorVisible = true; } } else if (cmd == 'l') { if (mode == 6) { - w->as.ansiTerm.originMode = false; - w->as.ansiTerm.cursorRow = 0; - w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm->originMode = false; + w->as.ansiTerm->cursorRow = 0; + w->as.ansiTerm->cursorCol = 0; } if (mode == 7) { - w->as.ansiTerm.wrapMode = false; + w->as.ansiTerm->wrapMode = false; } if (mode == 25) { - w->as.ansiTerm.cursorVisible = false; + w->as.ansiTerm->cursorVisible = false; } } @@ -319,21 +319,21 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case '@': // ICH - insert character { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t cols = w->as.ansiTerm.cols; - int32_t row = w->as.ansiTerm.cursorRow; - int32_t col = w->as.ansiTerm.cursorCol; + int32_t cols = w->as.ansiTerm->cols; + int32_t row = w->as.ansiTerm->cursorRow; + int32_t col = w->as.ansiTerm->cursorCol; if (count > cols - col) { count = cols - col; } if (count > 0) { - uint8_t *base = w->as.ansiTerm.cells + row * cols * 2; + uint8_t *base = w->as.ansiTerm->cells + row * cols * 2; memmove(base + (col + count) * 2, base + col * 2, (cols - col - count) * 2); for (int32_t i = col; i < col + count; i++) { base[i * 2] = ' '; - base[i * 2 + 1] = w->as.ansiTerm.curAttr; + base[i * 2 + 1] = w->as.ansiTerm->curAttr; } ansiTermDirtyRow(w, row); @@ -345,11 +345,11 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'A': // CUU - cursor up { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t minRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollTop : 0; - w->as.ansiTerm.cursorRow -= count; + int32_t minRow = w->as.ansiTerm->originMode ? w->as.ansiTerm->scrollTop : 0; + w->as.ansiTerm->cursorRow -= count; - if (w->as.ansiTerm.cursorRow < minRow) { - w->as.ansiTerm.cursorRow = minRow; + if (w->as.ansiTerm->cursorRow < minRow) { + w->as.ansiTerm->cursorRow = minRow; } break; @@ -358,11 +358,11 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'B': // CUD - cursor down { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t maxRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollBot : w->as.ansiTerm.rows - 1; - w->as.ansiTerm.cursorRow += count; + int32_t maxRow = w->as.ansiTerm->originMode ? w->as.ansiTerm->scrollBot : w->as.ansiTerm->rows - 1; + w->as.ansiTerm->cursorRow += count; - if (w->as.ansiTerm.cursorRow > maxRow) { - w->as.ansiTerm.cursorRow = maxRow; + if (w->as.ansiTerm->cursorRow > maxRow) { + w->as.ansiTerm->cursorRow = maxRow; } break; @@ -371,10 +371,10 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'C': // CUF - cursor forward { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - w->as.ansiTerm.cursorCol += count; + w->as.ansiTerm->cursorCol += count; - if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) { - w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1; + if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) { + w->as.ansiTerm->cursorCol = w->as.ansiTerm->cols - 1; } break; @@ -383,10 +383,10 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'D': // CUB - cursor back { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - w->as.ansiTerm.cursorCol -= count; + w->as.ansiTerm->cursorCol -= count; - if (w->as.ansiTerm.cursorCol < 0) { - w->as.ansiTerm.cursorCol = 0; + if (w->as.ansiTerm->cursorCol < 0) { + w->as.ansiTerm->cursorCol = 0; } break; @@ -394,10 +394,10 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'c': // DA - device attributes { - if (w->as.ansiTerm.commWrite) { + if (w->as.ansiTerm->commWrite) { // Respond as VT100 with advanced video option const uint8_t reply[] = "\033[?1;2c"; - w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, reply, 7); + w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, reply, 7); } break; @@ -411,7 +411,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { ansiTermNewline(w); } - w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm->cursorCol = 0; break; } @@ -422,11 +422,11 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { int32_t col = (n >= 2 && p[1]) ? p[1] - 1 : 0; // Origin mode: row is relative to scroll region - if (w->as.ansiTerm.originMode) { - row += w->as.ansiTerm.scrollTop; + if (w->as.ansiTerm->originMode) { + row += w->as.ansiTerm->scrollTop; - if (row > w->as.ansiTerm.scrollBot) { - row = w->as.ansiTerm.scrollBot; + if (row > w->as.ansiTerm->scrollBot) { + row = w->as.ansiTerm->scrollBot; } } @@ -434,20 +434,20 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { row = 0; } - if (row >= w->as.ansiTerm.rows) { - row = w->as.ansiTerm.rows - 1; + if (row >= w->as.ansiTerm->rows) { + row = w->as.ansiTerm->rows - 1; } if (col < 0) { col = 0; } - if (col >= w->as.ansiTerm.cols) { - col = w->as.ansiTerm.cols - 1; + if (col >= w->as.ansiTerm->cols) { + col = w->as.ansiTerm->cols - 1; } - w->as.ansiTerm.cursorRow = row; - w->as.ansiTerm.cursorCol = col; + w->as.ansiTerm->cursorRow = row; + w->as.ansiTerm->cursorCol = col; break; } @@ -482,21 +482,21 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'P': // DCH - delete character { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t cols = w->as.ansiTerm.cols; - int32_t row = w->as.ansiTerm.cursorRow; - int32_t col = w->as.ansiTerm.cursorCol; + int32_t cols = w->as.ansiTerm->cols; + int32_t row = w->as.ansiTerm->cursorRow; + int32_t col = w->as.ansiTerm->cursorCol; if (count > cols - col) { count = cols - col; } if (count > 0) { - uint8_t *base = w->as.ansiTerm.cells + row * cols * 2; + uint8_t *base = w->as.ansiTerm->cells + row * cols * 2; memmove(base + col * 2, base + (col + count) * 2, (cols - col - count) * 2); for (int32_t i = cols - count; i < cols; i++) { base[i * 2] = ' '; - base[i * 2 + 1] = w->as.ansiTerm.curAttr; + base[i * 2 + 1] = w->as.ansiTerm->curAttr; } ansiTermDirtyRow(w, row); @@ -529,13 +529,13 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'Z': // CBT - back tab { - int32_t col = w->as.ansiTerm.cursorCol; + int32_t col = w->as.ansiTerm->cursorCol; if (col > 0) { col = ((col - 1) / 8) * 8; } - w->as.ansiTerm.cursorCol = col; + w->as.ansiTerm->cursorCol = col; break; } @@ -547,17 +547,17 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { { int32_t mode = (n >= 1) ? p[0] : 0; - if (w->as.ansiTerm.commWrite) { + if (w->as.ansiTerm->commWrite) { if (mode == 6) { // CPR — cursor position report: ESC[row;colR (1-based) char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(w->as.ansiTerm.cursorRow + 1), (long)(w->as.ansiTerm.cursorCol + 1)); - w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, (const uint8_t *)reply, len); + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(w->as.ansiTerm->cursorRow + 1), (long)(w->as.ansiTerm->cursorCol + 1)); + w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len); } else if (mode == 255) { // Screen size report char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm.rows, (long)w->as.ansiTerm.cols); - w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, (const uint8_t *)reply, len); + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm->rows, (long)w->as.ansiTerm->cols); + w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len); } } @@ -566,7 +566,7 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { case 'r': // DECSTBM - set scrolling region { - int32_t rows = w->as.ansiTerm.rows; + int32_t rows = w->as.ansiTerm->rows; int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t bot = (n >= 2 && p[1]) ? p[1] - 1 : rows - 1; @@ -579,28 +579,28 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { } if (top < bot) { - w->as.ansiTerm.scrollTop = top; - w->as.ansiTerm.scrollBot = bot; + w->as.ansiTerm->scrollTop = top; + w->as.ansiTerm->scrollBot = bot; } else { // Invalid or reset — restore full screen - w->as.ansiTerm.scrollTop = 0; - w->as.ansiTerm.scrollBot = rows - 1; + w->as.ansiTerm->scrollTop = 0; + w->as.ansiTerm->scrollBot = rows - 1; } // Home cursor (relative to region if origin mode) - w->as.ansiTerm.cursorRow = w->as.ansiTerm.originMode ? w->as.ansiTerm.scrollTop : 0; - w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm->cursorRow = w->as.ansiTerm->originMode ? w->as.ansiTerm->scrollTop : 0; + w->as.ansiTerm->cursorCol = 0; break; } case 's': // SCP - save cursor position - w->as.ansiTerm.savedRow = w->as.ansiTerm.cursorRow; - w->as.ansiTerm.savedCol = w->as.ansiTerm.cursorCol; + w->as.ansiTerm->savedRow = w->as.ansiTerm->cursorRow; + w->as.ansiTerm->savedCol = w->as.ansiTerm->cursorCol; break; case 'u': // RCP - restore cursor position - w->as.ansiTerm.cursorRow = w->as.ansiTerm.savedRow; - w->as.ansiTerm.cursorCol = w->as.ansiTerm.savedCol; + w->as.ansiTerm->cursorRow = w->as.ansiTerm->savedRow; + w->as.ansiTerm->cursorCol = w->as.ansiTerm->savedCol; break; default: @@ -614,9 +614,9 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { // ============================================================ static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { - int32_t cols = w->as.ansiTerm.cols; - int32_t rows = w->as.ansiTerm.rows; - int32_t cur = w->as.ansiTerm.cursorRow * cols + w->as.ansiTerm.cursorCol; + int32_t cols = w->as.ansiTerm->cols; + int32_t rows = w->as.ansiTerm->rows; + int32_t cur = w->as.ansiTerm->cursorRow * cols + w->as.ansiTerm->cursorCol; if (mode == 0) { // Erase from cursor to end of screen @@ -626,14 +626,14 @@ static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { ansiTermFillCells(w, 0, cur + 1); } else if (mode == 2) { // Erase entire screen — push all lines to scrollback first - bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } if (wasAtBottom) { - w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; + w->as.ansiTerm->scrollPos = w->as.ansiTerm->scrollbackCount; } ansiTermFillCells(w, 0, cols * rows); @@ -646,9 +646,9 @@ static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { // ============================================================ static void ansiTermEraseLine(WidgetT *w, int32_t mode) { - int32_t cols = w->as.ansiTerm.cols; - int32_t row = w->as.ansiTerm.cursorRow; - int32_t col = w->as.ansiTerm.cursorCol; + int32_t cols = w->as.ansiTerm->cols; + int32_t row = w->as.ansiTerm->cursorRow; + int32_t col = w->as.ansiTerm->cursorCol; int32_t base = row * cols; if (mode == 0) { @@ -671,9 +671,9 @@ static void ansiTermEraseLine(WidgetT *w, int32_t mode) { // Fill a range of cells with space + current attribute. static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { - uint8_t *cells = w->as.ansiTerm.cells; - uint8_t attr = w->as.ansiTerm.curAttr; - int32_t total = w->as.ansiTerm.cols * w->as.ansiTerm.rows; + uint8_t *cells = w->as.ansiTerm->cells; + uint8_t attr = w->as.ansiTerm->curAttr; + int32_t total = w->as.ansiTerm->cols * w->as.ansiTerm->rows; if (start < 0) { count += start; @@ -703,20 +703,20 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { // lineIndex >= scrollbackCount → screen line static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { - int32_t cols = w->as.ansiTerm.cols; + int32_t cols = w->as.ansiTerm->cols; int32_t bytesPerRow = cols * 2; - int32_t sbCount = w->as.ansiTerm.scrollbackCount; + int32_t sbCount = w->as.ansiTerm->scrollbackCount; if (lineIndex < sbCount) { // Scrollback line (circular buffer) - int32_t sbMax = w->as.ansiTerm.scrollbackMax; - int32_t actual = (w->as.ansiTerm.scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; - return w->as.ansiTerm.scrollback + actual * bytesPerRow; + int32_t sbMax = w->as.ansiTerm->scrollbackMax; + int32_t actual = (w->as.ansiTerm->scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; + return w->as.ansiTerm->scrollback + actual * bytesPerRow; } // Screen line int32_t screenRow = lineIndex - sbCount; - return w->as.ansiTerm.cells + screenRow * bytesPerRow; + return w->as.ansiTerm->cells + screenRow * bytesPerRow; } @@ -725,12 +725,12 @@ static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { // ============================================================ static bool ansiTermHasSelection(const WidgetT *w) { - if (w->as.ansiTerm.selStartLine < 0) { + if (w->as.ansiTerm->selStartLine < 0) { return false; } - if (w->as.ansiTerm.selStartLine == w->as.ansiTerm.selEndLine && - w->as.ansiTerm.selStartCol == w->as.ansiTerm.selEndCol) { + if (w->as.ansiTerm->selStartLine == w->as.ansiTerm->selEndLine && + w->as.ansiTerm->selStartCol == w->as.ansiTerm->selEndCol) { return false; } @@ -743,9 +743,9 @@ static bool ansiTermHasSelection(const WidgetT *w) { // ============================================================ static void ansiTermInsertLines(WidgetT *w, int32_t count) { - int32_t cols = w->as.ansiTerm.cols; - int32_t bot = w->as.ansiTerm.scrollBot; - int32_t row = w->as.ansiTerm.cursorRow; + int32_t cols = w->as.ansiTerm->cols; + int32_t bot = w->as.ansiTerm->scrollBot; + int32_t row = w->as.ansiTerm->cursorRow; if (count > bot - row + 1) { count = bot - row + 1; @@ -755,7 +755,7 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) { return; } - uint8_t *cells = w->as.ansiTerm.cells; + uint8_t *cells = w->as.ansiTerm->cells; int32_t bytesPerRow = cols * 2; // Shift lines down within region @@ -784,12 +784,12 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) { // Move cursor to next line, scrolling if at the bottom. static void ansiTermNewline(WidgetT *w) { - int32_t bot = w->as.ansiTerm.scrollBot; + int32_t bot = w->as.ansiTerm->scrollBot; - if (w->as.ansiTerm.cursorRow == bot) { + if (w->as.ansiTerm->cursorRow == bot) { ansiTermScrollUp(w); - } else if (w->as.ansiTerm.cursorRow < w->as.ansiTerm.rows - 1) { - w->as.ansiTerm.cursorRow++; + } else if (w->as.ansiTerm->cursorRow < w->as.ansiTerm->rows - 1) { + w->as.ansiTerm->cursorRow++; } } @@ -801,29 +801,29 @@ static void ansiTermNewline(WidgetT *w) { // Feed one byte through the ANSI parser state machine. static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { - switch (w->as.ansiTerm.parseState) { + switch (w->as.ansiTerm->parseState) { case PARSE_NORMAL: if (ch == 0x1B) { - w->as.ansiTerm.parseState = PARSE_ESC; + w->as.ansiTerm->parseState = PARSE_ESC; } else if (ch == '\r') { - w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm->cursorCol = 0; } else if (ch == '\n') { ansiTermNewline(w); } else if (ch == '\b') { - if (w->as.ansiTerm.cursorCol > 0) { - w->as.ansiTerm.cursorCol--; + if (w->as.ansiTerm->cursorCol > 0) { + w->as.ansiTerm->cursorCol--; } } else if (ch == '\t') { - w->as.ansiTerm.cursorCol = (w->as.ansiTerm.cursorCol + 8) & ~7; + w->as.ansiTerm->cursorCol = (w->as.ansiTerm->cursorCol + 8) & ~7; - if (w->as.ansiTerm.cursorCol >= w->as.ansiTerm.cols) { - w->as.ansiTerm.cursorCol = w->as.ansiTerm.cols - 1; + if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) { + w->as.ansiTerm->cursorCol = w->as.ansiTerm->cols - 1; } } else if (ch == '\f') { // Form feed — clear screen and home cursor ansiTermEraseDisplay(w, 2); - w->as.ansiTerm.cursorRow = 0; - w->as.ansiTerm.cursorCol = 0; + w->as.ansiTerm->cursorRow = 0; + w->as.ansiTerm->cursorCol = 0; } else if (ch == '\a') { // Bell — ignored } else { @@ -835,61 +835,61 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { case PARSE_ESC: if (ch == '[') { - w->as.ansiTerm.parseState = PARSE_CSI; - w->as.ansiTerm.paramCount = 0; - w->as.ansiTerm.csiPrivate = false; - memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params)); + w->as.ansiTerm->parseState = PARSE_CSI; + w->as.ansiTerm->paramCount = 0; + w->as.ansiTerm->csiPrivate = false; + memset(w->as.ansiTerm->params, 0, sizeof(w->as.ansiTerm->params)); } else if (ch == 'D') { // IND — scroll up one line ansiTermScrollUp(w); - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->parseState = PARSE_NORMAL; } else if (ch == 'M') { // RI — scroll down one line ansiTermScrollDown(w); - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->parseState = PARSE_NORMAL; } else if (ch == 'c') { // RIS — terminal reset - w->as.ansiTerm.cursorRow = 0; - w->as.ansiTerm.cursorCol = 0; - w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm.bold = false; - w->as.ansiTerm.wrapMode = true; - w->as.ansiTerm.originMode = false; - w->as.ansiTerm.cursorVisible = true; - w->as.ansiTerm.scrollTop = 0; - w->as.ansiTerm.scrollBot = w->as.ansiTerm.rows - 1; + w->as.ansiTerm->cursorRow = 0; + w->as.ansiTerm->cursorCol = 0; + w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm->bold = false; + w->as.ansiTerm->wrapMode = true; + w->as.ansiTerm->originMode = false; + w->as.ansiTerm->cursorVisible = true; + w->as.ansiTerm->scrollTop = 0; + w->as.ansiTerm->scrollBot = w->as.ansiTerm->rows - 1; ansiTermEraseDisplay(w, 2); - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->parseState = PARSE_NORMAL; } else { // Unknown escape — return to normal - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->parseState = PARSE_NORMAL; } break; case PARSE_CSI: if (ch == '?') { - w->as.ansiTerm.csiPrivate = true; + w->as.ansiTerm->csiPrivate = true; } else if (ch >= '0' && ch <= '9') { - if (w->as.ansiTerm.paramCount == 0) { - w->as.ansiTerm.paramCount = 1; + if (w->as.ansiTerm->paramCount == 0) { + w->as.ansiTerm->paramCount = 1; } - int32_t idx = w->as.ansiTerm.paramCount - 1; + int32_t idx = w->as.ansiTerm->paramCount - 1; if (idx < ANSI_MAX_PARAMS) { - w->as.ansiTerm.params[idx] = w->as.ansiTerm.params[idx] * 10 + (ch - '0'); + w->as.ansiTerm->params[idx] = w->as.ansiTerm->params[idx] * 10 + (ch - '0'); } } else if (ch == ';') { - if (w->as.ansiTerm.paramCount < ANSI_MAX_PARAMS) { - w->as.ansiTerm.paramCount++; + if (w->as.ansiTerm->paramCount < ANSI_MAX_PARAMS) { + w->as.ansiTerm->paramCount++; } } else if (ch >= 0x40 && ch <= 0x7E) { // Final byte — dispatch the CSI sequence ansiTermDispatchCsi(w, ch); - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->parseState = PARSE_NORMAL; } else { // Unexpected byte — abort sequence - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->parseState = PARSE_NORMAL; } break; } @@ -903,24 +903,24 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { // Handle SGR (Select Graphic Rendition) escape sequence. static void ansiTermProcessSgr(WidgetT *w) { - if (w->as.ansiTerm.paramCount == 0) { + if (w->as.ansiTerm->paramCount == 0) { // ESC[m with no params = reset - w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm.bold = false; + w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm->bold = false; return; } - for (int32_t i = 0; i < w->as.ansiTerm.paramCount; i++) { - int32_t code = w->as.ansiTerm.params[i]; - uint8_t fg = w->as.ansiTerm.curAttr & 0x0F; - uint8_t bg = (w->as.ansiTerm.curAttr >> 4) & 0x0F; + for (int32_t i = 0; i < w->as.ansiTerm->paramCount; i++) { + int32_t code = w->as.ansiTerm->params[i]; + uint8_t fg = w->as.ansiTerm->curAttr & 0x0F; + uint8_t bg = (w->as.ansiTerm->curAttr >> 4) & 0x0F; if (code == 0) { fg = 7; bg = 0; - w->as.ansiTerm.bold = false; + w->as.ansiTerm->bold = false; } else if (code == 1) { - w->as.ansiTerm.bold = true; + w->as.ansiTerm->bold = true; fg |= 8; } else if (code == 5) { // Blink — sets bit 7 of attr byte via bg bit 3 @@ -938,12 +938,12 @@ static void ansiTermProcessSgr(WidgetT *w) { fg = bg & 0x07; } else if (code == 22) { // Normal intensity - w->as.ansiTerm.bold = false; + w->as.ansiTerm->bold = false; fg &= 7; } else if (code >= 30 && code <= 37) { fg = (uint8_t)sAnsiToCga[code - 30]; - if (w->as.ansiTerm.bold) { + if (w->as.ansiTerm->bold) { fg |= 8; } } else if (code >= 40 && code <= 47) { @@ -956,7 +956,7 @@ static void ansiTermProcessSgr(WidgetT *w) { bg = (uint8_t)(sAnsiToCga[code - 100] | 8); } - w->as.ansiTerm.curAttr = (uint8_t)((bg << 4) | fg); + w->as.ansiTerm->curAttr = (uint8_t)((bg << 4) | fg); } } @@ -966,7 +966,7 @@ static void ansiTermProcessSgr(WidgetT *w) { // ============================================================ static void ansiTermPasteToComm(WidgetT *w) { - if (!w->as.ansiTerm.commWrite) { + if (!w->as.ansiTerm->commWrite) { return; } @@ -985,7 +985,7 @@ static void ansiTermPasteToComm(WidgetT *w) { ch = '\r'; } - w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, &ch, 1); + w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, &ch, 1); } } @@ -997,26 +997,26 @@ static void ansiTermPasteToComm(WidgetT *w) { // Place a character at the cursor and advance. static void ansiTermPutChar(WidgetT *w, uint8_t ch) { - int32_t cols = w->as.ansiTerm.cols; - int32_t rows = w->as.ansiTerm.rows; - int32_t row = w->as.ansiTerm.cursorRow; - int32_t col = w->as.ansiTerm.cursorCol; + int32_t cols = w->as.ansiTerm->cols; + int32_t rows = w->as.ansiTerm->rows; + int32_t row = w->as.ansiTerm->cursorRow; + int32_t col = w->as.ansiTerm->cursorCol; if (row >= 0 && row < rows && col >= 0 && col < cols) { int32_t idx = (row * cols + col) * 2; - w->as.ansiTerm.cells[idx] = ch; - w->as.ansiTerm.cells[idx + 1] = w->as.ansiTerm.curAttr; + w->as.ansiTerm->cells[idx] = ch; + w->as.ansiTerm->cells[idx + 1] = w->as.ansiTerm->curAttr; ansiTermDirtyRow(w, row); } - w->as.ansiTerm.cursorCol++; + w->as.ansiTerm->cursorCol++; - if (w->as.ansiTerm.cursorCol >= cols) { - if (w->as.ansiTerm.wrapMode) { - w->as.ansiTerm.cursorCol = 0; + if (w->as.ansiTerm->cursorCol >= cols) { + if (w->as.ansiTerm->wrapMode) { + w->as.ansiTerm->cursorCol = 0; ansiTermNewline(w); } else { - w->as.ansiTerm.cursorCol = cols - 1; + w->as.ansiTerm->cursorCol = cols - 1; } } } @@ -1027,10 +1027,10 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) { // ============================================================ static void ansiTermScrollDown(WidgetT *w) { - int32_t cols = w->as.ansiTerm.cols; - int32_t top = w->as.ansiTerm.scrollTop; - int32_t bot = w->as.ansiTerm.scrollBot; - uint8_t *cells = w->as.ansiTerm.cells; + int32_t cols = w->as.ansiTerm->cols; + int32_t top = w->as.ansiTerm->scrollTop; + int32_t bot = w->as.ansiTerm->scrollBot; + uint8_t *cells = w->as.ansiTerm->cells; int32_t bytesPerRow = cols * 2; // Shift lines within region down by one @@ -1045,7 +1045,7 @@ static void ansiTermScrollDown(WidgetT *w) { // Dirty affected rows for (int32_t r = top; r <= bot && r < 32; r++) { - w->as.ansiTerm.dirtyRows |= (1U << r); + w->as.ansiTerm->dirtyRows |= (1U << r); } } @@ -1058,19 +1058,19 @@ static void ansiTermScrollDown(WidgetT *w) { // the scrollback buffer before being discarded. static void ansiTermScrollUp(WidgetT *w) { - int32_t cols = w->as.ansiTerm.cols; - int32_t top = w->as.ansiTerm.scrollTop; - int32_t bot = w->as.ansiTerm.scrollBot; - uint8_t *cells = w->as.ansiTerm.cells; + int32_t cols = w->as.ansiTerm->cols; + int32_t top = w->as.ansiTerm->scrollTop; + int32_t bot = w->as.ansiTerm->scrollBot; + uint8_t *cells = w->as.ansiTerm->cells; int32_t bytesPerRow = cols * 2; // Only push to scrollback when scrolling the full screen - if (top == 0 && bot == w->as.ansiTerm.rows - 1) { - bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + if (top == 0 && bot == w->as.ansiTerm->rows - 1) { + bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); ansiTermAddToScrollback(w, 0); if (wasAtBottom) { - w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; + w->as.ansiTerm->scrollPos = w->as.ansiTerm->scrollbackCount; } } @@ -1086,7 +1086,7 @@ static void ansiTermScrollUp(WidgetT *w) { // Dirty affected rows for (int32_t r = top; r <= bot && r < 32; r++) { - w->as.ansiTerm.dirtyRows |= (1U << r); + w->as.ansiTerm->dirtyRows |= (1U << r); } } @@ -1098,10 +1098,10 @@ static void ansiTermScrollUp(WidgetT *w) { // Return selection start/end in normalized order (start <= end). static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol) { - int32_t sl = w->as.ansiTerm.selStartLine; - int32_t sc = w->as.ansiTerm.selStartCol; - int32_t el = w->as.ansiTerm.selEndLine; - int32_t ec = w->as.ansiTerm.selEndCol; + int32_t sl = w->as.ansiTerm->selStartLine; + int32_t sc = w->as.ansiTerm->selStartCol; + int32_t el = w->as.ansiTerm->selEndLine; + int32_t ec = w->as.ansiTerm->selEndCol; if (sl > el || (sl == el && sc > ec)) { *startLine = el; @@ -1140,66 +1140,75 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { return NULL; } - int32_t cellCount = cols * rows; - w->as.ansiTerm.cells = (uint8_t *)malloc(cellCount * 2); + w->as.ansiTerm = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT)); - if (!w->as.ansiTerm.cells) { + if (!w->as.ansiTerm) { + free(w); + return NULL; + } + + int32_t cellCount = cols * rows; + w->as.ansiTerm->cells = (uint8_t *)malloc(cellCount * 2); + + if (!w->as.ansiTerm->cells) { + free(w->as.ansiTerm); free(w); return NULL; } // Allocate scrollback buffer int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; - w->as.ansiTerm.scrollback = (uint8_t *)malloc(sbMax * cols * 2); + w->as.ansiTerm->scrollback = (uint8_t *)malloc(sbMax * cols * 2); - if (!w->as.ansiTerm.scrollback) { - free(w->as.ansiTerm.cells); + if (!w->as.ansiTerm->scrollback) { + free(w->as.ansiTerm->cells); + free(w->as.ansiTerm); free(w); return NULL; } - w->as.ansiTerm.cols = cols; - w->as.ansiTerm.rows = rows; - w->as.ansiTerm.cursorRow = 0; - w->as.ansiTerm.cursorCol = 0; - w->as.ansiTerm.cursorVisible = true; - w->as.ansiTerm.wrapMode = true; - w->as.ansiTerm.bold = false; - w->as.ansiTerm.originMode = false; - w->as.ansiTerm.csiPrivate = false; - w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm.parseState = PARSE_NORMAL; - w->as.ansiTerm.paramCount = 0; - w->as.ansiTerm.savedRow = 0; - w->as.ansiTerm.savedCol = 0; - w->as.ansiTerm.scrollTop = 0; - w->as.ansiTerm.scrollBot = rows - 1; - w->as.ansiTerm.scrollbackMax = sbMax; - w->as.ansiTerm.scrollbackCount = 0; - w->as.ansiTerm.scrollbackHead = 0; - w->as.ansiTerm.scrollPos = 0; - w->as.ansiTerm.commCtx = NULL; - w->as.ansiTerm.commRead = NULL; - w->as.ansiTerm.commWrite = NULL; - 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; - w->as.ansiTerm.blinkVisible = true; - w->as.ansiTerm.blinkTime = clock(); - w->as.ansiTerm.cursorOn = true; - w->as.ansiTerm.cursorTime = clock(); - w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; - w->as.ansiTerm.lastCursorRow = -1; - w->as.ansiTerm.lastCursorCol = -1; + w->as.ansiTerm->cols = cols; + w->as.ansiTerm->rows = rows; + w->as.ansiTerm->cursorRow = 0; + w->as.ansiTerm->cursorCol = 0; + w->as.ansiTerm->cursorVisible = true; + w->as.ansiTerm->wrapMode = true; + w->as.ansiTerm->bold = false; + w->as.ansiTerm->originMode = false; + w->as.ansiTerm->csiPrivate = false; + w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm->parseState = PARSE_NORMAL; + w->as.ansiTerm->paramCount = 0; + w->as.ansiTerm->savedRow = 0; + w->as.ansiTerm->savedCol = 0; + w->as.ansiTerm->scrollTop = 0; + w->as.ansiTerm->scrollBot = rows - 1; + w->as.ansiTerm->scrollbackMax = sbMax; + w->as.ansiTerm->scrollbackCount = 0; + w->as.ansiTerm->scrollbackHead = 0; + w->as.ansiTerm->scrollPos = 0; + w->as.ansiTerm->commCtx = NULL; + w->as.ansiTerm->commRead = NULL; + w->as.ansiTerm->commWrite = NULL; + 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; + w->as.ansiTerm->blinkVisible = true; + w->as.ansiTerm->blinkTime = clock(); + w->as.ansiTerm->cursorOn = true; + w->as.ansiTerm->cursorTime = clock(); + w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; + w->as.ansiTerm->lastCursorRow = -1; + w->as.ansiTerm->lastCursorCol = -1; - memset(w->as.ansiTerm.params, 0, sizeof(w->as.ansiTerm.params)); + memset(w->as.ansiTerm->params, 0, sizeof(w->as.ansiTerm->params)); // Initialize all cells to space with default attribute for (int32_t i = 0; i < cellCount; i++) { - w->as.ansiTerm.cells[i * 2] = ' '; - w->as.ansiTerm.cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; + w->as.ansiTerm->cells[i * 2] = ' '; + w->as.ansiTerm->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } return w; @@ -1211,37 +1220,35 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { // ============================================================ void wgtAnsiTermClear(WidgetT *w) { - if (!w || w->type != WidgetAnsiTermE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetAnsiTermE); - int32_t rows = w->as.ansiTerm.rows; - int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm->rows; + int32_t cols = w->as.ansiTerm->cols; // Push all visible lines to scrollback - bool wasAtBottom = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } if (wasAtBottom) { - w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount; + w->as.ansiTerm->scrollPos = w->as.ansiTerm->scrollbackCount; } // Clear the screen int32_t cellCount = cols * rows; for (int32_t i = 0; i < cellCount; i++) { - w->as.ansiTerm.cells[i * 2] = ' '; - w->as.ansiTerm.cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; + w->as.ansiTerm->cells[i * 2] = ' '; + w->as.ansiTerm->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } - w->as.ansiTerm.cursorRow = 0; - w->as.ansiTerm.cursorCol = 0; - w->as.ansiTerm.curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm.bold = false; - w->as.ansiTerm.parseState = PARSE_NORMAL; + w->as.ansiTerm->cursorRow = 0; + w->as.ansiTerm->cursorCol = 0; + w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; + w->as.ansiTerm->bold = false; + w->as.ansiTerm->parseState = PARSE_NORMAL; } @@ -1254,19 +1261,19 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { return; } - int32_t cols = w->as.ansiTerm.cols; + int32_t cols = w->as.ansiTerm->cols; uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); if (!newBuf) { return; } - free(w->as.ansiTerm.scrollback); - w->as.ansiTerm.scrollback = newBuf; - w->as.ansiTerm.scrollbackMax = maxLines; - w->as.ansiTerm.scrollbackCount = 0; - w->as.ansiTerm.scrollbackHead = 0; - w->as.ansiTerm.scrollPos = 0; + free(w->as.ansiTerm->scrollback); + w->as.ansiTerm->scrollback = newBuf; + w->as.ansiTerm->scrollbackMax = maxLines; + w->as.ansiTerm->scrollbackCount = 0; + w->as.ansiTerm->scrollbackHead = 0; + w->as.ansiTerm->scrollPos = 0; } @@ -1275,29 +1282,27 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { // ============================================================ int32_t wgtAnsiTermPoll(WidgetT *w) { - if (!w || w->type != WidgetAnsiTermE) { - return 0; - } + VALIDATE_WIDGET(w, WidgetAnsiTermE, 0); // Text blink timer — toggle visibility and dirty rows with blinking cells clock_t now = clock(); clock_t blinkInterval = (clock_t)BLINK_MS * CLOCKS_PER_SEC / 1000; clock_t curInterval = (clock_t)CURSOR_MS * CLOCKS_PER_SEC / 1000; - if ((now - w->as.ansiTerm.blinkTime) >= blinkInterval) { - w->as.ansiTerm.blinkTime = now; - w->as.ansiTerm.blinkVisible = !w->as.ansiTerm.blinkVisible; + if ((now - w->as.ansiTerm->blinkTime) >= blinkInterval) { + w->as.ansiTerm->blinkTime = now; + w->as.ansiTerm->blinkVisible = !w->as.ansiTerm->blinkVisible; // Dirty any rows that contain cells with blink attribute - int32_t cols = w->as.ansiTerm.cols; - int32_t rows = w->as.ansiTerm.rows; + int32_t cols = w->as.ansiTerm->cols; + int32_t rows = w->as.ansiTerm->rows; for (int32_t row = 0; row < rows && row < 32; row++) { for (int32_t col = 0; col < cols; col++) { - uint8_t attr = w->as.ansiTerm.cells[(row * cols + col) * 2 + 1]; + uint8_t attr = w->as.ansiTerm->cells[(row * cols + col) * 2 + 1]; if (attr & ATTR_BLINK_BIT) { - w->as.ansiTerm.dirtyRows |= (1U << row); + w->as.ansiTerm->dirtyRows |= (1U << row); break; } } @@ -1305,24 +1310,24 @@ int32_t wgtAnsiTermPoll(WidgetT *w) { } // Cursor blink timer - if ((now - w->as.ansiTerm.cursorTime) >= curInterval) { - w->as.ansiTerm.cursorTime = now; - w->as.ansiTerm.cursorOn = !w->as.ansiTerm.cursorOn; + if ((now - w->as.ansiTerm->cursorTime) >= curInterval) { + w->as.ansiTerm->cursorTime = now; + w->as.ansiTerm->cursorOn = !w->as.ansiTerm->cursorOn; - int32_t cRow = w->as.ansiTerm.cursorRow; + int32_t cRow = w->as.ansiTerm->cursorRow; if (cRow >= 0 && cRow < 32) { - w->as.ansiTerm.dirtyRows |= (1U << cRow); + w->as.ansiTerm->dirtyRows |= (1U << cRow); } } // Read from comm - if (!w->as.ansiTerm.commRead) { + if (!w->as.ansiTerm->commRead) { return 0; } uint8_t buf[256]; - int32_t n = w->as.ansiTerm.commRead(w->as.ansiTerm.commCtx, buf, (int32_t)sizeof(buf)); + int32_t n = w->as.ansiTerm->commRead(w->as.ansiTerm->commCtx, buf, (int32_t)sizeof(buf)); if (n > 0) { wgtAnsiTermWrite(w, buf, n); @@ -1348,21 +1353,21 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { // Always repaint the row the cursor was on last time (to erase it) // and the row it's on now (to draw it) - int32_t prevRow = w->as.ansiTerm.lastCursorRow; - int32_t curRow = w->as.ansiTerm.cursorRow; - int32_t prevCol = w->as.ansiTerm.lastCursorCol; - int32_t curCol = w->as.ansiTerm.cursorCol; + int32_t prevRow = w->as.ansiTerm->lastCursorRow; + int32_t curRow = w->as.ansiTerm->cursorRow; + int32_t prevCol = w->as.ansiTerm->lastCursorCol; + int32_t curCol = w->as.ansiTerm->cursorCol; if (prevRow != curRow || prevCol != curCol) { - if (prevRow >= 0 && prevRow < w->as.ansiTerm.rows) { - w->as.ansiTerm.dirtyRows |= (1U << prevRow); + if (prevRow >= 0 && prevRow < w->as.ansiTerm->rows) { + w->as.ansiTerm->dirtyRows |= (1U << prevRow); } - if (curRow >= 0 && curRow < w->as.ansiTerm.rows) { - w->as.ansiTerm.dirtyRows |= (1U << curRow); + if (curRow >= 0 && curRow < w->as.ansiTerm->rows) { + w->as.ansiTerm->dirtyRows |= (1U << curRow); } } - uint32_t dirty = w->as.ansiTerm.dirtyRows; + uint32_t dirty = w->as.ansiTerm->dirtyRows; if (dirty == 0) { return 0; } @@ -1391,17 +1396,17 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { const BlitOpsT *ops = &ctx->blitOps; const BitmapFontT *font = &ctx->font; - int32_t cols = w->as.ansiTerm.cols; - int32_t rows = w->as.ansiTerm.rows; + int32_t cols = w->as.ansiTerm->cols; + int32_t rows = w->as.ansiTerm->rows; int32_t cellH = font->charHeight; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; // Use cached palette ansiTermBuildPalette(w, &cd); - const uint32_t *palette = w->as.ansiTerm.packedPalette; + const uint32_t *palette = w->as.ansiTerm->packedPalette; - bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + bool viewingLive = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); int32_t repainted = 0; int32_t minRow = rows; int32_t maxRow = -1; @@ -1411,17 +1416,17 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { continue; } - int32_t lineIndex = w->as.ansiTerm.scrollPos + row; + int32_t lineIndex = w->as.ansiTerm->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); // Cursor column for this row (-1 if cursor not on this row) int32_t curCol2 = -1; - if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn && - row == w->as.ansiTerm.cursorRow) { - curCol2 = w->as.ansiTerm.cursorCol; + if (viewingLive && w->as.ansiTerm->cursorVisible && w->as.ansiTerm->cursorOn && + row == w->as.ansiTerm->cursorRow) { + curCol2 = w->as.ansiTerm->cursorCol; } - drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol2); + drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm->blinkVisible, curCol2); ansiTermPaintSelRow(w, &cd, ops, font, row, baseX, baseY); if (row < minRow) { minRow = row; } @@ -1429,9 +1434,9 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { repainted++; } - w->as.ansiTerm.dirtyRows = 0; - w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow; - w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol; + w->as.ansiTerm->dirtyRows = 0; + w->as.ansiTerm->lastCursorRow = w->as.ansiTerm->cursorRow; + w->as.ansiTerm->lastCursorCol = w->as.ansiTerm->cursorCol; if (outY) { *outY = baseY + minRow * cellH; } if (outH) { *outH = (maxRow - minRow + 1) * cellH; } @@ -1445,13 +1450,11 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { // ============================================================ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { - if (!w || w->type != WidgetAnsiTermE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetAnsiTermE); - w->as.ansiTerm.commCtx = ctx; - w->as.ansiTerm.commRead = readFn; - w->as.ansiTerm.commWrite = writeFn; + w->as.ansiTerm->commCtx = ctx; + w->as.ansiTerm->commRead = readFn; + w->as.ansiTerm->commWrite = writeFn; } @@ -1477,8 +1480,10 @@ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { // ============================================================ void widgetAnsiTermDestroy(WidgetT *w) { - free(w->as.ansiTerm.cells); - free(w->as.ansiTerm.scrollback); + free(w->as.ansiTerm->cells); + free(w->as.ansiTerm->scrollback); + free(w->as.ansiTerm); + w->as.ansiTerm = NULL; } @@ -1487,8 +1492,8 @@ void widgetAnsiTermDestroy(WidgetT *w) { // ============================================================ void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = w->as.ansiTerm.cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; - w->calcMinH = w->as.ansiTerm.rows * font->charHeight + ANSI_BORDER * 2; + w->calcMinW = w->as.ansiTerm->cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; + w->calcMinH = w->as.ansiTerm->rows * font->charHeight + ANSI_BORDER * 2; } @@ -1510,8 +1515,8 @@ static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, co int32_t eCol; ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); - int32_t cols = w->as.ansiTerm.cols; - int32_t lineIndex = w->as.ansiTerm.scrollPos + screenRow; + int32_t cols = w->as.ansiTerm->cols; + int32_t lineIndex = w->as.ansiTerm->scrollPos + screenRow; if (lineIndex < sLine || lineIndex > eLine) { return; @@ -1525,7 +1530,7 @@ static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, co } const uint8_t *lineData = ansiTermGetLine(w, lineIndex); - const uint32_t *palette = w->as.ansiTerm.packedPalette; + const uint32_t *palette = w->as.ansiTerm->packedPalette; int32_t cellH = font->charHeight; int32_t cellW = font->charWidth; @@ -1567,7 +1572,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } - if (!w->as.ansiTerm.commWrite) { + if (!w->as.ansiTerm->commWrite) { return; } @@ -1642,7 +1647,7 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { } if (len > 0) { - w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len); + w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, buf, len); } wgtInvalidatePaint(w); @@ -1661,8 +1666,8 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) hit->focused = true; clearOtherSelections(hit); - int32_t cols = hit->as.ansiTerm.cols; - int32_t rows = hit->as.ansiTerm.rows; + int32_t cols = hit->as.ansiTerm->cols; + int32_t rows = hit->as.ansiTerm->rows; int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; int32_t sbY = hit->y + ANSI_BORDER; int32_t sbH = rows * font->charHeight; @@ -1691,17 +1696,17 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) clickCol = cols - 1; } - int32_t lineIndex = hit->as.ansiTerm.scrollPos + clickRow; + int32_t lineIndex = hit->as.ansiTerm->scrollPos + clickRow; int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 3) { // Triple-click: select entire line - hit->as.ansiTerm.selStartLine = lineIndex; - hit->as.ansiTerm.selStartCol = 0; - hit->as.ansiTerm.selEndLine = lineIndex; - hit->as.ansiTerm.selEndCol = cols; - hit->as.ansiTerm.selecting = false; + hit->as.ansiTerm->selStartLine = lineIndex; + hit->as.ansiTerm->selStartCol = 0; + hit->as.ansiTerm->selEndLine = lineIndex; + hit->as.ansiTerm->selEndCol = cols; + hit->as.ansiTerm->selecting = false; sDragTextSelect = NULL; } else if (clicks == 2) { // Double-click: select word @@ -1717,27 +1722,27 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) we++; } - hit->as.ansiTerm.selStartLine = lineIndex; - hit->as.ansiTerm.selStartCol = ws; - hit->as.ansiTerm.selEndLine = lineIndex; - hit->as.ansiTerm.selEndCol = we; - hit->as.ansiTerm.selecting = false; + hit->as.ansiTerm->selStartLine = lineIndex; + hit->as.ansiTerm->selStartCol = ws; + hit->as.ansiTerm->selEndLine = lineIndex; + hit->as.ansiTerm->selEndCol = we; + hit->as.ansiTerm->selecting = false; sDragTextSelect = NULL; } else { // Single click: start selection anchor - hit->as.ansiTerm.selStartLine = lineIndex; - hit->as.ansiTerm.selStartCol = clickCol; - hit->as.ansiTerm.selEndLine = lineIndex; - hit->as.ansiTerm.selEndCol = clickCol; - hit->as.ansiTerm.selecting = true; + hit->as.ansiTerm->selStartLine = lineIndex; + hit->as.ansiTerm->selStartCol = clickCol; + hit->as.ansiTerm->selEndLine = lineIndex; + hit->as.ansiTerm->selEndCol = clickCol; + hit->as.ansiTerm->selecting = true; sDragTextSelect = hit; } - hit->as.ansiTerm.dirtyRows = 0xFFFFFFFF; + hit->as.ansiTerm->dirtyRows = 0xFFFFFFFF; return; } - int32_t sbCount = hit->as.ansiTerm.scrollbackCount; + int32_t sbCount = hit->as.ansiTerm->scrollbackCount; if (sbCount == 0) { return; @@ -1747,10 +1752,10 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) if (vy >= sbY && vy < sbY + arrowH) { // Up arrow - hit->as.ansiTerm.scrollPos--; + hit->as.ansiTerm->scrollPos--; } else if (vy >= sbY + sbH - arrowH && vy < sbY + sbH) { // Down arrow - hit->as.ansiTerm.scrollPos++; + hit->as.ansiTerm->scrollPos++; } else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { // Track area — compute thumb position to determine page direction int32_t trackY = sbY + arrowH; @@ -1771,25 +1776,25 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) int32_t thumbY = trackY; if (maxScroll > 0 && thumbRange > 0) { - thumbY = trackY + (hit->as.ansiTerm.scrollPos * thumbRange) / maxScroll; + thumbY = trackY + (hit->as.ansiTerm->scrollPos * thumbRange) / maxScroll; } if (vy < thumbY) { // Page up - hit->as.ansiTerm.scrollPos -= rows; + hit->as.ansiTerm->scrollPos -= rows; } else if (vy >= thumbY + thumbH) { // Page down - hit->as.ansiTerm.scrollPos += rows; + hit->as.ansiTerm->scrollPos += rows; } } // Clamp - if (hit->as.ansiTerm.scrollPos < 0) { - hit->as.ansiTerm.scrollPos = 0; + if (hit->as.ansiTerm->scrollPos < 0) { + hit->as.ansiTerm->scrollPos = 0; } - if (hit->as.ansiTerm.scrollPos > maxScroll) { - hit->as.ansiTerm.scrollPos = maxScroll; + if (hit->as.ansiTerm->scrollPos > maxScroll) { + hit->as.ansiTerm->scrollPos = maxScroll; } } @@ -1809,22 +1814,22 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Build/cache the 16-color packed palette ansiTermBuildPalette(w, d); - const uint32_t *palette = w->as.ansiTerm.packedPalette; + const uint32_t *palette = w->as.ansiTerm->packedPalette; - int32_t cols = w->as.ansiTerm.cols; - int32_t rows = w->as.ansiTerm.rows; + int32_t cols = w->as.ansiTerm->cols; + int32_t rows = w->as.ansiTerm->rows; int32_t cellW = font->charWidth; int32_t cellH = font->charHeight; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; - int32_t sbCount = w->as.ansiTerm.scrollbackCount; + int32_t sbCount = w->as.ansiTerm->scrollbackCount; // Determine if viewing live terminal or scrollback - bool viewingLive = (w->as.ansiTerm.scrollPos == sbCount); + bool viewingLive = (w->as.ansiTerm->scrollPos == sbCount); // Render character cells row by row using bulk renderer. // Only repaint rows marked dirty; 0xFFFFFFFF means all rows. - uint32_t dirty = w->as.ansiTerm.dirtyRows; + uint32_t dirty = w->as.ansiTerm->dirtyRows; if (dirty == 0) { dirty = 0xFFFFFFFF; } @@ -1834,21 +1839,21 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit continue; } - int32_t lineIndex = w->as.ansiTerm.scrollPos + row; + int32_t lineIndex = w->as.ansiTerm->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); // Cursor column for this row (-1 if cursor not on this row) int32_t curCol = -1; - if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn && - row == w->as.ansiTerm.cursorRow) { - curCol = w->as.ansiTerm.cursorCol; + if (viewingLive && w->as.ansiTerm->cursorVisible && w->as.ansiTerm->cursorOn && + row == w->as.ansiTerm->cursorRow) { + curCol = w->as.ansiTerm->cursorCol; } - drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol); + drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm->blinkVisible, curCol); ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY); } - w->as.ansiTerm.dirtyRows = 0; + w->as.ansiTerm->dirtyRows = 0; // Draw scrollbar int32_t sbX = baseX + cols * cellW; @@ -1907,7 +1912,7 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t thumbY = trackY; if (maxScroll > 0 && thumbRange > 0) { - thumbY = trackY + (w->as.ansiTerm.scrollPos * thumbRange) / maxScroll; + thumbY = trackY + (w->as.ansiTerm->scrollPos * thumbRange) / maxScroll; } drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 0e2ab31..6c11fba 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -40,9 +40,7 @@ WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) { // ============================================================ int32_t wgtComboBoxGetSelected(const WidgetT *w) { - if (!w || w->type != WidgetComboBoxE) { - return -1; - } + VALIDATE_WIDGET(w, WidgetComboBoxE, -1); return w->as.comboBox.selectedIdx; } @@ -53,9 +51,7 @@ int32_t wgtComboBoxGetSelected(const WidgetT *w) { // ============================================================ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { - if (!w || w->type != WidgetComboBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetComboBoxE); w->as.comboBox.items = items; w->as.comboBox.itemCount = count; @@ -84,9 +80,7 @@ void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { // ============================================================ void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetComboBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetComboBoxE); w->as.comboBox.selectedIdx = idx; @@ -402,40 +396,7 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons int32_t popH; widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH); - - // Draw popup border - BevelStyleT bevel; - bevel.highlight = colors->windowHighlight; - bevel.shadow = colors->windowShadow; - bevel.face = colors->contentBg; - bevel.width = 2; - drawBevel(d, ops, popX, popY, popW, popH, &bevel); - - // Draw items - int32_t itemCount = w->as.comboBox.itemCount; - const char **items = w->as.comboBox.items; - int32_t hoverIdx = w->as.comboBox.hoverIdx; - int32_t scrollPos = w->as.comboBox.listScrollPos; - - int32_t visibleItems = popH / font->charHeight; - int32_t textX = popX + TEXT_INPUT_PAD; - int32_t textY = popY + 2; - int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; - - for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { - int32_t idx = scrollPos + i; - int32_t iy = textY + i * font->charHeight; - uint32_t ifg = colors->contentFg; - uint32_t ibg = colors->contentBg; - - if (idx == hoverIdx) { - ifg = colors->menuHighlightFg; - ibg = colors->menuHighlightBg; - rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg); - } - - drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); - } + widgetPaintPopupList(d, ops, font, colors, popX, popY, popW, popH, w->as.comboBox.items, w->as.comboBox.itemCount, w->as.comboBox.hoverIdx, w->as.comboBox.listScrollPos); } diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 40e3154..75aa6e3 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -423,6 +423,96 @@ bool widgetIsHorizContainer(WidgetTypeE type) { } +// ============================================================ +// widgetNavigateIndex +// ============================================================ +// +// Shared Up/Down/Home/End/PgUp/PgDn handler for list-like widgets. +// Returns the new index, or -1 if the key is not a navigation key. + +int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) { + if (key == (0x50 | 0x100)) { + // Down arrow + if (current < count - 1) { + return current + 1; + } + + return current < 0 ? 0 : current; + } + + if (key == (0x48 | 0x100)) { + // Up arrow + if (current > 0) { + return current - 1; + } + + return current < 0 ? 0 : current; + } + + if (key == (0x47 | 0x100)) { + // Home + return 0; + } + + if (key == (0x4F | 0x100)) { + // End + return count - 1; + } + + if (key == (0x51 | 0x100)) { + // Page Down + int32_t n = current + pageSize; + return n >= count ? count - 1 : n; + } + + if (key == (0x49 | 0x100)) { + // Page Up + int32_t n = current - pageSize; + return n < 0 ? 0 : n; + } + + return -1; +} + + +// ============================================================ +// widgetPaintPopupList +// ============================================================ +// +// Shared popup list painting for Dropdown and ComboBox. + +void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) { + // Draw popup border + BevelStyleT bevel; + bevel.highlight = colors->windowHighlight; + bevel.shadow = colors->windowShadow; + bevel.face = colors->contentBg; + bevel.width = 2; + drawBevel(d, ops, popX, popY, popW, popH, &bevel); + + // Draw items + int32_t visibleItems = popH / font->charHeight; + int32_t textX = popX + TEXT_INPUT_PAD; + int32_t textY = popY + 2; + int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; + + for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { + int32_t idx = scrollPos + i; + int32_t iy = textY + i * font->charHeight; + uint32_t ifg = colors->contentFg; + uint32_t ibg = colors->contentBg; + + if (idx == hoverIdx) { + ifg = colors->menuHighlightFg; + ibg = colors->menuHighlightBg; + rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg); + } + + drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); + } +} + + // ============================================================ // widgetScrollbarThumb // ============================================================ diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index 0bf192e..3c4ca36 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -24,9 +24,7 @@ WidgetT *wgtDropdown(WidgetT *parent) { // ============================================================ int32_t wgtDropdownGetSelected(const WidgetT *w) { - if (!w || w->type != WidgetDropdownE) { - return -1; - } + VALIDATE_WIDGET(w, WidgetDropdownE, -1); return w->as.dropdown.selectedIdx; } @@ -37,9 +35,7 @@ int32_t wgtDropdownGetSelected(const WidgetT *w) { // ============================================================ void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) { - if (!w || w->type != WidgetDropdownE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetDropdownE); w->as.dropdown.items = items; w->as.dropdown.itemCount = count; @@ -68,9 +64,7 @@ void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) { // ============================================================ void wgtDropdownSetSelected(WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetDropdownE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetDropdownE); w->as.dropdown.selectedIdx = idx; } @@ -254,38 +248,5 @@ void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons int32_t popH; widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH); - - // Draw popup border - BevelStyleT bevel; - bevel.highlight = colors->windowHighlight; - bevel.shadow = colors->windowShadow; - bevel.face = colors->contentBg; - bevel.width = 2; - drawBevel(d, ops, popX, popY, popW, popH, &bevel); - - // Draw items - int32_t itemCount = w->as.dropdown.itemCount; - const char **items = w->as.dropdown.items; - int32_t hoverIdx = w->as.dropdown.hoverIdx; - int32_t scrollPos = w->as.dropdown.scrollPos; - - int32_t visibleItems = popH / font->charHeight; - int32_t textX = popX + TEXT_INPUT_PAD; - int32_t textY = popY + 2; - int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; - - for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { - int32_t idx = scrollPos + i; - int32_t iy = textY + i * font->charHeight; - uint32_t ifg = colors->contentFg; - uint32_t ibg = colors->contentBg; - - if (idx == hoverIdx) { - ifg = colors->menuHighlightFg; - ibg = colors->menuHighlightBg; - rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg); - } - - drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); - } + widgetPaintPopupList(d, ops, font, colors, popX, popY, popW, popH, w->as.dropdown.items, w->as.dropdown.itemCount, w->as.dropdown.hoverIdx, w->as.dropdown.scrollPos); } diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 825500e..68de286 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -266,17 +266,17 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { newW = 20; } - if (newW != sResizeListView->as.listView.resolvedColW[sResizeCol]) { - sResizeListView->as.listView.resolvedColW[sResizeCol] = newW; + if (newW != sResizeListView->as.listView->resolvedColW[sResizeCol]) { + sResizeListView->as.listView->resolvedColW[sResizeCol] = newW; // Recalculate totalColW int32_t total = 0; - for (int32_t c = 0; c < sResizeListView->as.listView.colCount; c++) { - total += sResizeListView->as.listView.resolvedColW[c]; + for (int32_t c = 0; c < sResizeListView->as.listView->colCount; c++) { + total += sResizeListView->as.listView->resolvedColW[c]; } - sResizeListView->as.listView.totalColW = total; + sResizeListView->as.listView->totalColW = total; wgtInvalidatePaint(root); } @@ -651,11 +651,11 @@ void widgetReorderDrop(WidgetT *w) { w->onChange(w); } } else if (w->type == WidgetListViewE) { - int32_t drag = w->as.listView.dragIdx; - int32_t drop = w->as.listView.dropIdx; - int32_t colCnt = w->as.listView.colCount; - w->as.listView.dragIdx = -1; - w->as.listView.dropIdx = -1; + int32_t drag = w->as.listView->dragIdx; + int32_t drop = w->as.listView->dropIdx; + int32_t colCnt = w->as.listView->colCount; + w->as.listView->dragIdx = -1; + w->as.listView->dropIdx = -1; if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) { return; @@ -665,79 +665,79 @@ void widgetReorderDrop(WidgetT *w) { const char *temp[LISTVIEW_MAX_COLS]; for (int32_t c = 0; c < colCnt; c++) { - temp[c] = w->as.listView.cellData[drag * colCnt + c]; + temp[c] = w->as.listView->cellData[drag * colCnt + c]; } uint8_t selBit = 0; - if (w->as.listView.multiSelect && w->as.listView.selBits) { - selBit = w->as.listView.selBits[drag]; + if (w->as.listView->multiSelect && w->as.listView->selBits) { + selBit = w->as.listView->selBits[drag]; } int32_t sortVal = 0; - if (w->as.listView.sortIndex) { - sortVal = w->as.listView.sortIndex[drag]; + if (w->as.listView->sortIndex) { + sortVal = w->as.listView->sortIndex[drag]; } if (drag < drop) { for (int32_t i = drag; i < drop - 1; i++) { for (int32_t c = 0; c < colCnt; c++) { - w->as.listView.cellData[i * colCnt + c] = w->as.listView.cellData[(i + 1) * colCnt + c]; + w->as.listView->cellData[i * colCnt + c] = w->as.listView->cellData[(i + 1) * colCnt + c]; } - if (w->as.listView.selBits) { - w->as.listView.selBits[i] = w->as.listView.selBits[i + 1]; + if (w->as.listView->selBits) { + w->as.listView->selBits[i] = w->as.listView->selBits[i + 1]; } - if (w->as.listView.sortIndex) { - w->as.listView.sortIndex[i] = w->as.listView.sortIndex[i + 1]; + if (w->as.listView->sortIndex) { + w->as.listView->sortIndex[i] = w->as.listView->sortIndex[i + 1]; } } int32_t dest = drop - 1; for (int32_t c = 0; c < colCnt; c++) { - w->as.listView.cellData[dest * colCnt + c] = temp[c]; + w->as.listView->cellData[dest * colCnt + c] = temp[c]; } - if (w->as.listView.selBits) { - w->as.listView.selBits[dest] = selBit; + if (w->as.listView->selBits) { + w->as.listView->selBits[dest] = selBit; } - if (w->as.listView.sortIndex) { - w->as.listView.sortIndex[dest] = sortVal; + if (w->as.listView->sortIndex) { + w->as.listView->sortIndex[dest] = sortVal; } - w->as.listView.selectedIdx = dest; + w->as.listView->selectedIdx = dest; } else { for (int32_t i = drag; i > drop; i--) { for (int32_t c = 0; c < colCnt; c++) { - w->as.listView.cellData[i * colCnt + c] = w->as.listView.cellData[(i - 1) * colCnt + c]; + w->as.listView->cellData[i * colCnt + c] = w->as.listView->cellData[(i - 1) * colCnt + c]; } - if (w->as.listView.selBits) { - w->as.listView.selBits[i] = w->as.listView.selBits[i - 1]; + if (w->as.listView->selBits) { + w->as.listView->selBits[i] = w->as.listView->selBits[i - 1]; } - if (w->as.listView.sortIndex) { - w->as.listView.sortIndex[i] = w->as.listView.sortIndex[i - 1]; + if (w->as.listView->sortIndex) { + w->as.listView->sortIndex[i] = w->as.listView->sortIndex[i - 1]; } } for (int32_t c = 0; c < colCnt; c++) { - w->as.listView.cellData[drop * colCnt + c] = temp[c]; + w->as.listView->cellData[drop * colCnt + c] = temp[c]; } - if (w->as.listView.selBits) { - w->as.listView.selBits[drop] = selBit; + if (w->as.listView->selBits) { + w->as.listView->selBits[drop] = selBit; } - if (w->as.listView.sortIndex) { - w->as.listView.sortIndex[drop] = sortVal; + if (w->as.listView->sortIndex) { + w->as.listView->sortIndex[drop] = sortVal; } - w->as.listView.selectedIdx = drop; + w->as.listView->selectedIdx = drop; } if (w->onChange) { @@ -844,21 +844,21 @@ void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { int32_t innerY = w->y + LISTVIEW_BORDER + headerH; int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t visibleRows = innerH / font->charHeight; - int32_t maxScroll = w->as.listView.rowCount - visibleRows; + int32_t maxScroll = w->as.listView->rowCount - visibleRows; if (maxScroll < 0) { maxScroll = 0; } // Auto-scroll when dragging near edges - if (y < innerY + font->charHeight && w->as.listView.scrollPos > 0) { - w->as.listView.scrollPos--; - } else if (y > innerY + innerH - font->charHeight && w->as.listView.scrollPos < maxScroll) { - w->as.listView.scrollPos++; + if (y < innerY + font->charHeight && w->as.listView->scrollPos > 0) { + w->as.listView->scrollPos--; + } else if (y > innerY + innerH - font->charHeight && w->as.listView->scrollPos < maxScroll) { + w->as.listView->scrollPos++; } int32_t relY = y - innerY; - int32_t row = w->as.listView.scrollPos + relY / font->charHeight; + int32_t row = w->as.listView->scrollPos + relY / font->charHeight; int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0; int32_t drop = row + halfRow; @@ -866,11 +866,11 @@ void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { drop = 0; } - if (drop > w->as.listView.rowCount) { - drop = w->as.listView.rowCount; + if (drop > w->as.listView->rowCount) { + drop = w->as.listView->rowCount; } - w->as.listView.dropIdx = drop; + w->as.listView->dropIdx = drop; } else if (w->type == WidgetTreeViewE) { int32_t innerY = w->y + TREE_BORDER; int32_t innerH = w->h - TREE_BORDER * 2; diff --git a/dvx/widgets/widgetImage.c b/dvx/widgets/widgetImage.c index 1af41a9..59cf3a7 100644 --- a/dvx/widgets/widgetImage.c +++ b/dvx/widgets/widgetImage.c @@ -100,9 +100,7 @@ WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) { // ============================================================ void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch) { - if (!w || w->type != WidgetImageE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetImageE); free(w->as.image.data); w->as.image.data = data; diff --git a/dvx/widgets/widgetImageButton.c b/dvx/widgets/widgetImageButton.c index 4a054f4..2c7f8a3 100644 --- a/dvx/widgets/widgetImageButton.c +++ b/dvx/widgets/widgetImageButton.c @@ -33,9 +33,7 @@ WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, in // ============================================================ void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch) { - if (!w || w->type != WidgetImageButtonE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetImageButtonE); free(w->as.imageButton.data); w->as.imageButton.data = data; diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index f0e2a22..0b6ed38 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -37,6 +37,16 @@ typedef struct WidgetClassT { extern const WidgetClassT *widgetClassTable[]; +// ============================================================ +// Validation macros +// ============================================================ + +#define VALIDATE_WIDGET(w, wtype, retval) \ + do { if (!(w) || (w)->type != (wtype)) { return (retval); } } while (0) + +#define VALIDATE_WIDGET_VOID(w, wtype) \ + do { if (!(w) || (w)->type != (wtype)) { return; } } while (0) + // ============================================================ // Constants // ============================================================ @@ -69,7 +79,7 @@ extern const WidgetClassT *widgetClassTable[]; #define TREE_EXPAND_SIZE 9 #define TREE_ICON_GAP 4 #define TREE_BORDER 2 -#define TREE_SB_W 14 +#define WGT_SB_W 14 #define TREE_MIN_ROWS 4 #define SB_MIN_THUMB 14 #define SPLITTER_BAR_W 5 @@ -137,9 +147,28 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); bool widgetIsFocusable(WidgetTypeE type); bool widgetIsBoxContainer(WidgetTypeE type); bool widgetIsHorizContainer(WidgetTypeE type); +int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); +void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); void widgetRemoveChild(WidgetT *parent, WidgetT *child); void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize); +// ============================================================ +// Shared scrollbar functions (widgetScrollbar.c) +// ============================================================ + +typedef enum { + ScrollHitNoneE, + ScrollHitArrowDecE, + ScrollHitArrowIncE, + ScrollHitPageDecE, + ScrollHitPageIncE, + ScrollHitThumbE +} ScrollHitE; + +void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); +void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); +ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); + // ============================================================ // Layout functions (widgetLayout.c) // ============================================================ diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index e887c1e..58d8246 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -7,7 +7,6 @@ #define LISTBOX_PAD 2 #define LISTBOX_MIN_ROWS 4 -#define LISTBOX_SB_W 14 // ============================================================ @@ -137,9 +136,7 @@ void wgtListBoxClearSelection(WidgetT *w) { // ============================================================ int32_t wgtListBoxGetSelected(const WidgetT *w) { - if (!w || w->type != WidgetListBoxE) { - return -1; - } + VALIDATE_WIDGET(w, WidgetListBoxE, -1); return w->as.listBox.selectedIdx; } @@ -150,9 +147,7 @@ int32_t wgtListBoxGetSelected(const WidgetT *w) { // ============================================================ bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetListBoxE) { - return false; - } + VALIDATE_WIDGET(w, WidgetListBoxE, false); if (!w->as.listBox.multiSelect) { return idx == w->as.listBox.selectedIdx; @@ -184,9 +179,7 @@ void wgtListBoxSelectAll(WidgetT *w) { // ============================================================ void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) { - if (!w || w->type != WidgetListBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListBoxE); if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) { return; @@ -201,9 +194,7 @@ void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) { // ============================================================ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { - if (!w || w->type != WidgetListBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListBoxE); w->as.listBox.items = items; w->as.listBox.itemCount = count; @@ -246,9 +237,7 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { // ============================================================ void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) { - if (!w || w->type != WidgetListBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListBoxE); w->as.listBox.reorderable = reorderable; } @@ -259,9 +248,7 @@ void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) { // ============================================================ void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) { - if (!w || w->type != WidgetListBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListBoxE); w->as.listBox.multiSelect = multi; allocSelBits(w); @@ -278,9 +265,7 @@ void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) { // ============================================================ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetListBoxE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListBoxE); w->as.listBox.selectedIdx = idx; w->as.listBox.anchorIdx = idx; @@ -308,7 +293,7 @@ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { maxItemW = minW; } - w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + LISTBOX_SB_W; + w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + WGT_SB_W; w->calcMinH = LISTBOX_MIN_ROWS * font->charHeight + LISTBOX_BORDER * 2; } @@ -349,59 +334,17 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } - int32_t newSel = sel; + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + const BitmapFontT *font = &ctx->font; + int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; - if (key == (0x50 | 0x100)) { - // Down arrow - if (newSel < w->as.listBox.itemCount - 1) { - newSel++; - } else if (newSel < 0) { - newSel = 0; - } - } else if (key == (0x48 | 0x100)) { - // Up arrow - if (newSel > 0) { - newSel--; - } else if (newSel < 0) { - newSel = 0; - } - } else if (key == (0x47 | 0x100)) { - // Home - newSel = 0; - } else if (key == (0x4F | 0x100)) { - // End - newSel = w->as.listBox.itemCount - 1; - } else if (key == (0x51 | 0x100)) { - // Page Down - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; + if (visibleRows < 1) { + visibleRows = 1; + } - if (visibleRows < 1) { - visibleRows = 1; - } + int32_t newSel = widgetNavigateIndex(key, sel, w->as.listBox.itemCount, visibleRows); - newSel += visibleRows; - - if (newSel >= w->as.listBox.itemCount) { - newSel = w->as.listBox.itemCount - 1; - } - } else if (key == (0x49 | 0x100)) { - // Page Up - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; - - if (visibleRows < 1) { - visibleRows = 1; - } - - newSel -= visibleRows; - - if (newSel < 0) { - newSel = 0; - } - } else { + if (newSel < 0) { return; } @@ -454,39 +397,26 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { // Check if click is on the scrollbar if (needSb) { - int32_t sbX = hit->x + hit->w - LISTBOX_BORDER - LISTBOX_SB_W; + int32_t sbX = hit->x + hit->w - LISTBOX_BORDER - WGT_SB_W; if (vx >= sbX) { - int32_t sbY = hit->y + LISTBOX_BORDER; - int32_t sbH = innerH; - int32_t relY = vy - sbY; - int32_t trackLen = sbH - LISTBOX_SB_W * 2; + int32_t relY = vy - (hit->y + LISTBOX_BORDER); + ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, hit->as.listBox.itemCount, visibleRows, hit->as.listBox.scrollPos); - if (relY < LISTBOX_SB_W) { - // Up arrow + if (sh == ScrollHitArrowDecE) { if (hit->as.listBox.scrollPos > 0) { hit->as.listBox.scrollPos--; } - } else if (relY >= sbH - LISTBOX_SB_W) { - // Down arrow + } else if (sh == ScrollHitArrowIncE) { if (hit->as.listBox.scrollPos < maxScroll) { hit->as.listBox.scrollPos++; } - } else if (trackLen > 0) { - // Track — page up/down - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, hit->as.listBox.itemCount, visibleRows, hit->as.listBox.scrollPos, &thumbPos, &thumbSize); - - int32_t trackRelY = relY - LISTBOX_SB_W; - - if (trackRelY < thumbPos) { - hit->as.listBox.scrollPos -= visibleRows; - hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll); - } else if (trackRelY >= thumbPos + thumbSize) { - hit->as.listBox.scrollPos += visibleRows; - hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll); - } + } else if (sh == ScrollHitPageDecE) { + hit->as.listBox.scrollPos -= visibleRows; + hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll); + } else if (sh == ScrollHitPageIncE) { + hit->as.listBox.scrollPos += visibleRows; + hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll); } hit->focused = true; @@ -564,7 +494,7 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm int32_t contentW = w->w - LISTBOX_BORDER * 2; if (needSb) { - contentW -= LISTBOX_SB_W; + contentW -= WGT_SB_W; } // Sunken border @@ -628,52 +558,9 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm // Draw scrollbar if (needSb) { - int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W; + int32_t sbX = w->x + w->w - LISTBOX_BORDER - WGT_SB_W; int32_t sbY = w->y + LISTBOX_BORDER; - int32_t sbH = innerH; - - // Trough - BevelStyleT troughBevel = BEVEL_TROUGH(colors); - drawBevel(d, ops, sbX, sbY, LISTBOX_SB_W, sbH, &troughBevel); - - // Up arrow button - BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); - drawBevel(d, ops, sbX, sbY, LISTBOX_SB_W, LISTBOX_SB_W, &btnBevel); - - // Up arrow triangle - { - int32_t cx = sbX + LISTBOX_SB_W / 2; - int32_t cy = sbY + LISTBOX_SB_W / 2; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg); - } - } - - // Down arrow button - int32_t downY = sbY + sbH - LISTBOX_SB_W; - drawBevel(d, ops, sbX, downY, LISTBOX_SB_W, LISTBOX_SB_W, &btnBevel); - - // Down arrow triangle - { - int32_t cx = sbX + LISTBOX_SB_W / 2; - int32_t cy = downY + LISTBOX_SB_W / 2; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg); - } - } - - // Thumb - int32_t trackLen = sbH - LISTBOX_SB_W * 2; - - if (trackLen > 0) { - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, w->as.listBox.itemCount, visibleRows, w->as.listBox.scrollPos, &thumbPos, &thumbSize); - - drawBevel(d, ops, sbX, sbY + LISTBOX_SB_W + thumbPos, LISTBOX_SB_W, thumbSize, &btnBevel); - } + widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, w->as.listBox.itemCount, visibleRows, w->as.listBox.scrollPos); } if (w->focused) { diff --git a/dvx/widgets/widgetListView.c b/dvx/widgets/widgetListView.c index fef0207..e3420d5 100644 --- a/dvx/widgets/widgetListView.c +++ b/dvx/widgets/widgetListView.c @@ -6,7 +6,6 @@ #include #define LISTVIEW_PAD 3 -#define LISTVIEW_SB_W 14 #define LISTVIEW_MIN_ROWS 4 #define LISTVIEW_COL_PAD 6 #define LISTVIEW_SORT_W 10 @@ -17,8 +16,6 @@ // ============================================================ static void allocListViewSelBits(WidgetT *w); -static void drawListViewHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW); -static void drawListViewVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalRows, int32_t visibleRows); static void listViewBuildSortIndex(WidgetT *w); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); @@ -28,127 +25,15 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); // ============================================================ static void allocListViewSelBits(WidgetT *w) { - if (w->as.listView.selBits) { - free(w->as.listView.selBits); - w->as.listView.selBits = NULL; + if (w->as.listView->selBits) { + free(w->as.listView->selBits); + w->as.listView->selBits = NULL; } - int32_t count = w->as.listView.rowCount; + int32_t count = w->as.listView->rowCount; - if (count > 0 && w->as.listView.multiSelect) { - w->as.listView.selBits = (uint8_t *)calloc(count, 1); - } -} - - -// ============================================================ -// drawListViewHScrollbar -// ============================================================ - -static void drawListViewHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW) { - if (sbW < LISTVIEW_SB_W * 3) { - return; - } - - // Trough background - BevelStyleT troughBevel = BEVEL_TROUGH(colors); - drawBevel(d, ops, sbX, sbY, sbW, LISTVIEW_SB_W, &troughBevel); - - // Left arrow button - BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); - drawBevel(d, ops, sbX, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); - - // Left arrow triangle - { - int32_t cx = sbX + LISTVIEW_SB_W / 2; - int32_t cy = sbY + LISTVIEW_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); - } - } - - // Right arrow button - int32_t rightX = sbX + sbW - LISTVIEW_SB_W; - drawBevel(d, ops, rightX, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); - - // Right arrow triangle - { - int32_t cx = rightX + LISTVIEW_SB_W / 2; - int32_t cy = sbY + LISTVIEW_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); - } - } - - // Thumb - int32_t trackLen = sbW - LISTVIEW_SB_W * 2; - - if (trackLen > 0 && totalW > 0) { - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalW, visibleW, w->as.listView.scrollPosH, &thumbPos, &thumbSize); - - drawBevel(d, ops, sbX + LISTVIEW_SB_W + thumbPos, sbY, thumbSize, LISTVIEW_SB_W, &btnBevel); - } -} - - -// ============================================================ -// drawListViewVScrollbar -// ============================================================ - -static void drawListViewVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalRows, int32_t visibleRows) { - if (sbH < LISTVIEW_SB_W * 3) { - return; - } - - // Trough background - BevelStyleT troughBevel = BEVEL_TROUGH(colors); - drawBevel(d, ops, sbX, sbY, LISTVIEW_SB_W, sbH, &troughBevel); - - // Up arrow button - BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); - drawBevel(d, ops, sbX, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); - - // Up arrow triangle - { - int32_t cx = sbX + LISTVIEW_SB_W / 2; - int32_t cy = sbY + LISTVIEW_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); - } - } - - // Down arrow button - int32_t downY = sbY + sbH - LISTVIEW_SB_W; - drawBevel(d, ops, sbX, downY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); - - // Down arrow triangle - { - int32_t cx = sbX + LISTVIEW_SB_W / 2; - int32_t cy = downY + LISTVIEW_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); - } - } - - // Thumb - int32_t trackLen = sbH - LISTVIEW_SB_W * 2; - - if (trackLen > 0 && totalRows > 0) { - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalRows, visibleRows, w->as.listView.scrollPos, &thumbPos, &thumbSize); - - drawBevel(d, ops, sbX, sbY + LISTVIEW_SB_W + thumbPos, LISTVIEW_SB_W, thumbSize, &btnBevel); + if (count > 0 && w->as.listView->multiSelect) { + w->as.listView->selBits = (uint8_t *)calloc(count, 1); } } @@ -162,22 +47,22 @@ static void drawListViewVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, // the index so paint uses natural order. static void listViewBuildSortIndex(WidgetT *w) { - int32_t rowCount = w->as.listView.rowCount; - int32_t sortCol = w->as.listView.sortCol; - int32_t colCount = w->as.listView.colCount; + int32_t rowCount = w->as.listView->rowCount; + int32_t sortCol = w->as.listView->sortCol; + int32_t colCount = w->as.listView->colCount; // No sort active — clear index - if (sortCol < 0 || w->as.listView.sortDir == ListViewSortNoneE || rowCount <= 0) { - if (w->as.listView.sortIndex) { - free(w->as.listView.sortIndex); - w->as.listView.sortIndex = NULL; + if (sortCol < 0 || w->as.listView->sortDir == ListViewSortNoneE || rowCount <= 0) { + if (w->as.listView->sortIndex) { + free(w->as.listView->sortIndex); + w->as.listView->sortIndex = NULL; } return; } // Allocate or reuse - int32_t *idx = w->as.listView.sortIndex; + int32_t *idx = w->as.listView->sortIndex; if (!idx) { idx = (int32_t *)malloc(rowCount * sizeof(int32_t)); @@ -186,7 +71,7 @@ static void listViewBuildSortIndex(WidgetT *w) { return; } - w->as.listView.sortIndex = idx; + w->as.listView->sortIndex = idx; } // Initialize identity @@ -195,11 +80,11 @@ static void listViewBuildSortIndex(WidgetT *w) { } // Insertion sort — stable, O(n^2) but fine for typical row counts - bool ascending = (w->as.listView.sortDir == ListViewSortAscE); + bool ascending = (w->as.listView->sortDir == ListViewSortAscE); for (int32_t i = 1; i < rowCount; i++) { int32_t key = idx[i]; - const char *keyStr = w->as.listView.cellData[key * colCount + sortCol]; + const char *keyStr = w->as.listView->cellData[key * colCount + sortCol]; if (!keyStr) { keyStr = ""; @@ -208,7 +93,7 @@ static void listViewBuildSortIndex(WidgetT *w) { int32_t j = i - 1; while (j >= 0) { - const char *jStr = w->as.listView.cellData[idx[j] * colCount + sortCol]; + const char *jStr = w->as.listView->cellData[idx[j] * colCount + sortCol]; if (!jStr) { jStr = ""; @@ -242,7 +127,7 @@ static void listViewBuildSortIndex(WidgetT *w) { // cellData for the widest string in that column. static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) { - int32_t colCount = w->as.listView.colCount; + int32_t colCount = w->as.listView->colCount; int32_t parentW = w->w - LISTVIEW_BORDER * 2; if (parentW < 0) { @@ -252,14 +137,14 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) { int32_t totalW = 0; for (int32_t c = 0; c < colCount; c++) { - int32_t taggedW = w->as.listView.cols[c].width; + int32_t taggedW = w->as.listView->cols[c].width; if (taggedW == 0) { // Auto-size: scan data for widest string in this column - int32_t maxLen = (int32_t)strlen(w->as.listView.cols[c].title); + int32_t maxLen = (int32_t)strlen(w->as.listView->cols[c].title); - for (int32_t r = 0; r < w->as.listView.rowCount; r++) { - const char *cell = w->as.listView.cellData[r * colCount + c]; + for (int32_t r = 0; r < w->as.listView->rowCount; r++) { + const char *cell = w->as.listView->cellData[r * colCount + c]; if (cell) { int32_t slen = (int32_t)strlen(cell); @@ -270,15 +155,15 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) { } } - w->as.listView.resolvedColW[c] = maxLen * font->charWidth + LISTVIEW_COL_PAD; + w->as.listView->resolvedColW[c] = maxLen * font->charWidth + LISTVIEW_COL_PAD; } else { - w->as.listView.resolvedColW[c] = wgtResolveSize(taggedW, parentW, font->charWidth); + w->as.listView->resolvedColW[c] = wgtResolveSize(taggedW, parentW, font->charWidth); } - totalW += w->as.listView.resolvedColW[c]; + totalW += w->as.listView->resolvedColW[c]; } - w->as.listView.totalColW = totalW; + w->as.listView->totalColW = totalW; } @@ -290,7 +175,7 @@ static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) { // Coordinates are in widget/virtual space (scroll-adjusted). bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) { - if (!w || w->type != WidgetListViewE || w->as.listView.colCount == 0) { + if (!w || w->type != WidgetListViewE || w->as.listView->colCount == 0) { return false; } @@ -303,16 +188,16 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) { return false; } - int32_t colX = w->x + LISTVIEW_BORDER - w->as.listView.scrollPosH; + int32_t colX = w->x + LISTVIEW_BORDER - w->as.listView->scrollPosH; - for (int32_t c = 0; c < w->as.listView.colCount; c++) { - int32_t border = colX + w->as.listView.resolvedColW[c]; + for (int32_t c = 0; c < w->as.listView->colCount; c++) { + int32_t border = colX + w->as.listView->resolvedColW[c]; if (vx >= border - 3 && vx <= border + 3) { return true; } - colX += w->as.listView.resolvedColW[c]; + colX += w->as.listView->resolvedColW[c]; } return false; @@ -324,15 +209,16 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) { // ============================================================ void widgetListViewDestroy(WidgetT *w) { - if (w->as.listView.sortIndex) { - free(w->as.listView.sortIndex); - w->as.listView.sortIndex = NULL; + if (w->as.listView->sortIndex) { + free(w->as.listView->sortIndex); } - if (w->as.listView.selBits) { - free(w->as.listView.selBits); - w->as.listView.selBits = NULL; + if (w->as.listView->selBits) { + free(w->as.listView->selBits); } + + free(w->as.listView); + w->as.listView = NULL; } @@ -343,16 +229,25 @@ void widgetListViewDestroy(WidgetT *w) { WidgetT *wgtListView(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, WidgetListViewE); - if (w) { - w->as.listView.selectedIdx = -1; - w->as.listView.anchorIdx = -1; - w->as.listView.sortCol = -1; - w->as.listView.sortDir = ListViewSortNoneE; - w->as.listView.dragIdx = -1; - w->as.listView.dropIdx = -1; - w->weight = 100; + if (!w) { + return NULL; } + w->as.listView = (ListViewDataT *)calloc(1, sizeof(ListViewDataT)); + + if (!w->as.listView) { + free(w); + return NULL; + } + + w->as.listView->selectedIdx = -1; + w->as.listView->anchorIdx = -1; + w->as.listView->sortCol = -1; + w->as.listView->sortDir = ListViewSortNoneE; + w->as.listView->dragIdx = -1; + w->as.listView->dropIdx = -1; + w->weight = 100; + return w; } @@ -362,11 +257,9 @@ WidgetT *wgtListView(WidgetT *parent) { // ============================================================ int32_t wgtListViewGetSelected(const WidgetT *w) { - if (!w || w->type != WidgetListViewE) { - return -1; - } + VALIDATE_WIDGET(w, WidgetListViewE, -1); - return w->as.listView.selectedIdx; + return w->as.listView->selectedIdx; } @@ -375,11 +268,11 @@ int32_t wgtListViewGetSelected(const WidgetT *w) { // ============================================================ void wgtListViewClearSelection(WidgetT *w) { - if (!w || w->type != WidgetListViewE || !w->as.listView.selBits) { + if (!w || w->type != WidgetListViewE || !w->as.listView->selBits) { return; } - memset(w->as.listView.selBits, 0, w->as.listView.rowCount); + memset(w->as.listView->selBits, 0, w->as.listView->rowCount); } @@ -388,19 +281,17 @@ void wgtListViewClearSelection(WidgetT *w) { // ============================================================ bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetListViewE) { + VALIDATE_WIDGET(w, WidgetListViewE, false); + + if (!w->as.listView->multiSelect) { + return idx == w->as.listView->selectedIdx; + } + + if (!w->as.listView->selBits || idx < 0 || idx >= w->as.listView->rowCount) { return false; } - if (!w->as.listView.multiSelect) { - return idx == w->as.listView.selectedIdx; - } - - if (!w->as.listView.selBits || idx < 0 || idx >= w->as.listView.rowCount) { - return false; - } - - return w->as.listView.selBits[idx] != 0; + return w->as.listView->selBits[idx] != 0; } @@ -409,11 +300,11 @@ bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) { // ============================================================ void wgtListViewSelectAll(WidgetT *w) { - if (!w || w->type != WidgetListViewE || !w->as.listView.selBits) { + if (!w || w->type != WidgetListViewE || !w->as.listView->selBits) { return; } - memset(w->as.listView.selBits, 1, w->as.listView.rowCount); + memset(w->as.listView->selBits, 1, w->as.listView->rowCount); } @@ -422,17 +313,15 @@ void wgtListViewSelectAll(WidgetT *w) { // ============================================================ void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); if (count > LISTVIEW_MAX_COLS) { count = LISTVIEW_MAX_COLS; } - w->as.listView.cols = cols; - w->as.listView.colCount = count; - w->as.listView.totalColW = 0; + w->as.listView->cols = cols; + w->as.listView->colCount = count; + w->as.listView->totalColW = 0; } @@ -441,29 +330,27 @@ void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) // ============================================================ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); // Free old sort index since row count may have changed - if (w->as.listView.sortIndex) { - free(w->as.listView.sortIndex); - w->as.listView.sortIndex = NULL; + if (w->as.listView->sortIndex) { + free(w->as.listView->sortIndex); + w->as.listView->sortIndex = NULL; } - w->as.listView.cellData = cellData; - w->as.listView.rowCount = rowCount; - w->as.listView.totalColW = 0; + w->as.listView->cellData = cellData; + w->as.listView->rowCount = rowCount; + w->as.listView->totalColW = 0; - if (w->as.listView.selectedIdx >= rowCount) { - w->as.listView.selectedIdx = rowCount > 0 ? 0 : -1; + if (w->as.listView->selectedIdx >= rowCount) { + w->as.listView->selectedIdx = rowCount > 0 ? 0 : -1; } - if (w->as.listView.selectedIdx < 0 && rowCount > 0) { - w->as.listView.selectedIdx = 0; + if (w->as.listView->selectedIdx < 0 && rowCount > 0) { + w->as.listView->selectedIdx = 0; } - w->as.listView.anchorIdx = w->as.listView.selectedIdx; + w->as.listView->anchorIdx = w->as.listView->selectedIdx; // Rebuild sort index if sort is active listViewBuildSortIndex(w); @@ -471,8 +358,8 @@ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { // Reallocate selection bits allocListViewSelBits(w); - if (w->as.listView.selBits && w->as.listView.selectedIdx >= 0) { - w->as.listView.selBits[w->as.listView.selectedIdx] = 1; + if (w->as.listView->selBits && w->as.listView->selectedIdx >= 0) { + w->as.listView->selBits[w->as.listView->selectedIdx] = 1; } } @@ -482,11 +369,9 @@ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { // ============================================================ void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); - w->as.listView.onHeaderClick = cb; + w->as.listView->onHeaderClick = cb; } @@ -495,15 +380,13 @@ void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_ // ============================================================ void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) { - if (!w || w->type != WidgetListViewE) { + VALIDATE_WIDGET_VOID(w, WidgetListViewE); + + if (!w->as.listView->selBits || idx < 0 || idx >= w->as.listView->rowCount) { return; } - if (!w->as.listView.selBits || idx < 0 || idx >= w->as.listView.rowCount) { - return; - } - - w->as.listView.selBits[idx] = selected ? 1 : 0; + w->as.listView->selBits[idx] = selected ? 1 : 0; } @@ -512,20 +395,18 @@ void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) { // ============================================================ void wgtListViewSetReorderable(WidgetT *w, bool reorderable) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); - w->as.listView.reorderable = reorderable; + w->as.listView->reorderable = reorderable; // Disable sorting when reorderable — sort order conflicts with manual order if (reorderable) { - w->as.listView.sortCol = -1; - w->as.listView.sortDir = ListViewSortNoneE; + w->as.listView->sortCol = -1; + w->as.listView->sortDir = ListViewSortNoneE; - if (w->as.listView.sortIndex) { - free(w->as.listView.sortIndex); - w->as.listView.sortIndex = NULL; + if (w->as.listView->sortIndex) { + free(w->as.listView->sortIndex); + w->as.listView->sortIndex = NULL; } } } @@ -536,15 +417,13 @@ void wgtListViewSetReorderable(WidgetT *w, bool reorderable) { // ============================================================ void wgtListViewSetMultiSelect(WidgetT *w, bool multi) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); - w->as.listView.multiSelect = multi; + w->as.listView->multiSelect = multi; allocListViewSelBits(w); - if (w->as.listView.selBits && w->as.listView.selectedIdx >= 0) { - w->as.listView.selBits[w->as.listView.selectedIdx] = 1; + if (w->as.listView->selBits && w->as.listView->selectedIdx >= 0) { + w->as.listView->selBits[w->as.listView->selectedIdx] = 1; } } @@ -554,18 +433,16 @@ void wgtListViewSetMultiSelect(WidgetT *w, bool multi) { // ============================================================ void wgtListViewSetSelected(WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); - w->as.listView.selectedIdx = idx; - w->as.listView.anchorIdx = idx; + w->as.listView->selectedIdx = idx; + w->as.listView->anchorIdx = idx; - if (w->as.listView.selBits) { - memset(w->as.listView.selBits, 0, w->as.listView.rowCount); + if (w->as.listView->selBits) { + memset(w->as.listView->selBits, 0, w->as.listView->rowCount); - if (idx >= 0 && idx < w->as.listView.rowCount) { - w->as.listView.selBits[idx] = 1; + if (idx >= 0 && idx < w->as.listView->rowCount) { + w->as.listView->selBits[idx] = 1; } } } @@ -576,12 +453,10 @@ void wgtListViewSetSelected(WidgetT *w, int32_t idx) { // ============================================================ void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) { - if (!w || w->type != WidgetListViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetListViewE); - w->as.listView.sortCol = col; - w->as.listView.sortDir = dir; + w->as.listView->sortCol = col; + w->as.listView->sortDir = dir; listViewBuildSortIndex(w); } @@ -592,7 +467,7 @@ void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) { void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t headerH = font->charHeight + 4; - int32_t minW = font->charWidth * 12 + LISTVIEW_BORDER * 2 + LISTVIEW_SB_W; + int32_t minW = font->charWidth * 12 + LISTVIEW_BORDER * 2 + WGT_SB_W; w->calcMinW = minW; w->calcMinH = headerH + LISTVIEW_MIN_ROWS * font->charHeight + LISTVIEW_BORDER * 2; @@ -604,15 +479,15 @@ void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { - if (!w || w->type != WidgetListViewE || w->as.listView.rowCount == 0) { + if (!w || w->type != WidgetListViewE || w->as.listView->rowCount == 0) { return; } - bool multi = w->as.listView.multiSelect; + bool multi = w->as.listView->multiSelect; bool shift = (mod & KEY_MOD_SHIFT) != 0; bool ctrl = (mod & KEY_MOD_CTRL) != 0; - int32_t rowCount = w->as.listView.rowCount; - int32_t *sortIdx = w->as.listView.sortIndex; + int32_t rowCount = w->as.listView->rowCount; + int32_t *sortIdx = w->as.listView->sortIndex; // Ctrl+A — select all (multi-select only) if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) { @@ -623,11 +498,11 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { // Space — toggle current item (multi-select only) if (multi && key == ' ') { - int32_t sel = w->as.listView.selectedIdx; + int32_t sel = w->as.listView->selectedIdx; - if (sel >= 0 && w->as.listView.selBits) { - w->as.listView.selBits[sel] ^= 1; - w->as.listView.anchorIdx = sel; + if (sel >= 0 && w->as.listView->selBits) { + w->as.listView->selBits[sel] ^= 1; + w->as.listView->anchorIdx = sel; } if (w->onChange) { @@ -641,16 +516,16 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { // Find current display row from selectedIdx (data row) int32_t displaySel = -1; - if (w->as.listView.selectedIdx >= 0) { + if (w->as.listView->selectedIdx >= 0) { if (sortIdx) { for (int32_t i = 0; i < rowCount; i++) { - if (sortIdx[i] == w->as.listView.selectedIdx) { + if (sortIdx[i] == w->as.listView->selectedIdx) { displaySel = i; break; } } } else { - displaySel = w->as.listView.selectedIdx; + displaySel = w->as.listView->selectedIdx; } } @@ -665,62 +540,32 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { visibleRows = 1; } - if (key == (0x50 | 0x100)) { - // Down arrow - if (displaySel < rowCount - 1) { - displaySel++; - } else if (displaySel < 0) { - displaySel = 0; - } - } else if (key == (0x48 | 0x100)) { - // Up arrow - if (displaySel > 0) { - displaySel--; - } else if (displaySel < 0) { - displaySel = 0; - } - } else if (key == (0x47 | 0x100)) { - // Home - displaySel = 0; - } else if (key == (0x4F | 0x100)) { - // End - displaySel = rowCount - 1; - } else if (key == (0x51 | 0x100)) { - // Page Down - displaySel += visibleRows; + int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, visibleRows); - if (displaySel >= rowCount) { - displaySel = rowCount - 1; - } - } else if (key == (0x49 | 0x100)) { - // Page Up - displaySel -= visibleRows; - - if (displaySel < 0) { - displaySel = 0; - } - } else { + if (newDisplaySel < 0) { return; } + displaySel = newDisplaySel; + // Convert display row back to data row int32_t newDataRow = sortIdx ? sortIdx[displaySel] : displaySel; - if (newDataRow == w->as.listView.selectedIdx) { + if (newDataRow == w->as.listView->selectedIdx) { return; } - w->as.listView.selectedIdx = newDataRow; + w->as.listView->selectedIdx = newDataRow; // Update multi-select - if (multi && w->as.listView.selBits) { + if (multi && w->as.listView->selBits) { if (shift) { // Shift+arrow: range from anchor to new cursor (in data-row space) - memset(w->as.listView.selBits, 0, rowCount); + memset(w->as.listView->selBits, 0, rowCount); // Convert anchor to display row, then select display range mapped to data rows int32_t anchorDisplay = -1; - int32_t anchor = w->as.listView.anchorIdx; + int32_t anchor = w->as.listView->anchorIdx; if (sortIdx && anchor >= 0) { for (int32_t i = 0; i < rowCount; i++) { @@ -747,7 +592,7 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { for (int32_t i = lo; i <= hi; i++) { int32_t dr = sortIdx ? sortIdx[i] : i; - w->as.listView.selBits[dr] = 1; + w->as.listView->selBits[dr] = 1; } } // Plain arrow: just move cursor, leave selections untouched @@ -755,10 +600,10 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { // Scroll to keep selection visible (in display-row space) if (displaySel >= 0) { - if (displaySel < w->as.listView.scrollPos) { - w->as.listView.scrollPos = displaySel; - } else if (displaySel >= w->as.listView.scrollPos + visibleRows) { - w->as.listView.scrollPos = displaySel - visibleRows + 1; + if (displaySel < w->as.listView->scrollPos) { + w->as.listView->scrollPos = displaySel; + } else if (displaySel >= w->as.listView->scrollPos + visibleRows) { + w->as.listView->scrollPos = displaySel - visibleRows + 1; } } @@ -781,7 +626,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) const BitmapFontT *font = &ctx->font; // Resolve column widths if needed - if (hit->as.listView.totalColW == 0 && hit->as.listView.colCount > 0) { + if (hit->as.listView->totalColW == 0 && hit->as.listView->colCount > 0) { resolveColumnWidths(hit, font); } @@ -789,22 +634,22 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) int32_t innerH = hit->h - LISTVIEW_BORDER * 2 - headerH; int32_t innerW = hit->w - LISTVIEW_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; - int32_t totalColW = hit->as.listView.totalColW; - bool needVSb = (hit->as.listView.rowCount > visibleRows); + int32_t totalColW = hit->as.listView->totalColW; + bool needVSb = (hit->as.listView->rowCount > visibleRows); bool needHSb = false; if (needVSb) { - innerW -= LISTVIEW_SB_W; + innerW -= WGT_SB_W; } if (totalColW > innerW) { needHSb = true; - innerH -= LISTVIEW_SB_W; + innerH -= WGT_SB_W; visibleRows = innerH / font->charHeight; - if (!needVSb && hit->as.listView.rowCount > visibleRows) { + if (!needVSb && hit->as.listView->rowCount > visibleRows) { needVSb = true; - innerW -= LISTVIEW_SB_W; + innerW -= WGT_SB_W; } } @@ -813,7 +658,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) } // Clamp scroll positions - int32_t maxScrollV = hit->as.listView.rowCount - visibleRows; + int32_t maxScrollV = hit->as.listView->rowCount - visibleRows; int32_t maxScrollH = totalColW - innerW; if (maxScrollV < 0) { @@ -824,44 +669,32 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) maxScrollH = 0; } - hit->as.listView.scrollPos = clampInt(hit->as.listView.scrollPos, 0, maxScrollV); - hit->as.listView.scrollPosH = clampInt(hit->as.listView.scrollPosH, 0, maxScrollH); + hit->as.listView->scrollPos = clampInt(hit->as.listView->scrollPos, 0, maxScrollV); + hit->as.listView->scrollPosH = clampInt(hit->as.listView->scrollPosH, 0, maxScrollH); // Check vertical scrollbar if (needVSb) { - int32_t sbX = hit->x + hit->w - LISTVIEW_BORDER - LISTVIEW_SB_W; + int32_t sbX = hit->x + hit->w - LISTVIEW_BORDER - WGT_SB_W; int32_t sbY = hit->y + LISTVIEW_BORDER + headerH; - int32_t sbH = innerH; - if (vx >= sbX && vy >= sbY && vy < sbY + sbH) { - int32_t relY = vy - sbY; - int32_t trackLen = sbH - LISTVIEW_SB_W * 2; + if (vx >= sbX && vy >= sbY && vy < sbY + innerH) { + int32_t relY = vy - sbY; + ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, hit->as.listView->rowCount, visibleRows, hit->as.listView->scrollPos); - if (relY < LISTVIEW_SB_W) { - // Up arrow - if (hit->as.listView.scrollPos > 0) { - hit->as.listView.scrollPos--; + if (sh == ScrollHitArrowDecE) { + if (hit->as.listView->scrollPos > 0) { + hit->as.listView->scrollPos--; } - } else if (relY >= sbH - LISTVIEW_SB_W) { - // Down arrow - if (hit->as.listView.scrollPos < maxScrollV) { - hit->as.listView.scrollPos++; - } - } else if (trackLen > 0) { - // Track — page up/down - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, hit->as.listView.rowCount, visibleRows, hit->as.listView.scrollPos, &thumbPos, &thumbSize); - - int32_t trackRelY = relY - LISTVIEW_SB_W; - - if (trackRelY < thumbPos) { - hit->as.listView.scrollPos -= visibleRows; - hit->as.listView.scrollPos = clampInt(hit->as.listView.scrollPos, 0, maxScrollV); - } else if (trackRelY >= thumbPos + thumbSize) { - hit->as.listView.scrollPos += visibleRows; - hit->as.listView.scrollPos = clampInt(hit->as.listView.scrollPos, 0, maxScrollV); + } else if (sh == ScrollHitArrowIncE) { + if (hit->as.listView->scrollPos < maxScrollV) { + hit->as.listView->scrollPos++; } + } else if (sh == ScrollHitPageDecE) { + hit->as.listView->scrollPos -= visibleRows; + hit->as.listView->scrollPos = clampInt(hit->as.listView->scrollPos, 0, maxScrollV); + } else if (sh == ScrollHitPageIncE) { + hit->as.listView->scrollPos += visibleRows; + hit->as.listView->scrollPos = clampInt(hit->as.listView->scrollPos, 0, maxScrollV); } return; @@ -871,48 +704,37 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) // Check horizontal scrollbar if (needHSb) { int32_t sbX = hit->x + LISTVIEW_BORDER; - int32_t sbY = hit->y + hit->h - LISTVIEW_BORDER - LISTVIEW_SB_W; - int32_t sbW = innerW; + int32_t sbY = hit->y + hit->h - LISTVIEW_BORDER - WGT_SB_W; - if (vy >= sbY && vx >= sbX && vx < sbX + sbW) { + if (vy >= sbY && vx >= sbX && vx < sbX + innerW) { int32_t relX = vx - sbX; - int32_t trackLen = sbW - LISTVIEW_SB_W * 2; int32_t pageSize = innerW - font->charWidth; if (pageSize < font->charWidth) { pageSize = font->charWidth; } - if (relX < LISTVIEW_SB_W) { - // Left arrow - hit->as.listView.scrollPosH -= font->charWidth; - } else if (relX >= sbW - LISTVIEW_SB_W) { - // Right arrow - hit->as.listView.scrollPosH += font->charWidth; - } else if (trackLen > 0) { - // Track — page left/right - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalColW, innerW, hit->as.listView.scrollPosH, &thumbPos, &thumbSize); + ScrollHitE sh = widgetScrollbarHitTest(innerW, relX, totalColW, innerW, hit->as.listView->scrollPosH); - int32_t trackRelX = relX - LISTVIEW_SB_W; - - if (trackRelX < thumbPos) { - hit->as.listView.scrollPosH -= pageSize; - } else if (trackRelX >= thumbPos + thumbSize) { - hit->as.listView.scrollPosH += pageSize; - } + if (sh == ScrollHitArrowDecE) { + hit->as.listView->scrollPosH -= font->charWidth; + } else if (sh == ScrollHitArrowIncE) { + hit->as.listView->scrollPosH += font->charWidth; + } else if (sh == ScrollHitPageDecE) { + hit->as.listView->scrollPosH -= pageSize; + } else if (sh == ScrollHitPageIncE) { + hit->as.listView->scrollPosH += pageSize; } - hit->as.listView.scrollPosH = clampInt(hit->as.listView.scrollPosH, 0, maxScrollH); + hit->as.listView->scrollPosH = clampInt(hit->as.listView->scrollPosH, 0, maxScrollH); return; } } // Check dead corner (both scrollbars present) if (needVSb && needHSb) { - int32_t cornerX = hit->x + hit->w - LISTVIEW_BORDER - LISTVIEW_SB_W; - int32_t cornerY = hit->y + hit->h - LISTVIEW_BORDER - LISTVIEW_SB_W; + int32_t cornerX = hit->x + hit->w - LISTVIEW_BORDER - WGT_SB_W; + int32_t cornerY = hit->y + hit->h - LISTVIEW_BORDER - WGT_SB_W; if (vx >= cornerX && vy >= cornerY) { return; @@ -924,13 +746,13 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) if (vy >= headerTop && vy < headerTop + headerH) { // Check for column border resize (3px zone on each side of border) - int32_t colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH; + int32_t colX = hit->x + LISTVIEW_BORDER - hit->as.listView->scrollPosH; - for (int32_t c = 0; c < hit->as.listView.colCount; c++) { - int32_t cw = hit->as.listView.resolvedColW[c]; + for (int32_t c = 0; c < hit->as.listView->colCount; c++) { + int32_t cw = hit->as.listView->resolvedColW[c]; int32_t border = colX + cw; - if (vx >= border - 3 && vx <= border + 3 && c < hit->as.listView.colCount) { + if (vx >= border - 3 && vx <= border + 3 && c < hit->as.listView->colCount) { // Start column resize drag sResizeListView = hit; sResizeCol = c; @@ -943,29 +765,29 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) } // Not on a border — check for sort click (disabled when reorderable) - if (!hit->as.listView.reorderable) { - colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH; + if (!hit->as.listView->reorderable) { + colX = hit->x + LISTVIEW_BORDER - hit->as.listView->scrollPosH; - for (int32_t c = 0; c < hit->as.listView.colCount; c++) { - int32_t cw = hit->as.listView.resolvedColW[c]; + for (int32_t c = 0; c < hit->as.listView->colCount; c++) { + int32_t cw = hit->as.listView->resolvedColW[c]; if (vx >= colX && vx < colX + cw) { // Toggle sort direction for this column - if (hit->as.listView.sortCol == c) { - if (hit->as.listView.sortDir == ListViewSortAscE) { - hit->as.listView.sortDir = ListViewSortDescE; + if (hit->as.listView->sortCol == c) { + if (hit->as.listView->sortDir == ListViewSortAscE) { + hit->as.listView->sortDir = ListViewSortDescE; } else { - hit->as.listView.sortDir = ListViewSortAscE; + hit->as.listView->sortDir = ListViewSortAscE; } } else { - hit->as.listView.sortCol = c; - hit->as.listView.sortDir = ListViewSortAscE; + hit->as.listView->sortCol = c; + hit->as.listView->sortDir = ListViewSortAscE; } listViewBuildSortIndex(hit); - if (hit->as.listView.onHeaderClick) { - hit->as.listView.onHeaderClick(hit, c, hit->as.listView.sortDir); + if (hit->as.listView->onHeaderClick) { + hit->as.listView->onHeaderClick(hit, c, hit->as.listView->sortDir); } wgtInvalidatePaint(hit); @@ -988,31 +810,31 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) } int32_t clickedRow = relY / font->charHeight; - int32_t displayRow = hit->as.listView.scrollPos + clickedRow; + int32_t displayRow = hit->as.listView->scrollPos + clickedRow; - if (displayRow >= 0 && displayRow < hit->as.listView.rowCount) { - int32_t dataRow = hit->as.listView.sortIndex ? hit->as.listView.sortIndex[displayRow] : displayRow; - hit->as.listView.selectedIdx = dataRow; + if (displayRow >= 0 && displayRow < hit->as.listView->rowCount) { + int32_t dataRow = hit->as.listView->sortIndex ? hit->as.listView->sortIndex[displayRow] : displayRow; + hit->as.listView->selectedIdx = dataRow; - bool multi = hit->as.listView.multiSelect; + bool multi = hit->as.listView->multiSelect; bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0; bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0; - if (multi && hit->as.listView.selBits) { + if (multi && hit->as.listView->selBits) { if (ctrl) { // Ctrl+click: toggle item, update anchor - hit->as.listView.selBits[dataRow] ^= 1; - hit->as.listView.anchorIdx = dataRow; + hit->as.listView->selBits[dataRow] ^= 1; + hit->as.listView->anchorIdx = dataRow; } else if (shift) { // Shift+click: range from anchor to clicked (in display-row space) - memset(hit->as.listView.selBits, 0, hit->as.listView.rowCount); + memset(hit->as.listView->selBits, 0, hit->as.listView->rowCount); int32_t anchorDisplay = -1; - int32_t anchor = hit->as.listView.anchorIdx; + int32_t anchor = hit->as.listView->anchorIdx; - if (hit->as.listView.sortIndex && anchor >= 0) { - for (int32_t i = 0; i < hit->as.listView.rowCount; i++) { - if (hit->as.listView.sortIndex[i] == anchor) { + if (hit->as.listView->sortIndex && anchor >= 0) { + for (int32_t i = 0; i < hit->as.listView->rowCount; i++) { + if (hit->as.listView->sortIndex[i] == anchor) { anchorDisplay = i; break; } @@ -1028,19 +850,19 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) lo = 0; } - if (hi >= hit->as.listView.rowCount) { - hi = hit->as.listView.rowCount - 1; + if (hi >= hit->as.listView->rowCount) { + hi = hit->as.listView->rowCount - 1; } for (int32_t i = lo; i <= hi; i++) { - int32_t dr = hit->as.listView.sortIndex ? hit->as.listView.sortIndex[i] : i; - hit->as.listView.selBits[dr] = 1; + int32_t dr = hit->as.listView->sortIndex ? hit->as.listView->sortIndex[i] : i; + hit->as.listView->selBits[dr] = 1; } } else { // Plain click: select only this item, update anchor - memset(hit->as.listView.selBits, 0, hit->as.listView.rowCount); - hit->as.listView.selBits[dataRow] = 1; - hit->as.listView.anchorIdx = dataRow; + memset(hit->as.listView->selBits, 0, hit->as.listView->rowCount); + hit->as.listView->selBits[dataRow] = 1; + hit->as.listView->anchorIdx = dataRow; } } @@ -1053,9 +875,9 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) } // Initiate drag-reorder if enabled (not from modifier clicks) - if (hit->as.listView.reorderable && !shift && !ctrl) { - hit->as.listView.dragIdx = dataRow; - hit->as.listView.dropIdx = dataRow; + if (hit->as.listView->reorderable && !shift && !ctrl) { + hit->as.listView->dragIdx = dataRow; + hit->as.listView->dropIdx = dataRow; sDragReorder = hit; } } @@ -1073,7 +895,7 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; // Resolve column widths if needed - if (w->as.listView.totalColW == 0 && w->as.listView.colCount > 0) { + if (w->as.listView->totalColW == 0 && w->as.listView->colCount > 0) { resolveColumnWidths(w, font); } @@ -1081,22 +903,22 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t innerW = w->w - LISTVIEW_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; - int32_t totalColW = w->as.listView.totalColW; - bool needVSb = (w->as.listView.rowCount > visibleRows); + int32_t totalColW = w->as.listView->totalColW; + bool needVSb = (w->as.listView->rowCount > visibleRows); bool needHSb = false; if (needVSb) { - innerW -= LISTVIEW_SB_W; + innerW -= WGT_SB_W; } if (totalColW > innerW) { needHSb = true; - innerH -= LISTVIEW_SB_W; + innerH -= WGT_SB_W; visibleRows = innerH / font->charHeight; - if (!needVSb && w->as.listView.rowCount > visibleRows) { + if (!needVSb && w->as.listView->rowCount > visibleRows) { needVSb = true; - innerW -= LISTVIEW_SB_W; + innerW -= WGT_SB_W; } } @@ -1109,7 +931,7 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Clamp scroll positions - int32_t maxScrollV = w->as.listView.rowCount - visibleRows; + int32_t maxScrollV = w->as.listView->rowCount - visibleRows; int32_t maxScrollH = totalColW - innerW; if (maxScrollV < 0) { @@ -1120,12 +942,12 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit maxScrollH = 0; } - w->as.listView.scrollPos = clampInt(w->as.listView.scrollPos, 0, maxScrollV); - w->as.listView.scrollPosH = clampInt(w->as.listView.scrollPosH, 0, maxScrollH); + w->as.listView->scrollPos = clampInt(w->as.listView->scrollPos, 0, maxScrollV); + w->as.listView->scrollPosH = clampInt(w->as.listView->scrollPosH, 0, maxScrollH); int32_t baseX = w->x + LISTVIEW_BORDER; int32_t baseY = w->y + LISTVIEW_BORDER; - int32_t colCount = w->as.listView.colCount; + int32_t colCount = w->as.listView->colCount; // ---- Draw column headers ---- { @@ -1136,11 +958,11 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t oldClipH = d->clipH; setClipRect(d, baseX, baseY, innerW, headerH); - int32_t hdrX = baseX - w->as.listView.scrollPosH; + int32_t hdrX = baseX - w->as.listView->scrollPosH; BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1); for (int32_t c = 0; c < colCount; c++) { - int32_t cw = w->as.listView.resolvedColW[c]; + int32_t cw = w->as.listView->resolvedColW[c]; // Draw raised button for header drawBevel(d, ops, hdrX, baseY, cw, headerH, &hdrBevel); @@ -1151,12 +973,12 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t availTextW = cw - LISTVIEW_PAD * 2; // Reserve space for sort indicator if this is the sort column - if (c == w->as.listView.sortCol && w->as.listView.sortDir != ListViewSortNoneE) { + if (c == w->as.listView->sortCol && w->as.listView->sortDir != ListViewSortNoneE) { availTextW -= LISTVIEW_SORT_W; } - if (w->as.listView.cols[c].title) { - int32_t titleLen = (int32_t)strlen(w->as.listView.cols[c].title); + if (w->as.listView->cols[c].title) { + int32_t titleLen = (int32_t)strlen(w->as.listView->cols[c].title); int32_t titleW = titleLen * font->charWidth; if (titleW > availTextW) { @@ -1164,16 +986,16 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } if (titleLen > 0) { - drawTextN(d, ops, font, textX, textY, w->as.listView.cols[c].title, titleLen, colors->contentFg, colors->windowFace, true); + drawTextN(d, ops, font, textX, textY, w->as.listView->cols[c].title, titleLen, colors->contentFg, colors->windowFace, true); } } // Sort indicator - if (c == w->as.listView.sortCol && w->as.listView.sortDir != ListViewSortNoneE) { + if (c == w->as.listView->sortCol && w->as.listView->sortDir != ListViewSortNoneE) { int32_t cx = hdrX + cw - LISTVIEW_SORT_W / 2 - LISTVIEW_PAD; int32_t cy = baseY + headerH / 2; - if (w->as.listView.sortDir == ListViewSortAscE) { + if (w->as.listView->sortDir == ListViewSortAscE) { // Up triangle (ascending) for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg); @@ -1208,16 +1030,16 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t oldClipH = d->clipH; setClipRect(d, baseX, dataY, innerW, innerH); - int32_t scrollPos = w->as.listView.scrollPos; - int32_t *sortIdx = w->as.listView.sortIndex; + int32_t scrollPos = w->as.listView->scrollPos; + int32_t *sortIdx = w->as.listView->sortIndex; // Fill entire data area background first rectFill(d, ops, baseX, dataY, innerW, innerH, bg); - bool multi = w->as.listView.multiSelect; - uint8_t *selBits = w->as.listView.selBits; + bool multi = w->as.listView->multiSelect; + uint8_t *selBits = w->as.listView->selBits; - for (int32_t i = 0; i < visibleRows && (scrollPos + i) < w->as.listView.rowCount; i++) { + for (int32_t i = 0; i < visibleRows && (scrollPos + i) < w->as.listView->rowCount; i++) { int32_t displayRow = scrollPos + i; int32_t dataRow = sortIdx ? sortIdx[displayRow] : displayRow; int32_t iy = dataY + i * font->charHeight; @@ -1229,7 +1051,7 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit if (multi && selBits) { selected = selBits[dataRow] != 0; } else { - selected = (dataRow == w->as.listView.selectedIdx); + selected = (dataRow == w->as.listView->selectedIdx); } if (selected) { @@ -1239,11 +1061,11 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } // Draw each cell - int32_t cellX = baseX - w->as.listView.scrollPosH; + int32_t cellX = baseX - w->as.listView->scrollPosH; for (int32_t c = 0; c < colCount; c++) { - int32_t cw = w->as.listView.resolvedColW[c]; - const char *cell = w->as.listView.cellData[dataRow * colCount + c]; + int32_t cw = w->as.listView->resolvedColW[c]; + const char *cell = w->as.listView->cellData[dataRow * colCount + c]; if (cell) { int32_t cellLen = (int32_t)strlen(cell); @@ -1259,11 +1081,11 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t tx = cellX + LISTVIEW_PAD; - if (w->as.listView.cols[c].align == ListViewAlignRightE) { + if (w->as.listView->cols[c].align == ListViewAlignRightE) { int32_t renderedW = cellLen * font->charWidth; int32_t availW = cw - LISTVIEW_PAD * 2; tx = cellX + LISTVIEW_PAD + (availW - renderedW); - } else if (w->as.listView.cols[c].align == ListViewAlignCenterE) { + } else if (w->as.listView->cols[c].align == ListViewAlignCenterE) { int32_t renderedW = cellLen * font->charWidth; int32_t availW = cw - LISTVIEW_PAD * 2; tx = cellX + LISTVIEW_PAD + (availW - renderedW) / 2; @@ -1278,15 +1100,15 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } // Draw cursor focus rect in multi-select mode (on top of text) - if (multi && dataRow == w->as.listView.selectedIdx && w->focused) { + if (multi && dataRow == w->as.listView->selectedIdx && w->focused) { drawFocusRect(d, ops, baseX, iy, innerW, font->charHeight, fg); } } // Draw drag-reorder insertion indicator - if (w->as.listView.reorderable && w->as.listView.dragIdx >= 0 && w->as.listView.dropIdx >= 0) { - int32_t drop = w->as.listView.dropIdx; - int32_t lineY = dataY + (drop - w->as.listView.scrollPos) * font->charHeight; + if (w->as.listView->reorderable && w->as.listView->dragIdx >= 0 && w->as.listView->dropIdx >= 0) { + int32_t drop = w->as.listView->dropIdx; + int32_t lineY = dataY + (drop - w->as.listView->scrollPos) * font->charHeight; if (lineY >= dataY && lineY <= dataY + innerH) { drawHLine(d, ops, baseX, lineY, innerW, fg); @@ -1299,19 +1121,19 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // ---- Draw scrollbars ---- if (needVSb) { - int32_t sbX = w->x + w->w - LISTVIEW_BORDER - LISTVIEW_SB_W; + int32_t sbX = w->x + w->w - LISTVIEW_BORDER - WGT_SB_W; int32_t sbY = w->y + LISTVIEW_BORDER + headerH; - drawListViewVScrollbar(w, d, ops, colors, sbX, sbY, innerH, w->as.listView.rowCount, visibleRows); + widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, w->as.listView->rowCount, visibleRows, w->as.listView->scrollPos); } if (needHSb) { int32_t sbX = w->x + LISTVIEW_BORDER; - int32_t sbY = w->y + w->h - LISTVIEW_BORDER - LISTVIEW_SB_W; - drawListViewHScrollbar(w, d, ops, colors, sbX, sbY, innerW, totalColW, innerW); + int32_t sbY = w->y + w->h - LISTVIEW_BORDER - WGT_SB_W; + widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalColW, innerW, w->as.listView->scrollPosH); // Fill dead corner when both scrollbars present if (needVSb) { - rectFill(d, ops, sbX + innerW, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, colors->windowFace); + rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace); } } diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index 9f5e6aa..1e00ed9 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -149,6 +149,43 @@ void wgtDestroy(WidgetT *w) { } +// ============================================================ +// widgetHashName — djb2 hash for widget name lookup +// ============================================================ + +static uint32_t widgetHashName(const char *s) { + uint32_t h = 5381; + + while (*s) { + h = ((h << 5) + h) + (uint8_t)*s; + s++; + } + + return h; +} + + +// ============================================================ +// wgtFindByHash — recursive search with hash fast-reject +// ============================================================ + +static WidgetT *wgtFindByHash(WidgetT *root, const char *name, uint32_t hash) { + if (root->nameHash == hash && root->name[0] && strcmp(root->name, name) == 0) { + return root; + } + + for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { + WidgetT *found = wgtFindByHash(c, name, hash); + + if (found) { + return found; + } + } + + return NULL; +} + + // ============================================================ // wgtFind // ============================================================ @@ -158,19 +195,24 @@ WidgetT *wgtFind(WidgetT *root, const char *name) { return NULL; } - if (root->name[0] && strcmp(root->name, name) == 0) { - return root; + uint32_t hash = widgetHashName(name); + + return wgtFindByHash(root, name, hash); +} + + +// ============================================================ +// wgtSetName +// ============================================================ + +void wgtSetName(WidgetT *w, const char *name) { + if (!w || !name) { + return; } - for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { - WidgetT *found = wgtFind(c, name); - - if (found) { - return found; - } - } - - return NULL; + strncpy(w->name, name, MAX_WIDGET_NAME - 1); + w->name[MAX_WIDGET_NAME - 1] = '\0'; + w->nameHash = widgetHashName(w->name); } @@ -306,8 +348,16 @@ void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT // wgtSetDebugLayout // ============================================================ -void wgtSetDebugLayout(bool enabled) { +void wgtSetDebugLayout(AppContextT *ctx, bool enabled) { sDebugLayout = enabled; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->widgetRoot) { + wgtInvalidate(win->widgetRoot); + } + } } diff --git a/dvx/widgets/widgetProgressBar.c b/dvx/widgets/widgetProgressBar.c index 5f2047e..714d42b 100644 --- a/dvx/widgets/widgetProgressBar.c +++ b/dvx/widgets/widgetProgressBar.c @@ -42,9 +42,7 @@ WidgetT *wgtProgressBarV(WidgetT *parent) { // ============================================================ int32_t wgtProgressBarGetValue(const WidgetT *w) { - if (!w || w->type != WidgetProgressBarE) { - return 0; - } + VALIDATE_WIDGET(w, WidgetProgressBarE, 0); return w->as.progressBar.value; } @@ -55,9 +53,7 @@ int32_t wgtProgressBarGetValue(const WidgetT *w) { // ============================================================ void wgtProgressBarSetValue(WidgetT *w, int32_t value) { - if (!w || w->type != WidgetProgressBarE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetProgressBarE); if (value < 0) { value = 0; diff --git a/dvx/widgets/widgetScrollbar.c b/dvx/widgets/widgetScrollbar.c new file mode 100644 index 0000000..9ebbc1c --- /dev/null +++ b/dvx/widgets/widgetScrollbar.c @@ -0,0 +1,152 @@ +// widgetScrollbar.c — Shared scrollbar painting and hit-testing + +#include "widgetInternal.h" + + +// ============================================================ +// widgetDrawScrollbarH +// ============================================================ + +void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) { + if (sbW < WGT_SB_W * 3) { + return; + } + + // Trough background + BevelStyleT troughBevel = BEVEL_TROUGH(colors); + drawBevel(d, ops, sbX, sbY, sbW, WGT_SB_W, &troughBevel); + + // Left arrow button + BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); + drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel); + + // Left arrow triangle + { + int32_t cx = sbX + WGT_SB_W / 2; + int32_t cy = sbY + WGT_SB_W / 2; + uint32_t fg = colors->contentFg; + + for (int32_t i = 0; i < 4; i++) { + drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); + } + } + + // Right arrow button + int32_t rightX = sbX + sbW - WGT_SB_W; + drawBevel(d, ops, rightX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel); + + // Right arrow triangle + { + int32_t cx = rightX + WGT_SB_W / 2; + int32_t cy = sbY + WGT_SB_W / 2; + uint32_t fg = colors->contentFg; + + for (int32_t i = 0; i < 4; i++) { + drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); + } + } + + // Thumb + int32_t trackLen = sbW - WGT_SB_W * 2; + + if (trackLen > 0 && totalSize > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize); + + drawBevel(d, ops, sbX + WGT_SB_W + thumbPos, sbY, thumbSize, WGT_SB_W, &btnBevel); + } +} + + +// ============================================================ +// widgetDrawScrollbarV +// ============================================================ + +void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) { + if (sbH < WGT_SB_W * 3) { + return; + } + + // Trough background + BevelStyleT troughBevel = BEVEL_TROUGH(colors); + drawBevel(d, ops, sbX, sbY, WGT_SB_W, sbH, &troughBevel); + + // Up arrow button + BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); + drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel); + + // Up arrow triangle + { + int32_t cx = sbX + WGT_SB_W / 2; + int32_t cy = sbY + WGT_SB_W / 2; + uint32_t fg = colors->contentFg; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); + } + } + + // Down arrow button + int32_t downY = sbY + sbH - WGT_SB_W; + drawBevel(d, ops, sbX, downY, WGT_SB_W, WGT_SB_W, &btnBevel); + + // Down arrow triangle + { + int32_t cx = sbX + WGT_SB_W / 2; + int32_t cy = downY + WGT_SB_W / 2; + uint32_t fg = colors->contentFg; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); + } + } + + // Thumb + int32_t trackLen = sbH - WGT_SB_W * 2; + + if (trackLen > 0 && totalSize > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize); + + drawBevel(d, ops, sbX, sbY + WGT_SB_W + thumbPos, WGT_SB_W, thumbSize, &btnBevel); + } +} + + +// ============================================================ +// widgetScrollbarHitTest +// ============================================================ + +ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) { + if (relPos < WGT_SB_W) { + return ScrollHitArrowDecE; + } + + if (relPos >= sbLen - WGT_SB_W) { + return ScrollHitArrowIncE; + } + + int32_t trackLen = sbLen - WGT_SB_W * 2; + + if (trackLen > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize); + + int32_t trackRel = relPos - WGT_SB_W; + + if (trackRel < thumbPos) { + return ScrollHitPageDecE; + } + + if (trackRel >= thumbPos + thumbSize) { + return ScrollHitPageIncE; + } + + return ScrollHitThumbE; + } + + return ScrollHitNoneE; +} diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c index 4818542..a5244df 100644 --- a/dvx/widgets/widgetSlider.c +++ b/dvx/widgets/widgetSlider.c @@ -27,9 +27,7 @@ WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) { // ============================================================ int32_t wgtSliderGetValue(const WidgetT *w) { - if (!w || w->type != WidgetSliderE) { - return 0; - } + VALIDATE_WIDGET(w, WidgetSliderE, 0); return w->as.slider.value; } @@ -40,9 +38,7 @@ int32_t wgtSliderGetValue(const WidgetT *w) { // ============================================================ void wgtSliderSetValue(WidgetT *w, int32_t value) { - if (!w || w->type != WidgetSliderE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetSliderE); if (value < w->as.slider.minValue) { value = w->as.slider.minValue; diff --git a/dvx/widgets/widgetSpinner.c b/dvx/widgets/widgetSpinner.c index 4c990e3..9d885c8 100644 --- a/dvx/widgets/widgetSpinner.c +++ b/dvx/widgets/widgetSpinner.c @@ -458,9 +458,7 @@ WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t ste // ============================================================ int32_t wgtSpinnerGetValue(const WidgetT *w) { - if (!w || w->type != WidgetSpinnerE) { - return 0; - } + VALIDATE_WIDGET(w, WidgetSpinnerE, 0); return w->as.spinner.value; } @@ -471,9 +469,7 @@ int32_t wgtSpinnerGetValue(const WidgetT *w) { // ============================================================ void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal) { - if (!w || w->type != WidgetSpinnerE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetSpinnerE); w->as.spinner.minValue = minVal; w->as.spinner.maxValue = maxVal; @@ -486,9 +482,7 @@ void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal) { // ============================================================ void wgtSpinnerSetStep(WidgetT *w, int32_t step) { - if (!w || w->type != WidgetSpinnerE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetSpinnerE); w->as.spinner.step = step > 0 ? step : 1; } @@ -499,9 +493,7 @@ void wgtSpinnerSetStep(WidgetT *w, int32_t step) { // ============================================================ void wgtSpinnerSetValue(WidgetT *w, int32_t value) { - if (!w || w->type != WidgetSpinnerE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetSpinnerE); w->as.spinner.value = value; spinnerClampAndFormat(w); diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index 34d96d7..b6dac67 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -137,9 +137,7 @@ WidgetT *wgtTabControl(WidgetT *parent) { // ============================================================ int32_t wgtTabControlGetActive(const WidgetT *w) { - if (!w || w->type != WidgetTabControlE) { - return 0; - } + VALIDATE_WIDGET(w, WidgetTabControlE, 0); return w->as.tabControl.activeTab; } @@ -150,9 +148,7 @@ int32_t wgtTabControlGetActive(const WidgetT *w) { // ============================================================ void wgtTabControlSetActive(WidgetT *w, int32_t idx) { - if (!w || w->type != WidgetTabControlE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTabControlE); w->as.tabControl.activeTab = idx; } diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index 9357fa3..095712d 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -158,23 +158,23 @@ static bool clearSelectionOnWidget(WidgetT *w) { w->as.comboBox.selStart = -1; w->as.comboBox.selEnd = -1; } else if (w->type == WidgetAnsiTermE) { - if (w->as.ansiTerm.selStartLine >= 0 && - (w->as.ansiTerm.selStartLine != w->as.ansiTerm.selEndLine || - w->as.ansiTerm.selStartCol != w->as.ansiTerm.selEndCol)) { - 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; + if (w->as.ansiTerm->selStartLine >= 0 && + (w->as.ansiTerm->selStartLine != w->as.ansiTerm->selEndLine || + w->as.ansiTerm->selStartCol != w->as.ansiTerm->selEndCol)) { + 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.selStartCol = -1; - w->as.ansiTerm.selEndLine = -1; - w->as.ansiTerm.selEndCol = -1; - w->as.ansiTerm.selecting = false; + 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 false; @@ -1789,8 +1789,8 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { } else if (w->type == WidgetAnsiTermE) { int32_t baseX = w->x + 2; // ANSI_BORDER int32_t baseY = w->y + 2; - int32_t cols = w->as.ansiTerm.cols; - int32_t rows = w->as.ansiTerm.rows; + int32_t cols = w->as.ansiTerm->cols; + int32_t rows = w->as.ansiTerm->rows; int32_t clickRow = (vy - baseY) / font->charHeight; int32_t clickCol = (vx - baseX) / font->charWidth; @@ -1811,9 +1811,9 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { clickCol = cols; } - w->as.ansiTerm.selEndLine = w->as.ansiTerm.scrollPos + clickRow; - w->as.ansiTerm.selEndCol = clickCol; - w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; + w->as.ansiTerm->selEndLine = w->as.ansiTerm->scrollPos + clickRow; + w->as.ansiTerm->selEndCol = clickCol; + w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; } } diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index f97227c..685d0fe 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -9,8 +9,6 @@ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font); static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth); static void clearAllSelections(WidgetT *parent); -static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb); -static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH); static WidgetT *firstVisibleItem(WidgetT *treeView); static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth); static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView); @@ -98,131 +96,6 @@ static void clearAllSelections(WidgetT *parent) { } -// ============================================================ -// drawTreeHScrollbar -// ============================================================ - -static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb) { - int32_t sbX = w->x + TREE_BORDER; - int32_t sbY = w->y + w->h - TREE_BORDER - TREE_SB_W; - int32_t sbW = innerW; - - if (sbW < TREE_SB_W * 3) { - return; - } - - // Trough background - BevelStyleT troughBevel = BEVEL_TROUGH(colors); - drawBevel(d, ops, sbX, sbY, sbW, TREE_SB_W, &troughBevel); - - // Left arrow button - BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); - drawBevel(d, ops, sbX, sbY, TREE_SB_W, TREE_SB_W, &btnBevel); - - // Left arrow triangle - { - int32_t cx = sbX + TREE_SB_W / 2; - int32_t cy = sbY + TREE_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); - } - } - - // Right arrow button - int32_t rightX = sbX + sbW - TREE_SB_W; - drawBevel(d, ops, rightX, sbY, TREE_SB_W, TREE_SB_W, &btnBevel); - - // Right arrow triangle - { - int32_t cx = rightX + TREE_SB_W / 2; - int32_t cy = sbY + TREE_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); - } - } - - // Thumb - int32_t trackLen = sbW - TREE_SB_W * 2; - - if (trackLen > 0 && totalW > 0) { - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalW, innerW, w->as.treeView.scrollPosH, &thumbPos, &thumbSize); - - drawBevel(d, ops, sbX + TREE_SB_W + thumbPos, sbY, thumbSize, TREE_SB_W, &btnBevel); - } - - // Fill dead corner when both scrollbars present - if (hasVSb) { - rectFill(d, ops, sbX + sbW, sbY, TREE_SB_W, TREE_SB_W, colors->windowFace); - } -} - - -// ============================================================ -// drawTreeVScrollbar -// ============================================================ - -static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH) { - int32_t sbX = w->x + w->w - TREE_BORDER - TREE_SB_W; - int32_t sbY = w->y + TREE_BORDER; - int32_t sbH = innerH; - - if (sbH < TREE_SB_W * 3) { - return; - } - - // Trough background - BevelStyleT troughBevel = BEVEL_TROUGH(colors); - drawBevel(d, ops, sbX, sbY, TREE_SB_W, sbH, &troughBevel); - - // Up arrow button - BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); - drawBevel(d, ops, sbX, sbY, TREE_SB_W, TREE_SB_W, &btnBevel); - - // Up arrow triangle - { - int32_t cx = sbX + TREE_SB_W / 2; - int32_t cy = sbY + TREE_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); - } - } - - // Down arrow button - int32_t downY = sbY + sbH - TREE_SB_W; - drawBevel(d, ops, sbX, downY, TREE_SB_W, TREE_SB_W, &btnBevel); - - // Down arrow triangle - { - int32_t cx = sbX + TREE_SB_W / 2; - int32_t cy = downY + TREE_SB_W / 2; - uint32_t fg = colors->contentFg; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); - } - } - - // Thumb - int32_t trackLen = sbH - TREE_SB_W * 2; - - if (trackLen > 0 && totalH > 0) { - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalH, innerH, w->as.treeView.scrollPos, &thumbPos, &thumbSize); - - drawBevel(d, ops, sbX, sbY + TREE_SB_W + thumbPos, TREE_SB_W, thumbSize, &btnBevel); - } -} - - // ============================================================ // firstVisibleItem // ============================================================ @@ -542,7 +415,7 @@ static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t // V scrollbar reduces available width — may trigger H scrollbar if (needVSb) { - innerW -= TREE_SB_W; + innerW -= WGT_SB_W; if (!needHSb && totalW > innerW) { needHSb = true; @@ -551,11 +424,11 @@ static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t // H scrollbar reduces available height — may trigger V scrollbar if (needHSb) { - innerH -= TREE_SB_W; + innerH -= WGT_SB_W; if (!needVSb && totalH > innerH) { needVSb = true; - innerW -= TREE_SB_W; + innerW -= WGT_SB_W; } } @@ -684,9 +557,7 @@ void widgetTreeItemSetText(WidgetT *w, const char *text) { // ============================================================ bool wgtTreeItemIsExpanded(const WidgetT *w) { - if (!w || w->type != WidgetTreeItemE) { - return false; - } + VALIDATE_WIDGET(w, WidgetTreeItemE, false); return w->as.treeItem.expanded; } @@ -697,9 +568,7 @@ bool wgtTreeItemIsExpanded(const WidgetT *w) { // ============================================================ void wgtTreeItemSetExpanded(WidgetT *w, bool expanded) { - if (!w || w->type != WidgetTreeItemE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTreeItemE); w->as.treeItem.expanded = expanded; } @@ -725,9 +594,7 @@ WidgetT *wgtTreeView(WidgetT *parent) { // ============================================================ WidgetT *wgtTreeViewGetSelected(const WidgetT *w) { - if (!w || w->type != WidgetTreeViewE) { - return NULL; - } + VALIDATE_WIDGET(w, WidgetTreeViewE, NULL); return w->as.treeView.selectedItem; } @@ -738,9 +605,7 @@ WidgetT *wgtTreeViewGetSelected(const WidgetT *w) { // ============================================================ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) { - if (!w || w->type != WidgetTreeViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); w->as.treeView.selectedItem = item; @@ -757,9 +622,7 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) { // ============================================================ void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi) { - if (!w || w->type != WidgetTreeViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); w->as.treeView.multiSelect = multi; } @@ -770,9 +633,7 @@ void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi) { // ============================================================ void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable) { - if (!w || w->type != WidgetTreeViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); w->as.treeView.reorderable = reorderable; } @@ -795,9 +656,7 @@ WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) { // ============================================================ bool wgtTreeItemIsSelected(const WidgetT *w) { - if (!w || w->type != WidgetTreeItemE) { - return false; - } + VALIDATE_WIDGET(w, WidgetTreeItemE, false); return w->as.treeItem.selected; } @@ -808,9 +667,7 @@ bool wgtTreeItemIsSelected(const WidgetT *w) { // ============================================================ void wgtTreeItemSetSelected(WidgetT *w, bool selected) { - if (!w || w->type != WidgetTreeItemE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTreeItemE); w->as.treeItem.selected = selected; } @@ -823,7 +680,7 @@ void wgtTreeItemSetSelected(WidgetT *w, bool selected) { void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t minContentW = TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP + 6 * font->charWidth; - w->calcMinW = minContentW + TREE_BORDER * 2 + TREE_SB_W; + w->calcMinW = minContentW + TREE_BORDER * 2 + WGT_SB_W; w->calcMinH = TREE_MIN_ROWS * font->charHeight + TREE_BORDER * 2; } @@ -833,9 +690,7 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { - if (!w || w->type != WidgetTreeViewE) { - return; - } + VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); bool multi = w->as.treeView.multiSelect; bool shift = (mod & KEY_MOD_SHIFT) != 0; @@ -1053,41 +908,28 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) // Check if click is on the vertical scrollbar if (needVSb) { - int32_t sbX = hit->x + hit->w - TREE_BORDER - TREE_SB_W; + int32_t sbX = hit->x + hit->w - TREE_BORDER - WGT_SB_W; if (vx >= sbX && vy < hit->y + TREE_BORDER + innerH) { - int32_t sbY = hit->y + TREE_BORDER; - int32_t sbH = innerH; - int32_t relY = vy - sbY; - int32_t trackLen = sbH - TREE_SB_W * 2; - int32_t pageSize = innerH - font->charHeight; + int32_t relY = vy - (hit->y + TREE_BORDER); + int32_t pageSize = innerH - font->charHeight; if (pageSize < font->charHeight) { pageSize = font->charHeight; } - if (relY < TREE_SB_W) { - // Up arrow — scroll up one row + ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, totalH, innerH, hit->as.treeView.scrollPos); + + if (sh == ScrollHitArrowDecE) { hit->as.treeView.scrollPos -= font->charHeight; - } else if (relY >= sbH - TREE_SB_W) { - // Down arrow — scroll down one row + } else if (sh == ScrollHitArrowIncE) { hit->as.treeView.scrollPos += font->charHeight; - } else if (trackLen > 0) { - // Track area — page up/down based on thumb position - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalH, innerH, hit->as.treeView.scrollPos, &thumbPos, &thumbSize); - - int32_t trackRelY = relY - TREE_SB_W; - - if (trackRelY < thumbPos) { - hit->as.treeView.scrollPos -= pageSize; - } else if (trackRelY >= thumbPos + thumbSize) { - hit->as.treeView.scrollPos += pageSize; - } + } else if (sh == ScrollHitPageDecE) { + hit->as.treeView.scrollPos -= pageSize; + } else if (sh == ScrollHitPageIncE) { + hit->as.treeView.scrollPos += pageSize; } - // Clamp after scroll hit->as.treeView.scrollPos = clampInt(hit->as.treeView.scrollPos, 0, maxScrollV); return; } @@ -1095,41 +937,28 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) // Check if click is on the horizontal scrollbar if (needHSb) { - int32_t sbY = hit->y + hit->h - TREE_BORDER - TREE_SB_W; + int32_t sbY = hit->y + hit->h - TREE_BORDER - WGT_SB_W; if (vy >= sbY && vx < hit->x + TREE_BORDER + innerW) { - int32_t sbX = hit->x + TREE_BORDER; - int32_t sbW = innerW; - int32_t relX = vx - sbX; - int32_t trackLen = sbW - TREE_SB_W * 2; - int32_t pageSize = innerW - font->charWidth; + int32_t relX = vx - (hit->x + TREE_BORDER); + int32_t pageSize = innerW - font->charWidth; if (pageSize < font->charWidth) { pageSize = font->charWidth; } - if (relX < TREE_SB_W) { - // Left arrow — scroll left + ScrollHitE sh = widgetScrollbarHitTest(innerW, relX, totalW, innerW, hit->as.treeView.scrollPosH); + + if (sh == ScrollHitArrowDecE) { hit->as.treeView.scrollPosH -= font->charWidth; - } else if (relX >= sbW - TREE_SB_W) { - // Right arrow — scroll right + } else if (sh == ScrollHitArrowIncE) { hit->as.treeView.scrollPosH += font->charWidth; - } else if (trackLen > 0) { - // Track area — page left/right based on thumb position - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalW, innerW, hit->as.treeView.scrollPosH, &thumbPos, &thumbSize); - - int32_t trackRelX = relX - TREE_SB_W; - - if (trackRelX < thumbPos) { - hit->as.treeView.scrollPosH -= pageSize; - } else if (trackRelX >= thumbPos + thumbSize) { - hit->as.treeView.scrollPosH += pageSize; - } + } else if (sh == ScrollHitPageDecE) { + hit->as.treeView.scrollPosH -= pageSize; + } else if (sh == ScrollHitPageIncE) { + hit->as.treeView.scrollPosH += pageSize; } - // Clamp after scroll hit->as.treeView.scrollPosH = clampInt(hit->as.treeView.scrollPosH, 0, maxScrollH); return; } @@ -1137,8 +966,8 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) // Click in dead corner (both scrollbars present) — ignore if (needVSb && needHSb) { - int32_t cornerX = hit->x + hit->w - TREE_BORDER - TREE_SB_W; - int32_t cornerY = hit->y + hit->h - TREE_BORDER - TREE_SB_W; + int32_t cornerX = hit->x + hit->w - TREE_BORDER - WGT_SB_W; + int32_t cornerY = hit->y + hit->h - TREE_BORDER - WGT_SB_W; if (vx >= cornerX && vy >= cornerY) { return; @@ -1313,11 +1142,20 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Draw scrollbars if (needVSb) { - drawTreeVScrollbar(w, d, ops, colors, totalH, innerH); + int32_t sbX = w->x + w->w - TREE_BORDER - WGT_SB_W; + int32_t sbY = w->y + TREE_BORDER; + widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, totalH, innerH, w->as.treeView.scrollPos); } if (needHSb) { - drawTreeHScrollbar(w, d, ops, colors, totalW, innerW, needVSb); + int32_t sbX = w->x + TREE_BORDER; + int32_t sbY = w->y + w->h - TREE_BORDER - WGT_SB_W; + widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalW, innerW, w->as.treeView.scrollPosH); + + // Fill dead corner when both scrollbars present + if (needVSb) { + rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace); + } } if (w->focused) { diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index ba9bb77..ea8de4d 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -32,6 +32,7 @@ #define CMD_VIEW_SIZE_SMALL 307 #define CMD_VIEW_SIZE_MED 308 #define CMD_VIEW_SIZE_LARGE 309 +#define CMD_VIEW_DEBUG_LYT 310 #define CMD_WIN_CASCADE 350 #define CMD_WIN_TILE_H 351 #define CMD_WIN_TILE_V 352 @@ -209,6 +210,13 @@ static void onMenuCb(WindowT *win, int32_t menuId) { setupControlsWindow(ctx); break; + case CMD_VIEW_DEBUG_LYT: { + static bool sDebugLyt = false; + sDebugLyt = !sDebugLyt; + wgtSetDebugLayout(ctx, sDebugLyt); + break; + } + case CMD_WIN_CASCADE: dvxCascadeWindows(ctx); break; @@ -623,21 +631,21 @@ static void setupControlsWindow(AppContextT *ctx) { uint8_t *newData = loadBmpPixels(ctx, "new.bmp", &imgW, &imgH, &imgPitch); if (newData) { WidgetT *btnNew = wgtImageButton(tb, newData, imgW, imgH, imgPitch); - strncpy(btnNew->name, "New", MAX_WIDGET_NAME); + wgtSetName(btnNew, "New"); btnNew->onClick = onToolbarClick; } uint8_t *openData = loadBmpPixels(ctx, "open.bmp", &imgW, &imgH, &imgPitch); if (openData) { WidgetT *btnOpen = wgtImageButton(tb, openData, imgW, imgH, imgPitch); - strncpy(btnOpen->name, "Open", MAX_WIDGET_NAME); + wgtSetName(btnOpen, "Open"); btnOpen->onClick = onToolbarClick; } uint8_t *saveData = loadBmpPixels(ctx, "save.bmp", &imgW, &imgH, &imgPitch); if (saveData) { WidgetT *btnSave = wgtImageButton(tb, saveData, imgW, imgH, imgPitch); - strncpy(btnSave->name, "Save", MAX_WIDGET_NAME); + wgtSetName(btnSave, "Save"); btnSave->onClick = onToolbarClick; } @@ -814,7 +822,7 @@ static void setupControlsWindow(AppContextT *ctx) { WidgetT *sb = wgtStatusBar(root); WidgetT *sbLabel = wgtLabel(sb, "Ready"); sbLabel->weight = 100; - strncpy(sbLabel->name, "advStatus", MAX_WIDGET_NAME); + wgtSetName(sbLabel, "advStatus"); wgtLabel(sb, "Line 1, Col 1"); wgtInvalidate(root); @@ -878,6 +886,9 @@ static void setupMainWindow(AppContextT *ctx) { wmAddMenuSeparator(zoomMenu); wmAddMenuItem(zoomMenu, "&Fit to Window", CMD_VIEW_ZOOM_FIT); } + + wmAddMenuSeparator(viewMenu); + wmAddMenuCheckItem(viewMenu, "&Debug Layout", CMD_VIEW_DEBUG_LYT, false); } MenuT *winMenu = wmAddMenu(bar, "&Window"); @@ -1052,7 +1063,7 @@ static void setupWidgetDemo(AppContextT *ctx) { // Status label at top WidgetT *status = wgtLabel(root, "Ready."); - strncpy(status->name, "status", MAX_WIDGET_NAME); + wgtSetName(status, "status"); wgtHSeparator(root);