From 97530513b83af6791846e92af09be5a09d3fee3f Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 15 Mar 2026 18:39:35 -0500 Subject: [PATCH] More optimizations. Word-at-a-time keyboard selection added. --- dvx/dvxDraw.c | 154 ++++++++++++++ dvx/dvxDraw.h | 5 + dvx/widgets/widgetComboBox.c | 25 ++- dvx/widgets/widgetTextInput.c | 390 +++++++++++++++++++++++++--------- 4 files changed, 466 insertions(+), 108 deletions(-) diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index af7d3d8..35afb88 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -266,6 +266,160 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 } +// ============================================================ +// drawTextN +// ============================================================ +// +// Renders exactly 'count' characters from a buffer in one pass. +// Same idea as drawTermRow but for uniform fg/bg text runs. +// Avoids per-character function call overhead, redundant clip +// calculation, and spanFill startup costs. + +void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque) { + if (count <= 0) { + return; + } + + int32_t cw = font->charWidth; + int32_t ch = font->charHeight; + int32_t bpp = ops->bytesPerPixel; + int32_t pitch = d->pitch; + + // Row-level clip: reject if entirely outside vertically + int32_t clipX1 = d->clipX; + int32_t clipX2 = d->clipX + d->clipW; + int32_t clipY1 = d->clipY; + int32_t clipY2 = d->clipY + d->clipH; + + if (y + ch <= clipY1 || y >= clipY2) { + return; + } + + int32_t totalW = count * cw; + + if (x + totalW <= clipX1 || x >= clipX2) { + return; + } + + // Vertical clip for glyph scanlines + int32_t rowStart = 0; + int32_t rowEnd = ch; + if (y < clipY1) { rowStart = clipY1 - y; } + if (y + ch > clipY2) { rowEnd = clipY2 - y; } + + // Horizontal clip: find first and last visible column (character index) + int32_t firstChar = 0; + int32_t lastChar = count; + + if (x < clipX1) { + firstChar = (clipX1 - x) / cw; + } + + if (x + totalW > clipX2) { + lastChar = (clipX2 - x + cw - 1) / cw; + if (lastChar > count) { lastChar = count; } + } + + // Per-pixel clip for partially visible edge characters + int32_t edgeColStart = 0; + + if (x + firstChar * cw < clipX1) { + edgeColStart = clipX1 - (x + firstChar * cw); + } + + if (opaque) { + // Opaque: fill background for the entire visible span once per scanline, + // then overlay foreground glyph pixels + int32_t fillX1 = x + firstChar * cw; + int32_t fillX2 = x + lastChar * cw; + + if (fillX1 < clipX1) { fillX1 = clipX1; } + if (fillX2 > clipX2) { fillX2 = clipX2; } + + int32_t fillW = fillX2 - fillX1; + + if (fillW > 0) { + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t *dst = d->backBuf + (y + row) * pitch + fillX1 * bpp; + ops->spanFill(dst, bg, fillW); + } + } + } + + // Render glyph foreground pixels + for (int32_t ci = firstChar; ci < lastChar; ci++) { + int32_t cx = x + ci * cw; + + int32_t cStart = 0; + int32_t cEnd = cw; + + if (ci == firstChar) { + cStart = edgeColStart; + } + + if (cx + cw > clipX2) { + cEnd = clipX2 - cx; + } + + int32_t idx = (uint8_t)text[ci] - font->firstChar; + const uint8_t *glyph = NULL; + + if (idx >= 0 && idx < font->numChars) { + glyph = font->glyphData + idx * ch; + } + + if (!glyph) { + continue; + } + + if (bpp == 2) { + uint16_t fg16 = (uint16_t)fg; + + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t bits = glyph[row]; + if (bits == 0) { continue; } + + uint16_t *dst = (uint16_t *)(d->backBuf + (y + row) * pitch + cx * 2); + + for (int32_t p = cStart; p < cEnd; p++) { + if (bits & sGlyphBit[p]) { + dst[p] = fg16; + } + } + } + } else if (bpp == 4) { + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t bits = glyph[row]; + if (bits == 0) { continue; } + + uint32_t *dst = (uint32_t *)(d->backBuf + (y + row) * pitch + cx * 4); + + for (int32_t p = cStart; p < cEnd; p++) { + if (bits & sGlyphBit[p]) { + dst[p] = fg; + } + } + } + } else { + uint8_t fg8 = (uint8_t)fg; + + for (int32_t row = rowStart; row < rowEnd; row++) { + uint8_t bits = glyph[row]; + if (bits == 0) { continue; } + + uint8_t *dst = d->backBuf + (y + row) * pitch + cx; + + for (int32_t p = cStart; p < cEnd; p++) { + if (bits & sGlyphBit[p]) { + dst[p] = fg8; + } + } + } + } + } +} + + // ============================================================ // drawFocusRect // ============================================================ diff --git a/dvx/dvxDraw.h b/dvx/dvxDraw.h index 9d5c4d1..1b90850 100644 --- a/dvx/dvxDraw.h +++ b/dvx/dvxDraw.h @@ -22,6 +22,11 @@ int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // Draw a null-terminated string void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque); +// Draw exactly 'count' characters from a buffer (not null-terminated). +// Much faster than calling drawChar per character: computes clip once, +// fills background in bulk, then overlays glyph foreground pixels. +void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque); + // Measure text width in pixels int32_t textWidth(const BitmapFontT *font, const char *text); diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 3b3096e..7fe4d99 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -340,18 +340,25 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit selHi = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selEnd : w->as.comboBox.selStart; } - for (int32_t i = 0; i < len; i++) { - int32_t charIdx = off + i; - uint32_t cfgc = fg; - uint32_t cbgc = bg; + // Draw up to 3 runs: before selection, selection, after selection + int32_t visSelLo = selLo - off; + int32_t visSelHi = selHi - off; - if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) { - cfgc = colors->menuHighlightFg; - cbgc = colors->menuHighlightBg; + if (visSelLo < 0) { visSelLo = 0; } + if (visSelHi > len) { visSelHi = len; } + + if (selLo >= 0 && visSelLo < visSelHi) { + if (visSelLo > 0) { + drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, visSelLo, fg, bg, true); } - drawChar(d, ops, font, textX + i * font->charWidth, textY, - w->as.comboBox.buf[charIdx], cfgc, cbgc, true); + drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, w->as.comboBox.buf + off + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); + + if (visSelHi < len) { + drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, w->as.comboBox.buf + off + visSelHi, len - visSelHi, fg, bg, true); + } + } else { + drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, len, fg, bg, true); } // Draw cursor diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index d49fef5..5a6a72e 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -35,6 +35,8 @@ static int32_t textAreaMaxLineLen(const char *buf, int32_t len); static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col); static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); +static int32_t wordBoundaryLeft(const char *buf, int32_t pos); +static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); // ============================================================ // Shared clipboard @@ -122,65 +124,60 @@ int32_t wordEnd(const char *buf, int32_t len, int32_t pos) { // Clear selection on all text widgets except 'except' // ============================================================ -static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) { - if (!root) { - return false; - } +// Track the widget that last had an active selection so we can +// clear it in O(1) instead of walking every widget in every window. +static WidgetT *sLastSelectedWidget = NULL; - bool cleared = false; - WidgetT *stack[64]; - int32_t top = 0; - stack[top++] = root; - while (top > 0) { - WidgetT *w = stack[--top]; - - if (w != except) { - if (w->type == WidgetTextInputE) { - if (w->as.textInput.selStart != w->as.textInput.selEnd) { - cleared = true; - } - - w->as.textInput.selStart = -1; - w->as.textInput.selEnd = -1; - } else if (w->type == WidgetTextAreaE) { - if (w->as.textArea.selAnchor != w->as.textArea.selCursor) { - cleared = true; - } - - w->as.textArea.selAnchor = -1; - w->as.textArea.selCursor = -1; - } else if (w->type == WidgetComboBoxE) { - if (w->as.comboBox.selStart != w->as.comboBox.selEnd) { - cleared = true; - } - - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; - } 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)) { - cleared = true; - w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; - } - - w->as.ansiTerm.selStartLine = -1; - w->as.ansiTerm.selStartCol = -1; - w->as.ansiTerm.selEndLine = -1; - w->as.ansiTerm.selEndCol = -1; - w->as.ansiTerm.selecting = false; - } +static bool clearSelectionOnWidget(WidgetT *w) { + if (w->type == WidgetTextInputE) { + if (w->as.textInput.selStart != w->as.textInput.selEnd) { + w->as.textInput.selStart = -1; + w->as.textInput.selEnd = -1; + return true; } - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (top < 64) { - stack[top++] = c; - } + w->as.textInput.selStart = -1; + w->as.textInput.selEnd = -1; + } else if (w->type == WidgetTextAreaE) { + if (w->as.textArea.selAnchor != w->as.textArea.selCursor) { + w->as.textArea.selAnchor = -1; + w->as.textArea.selCursor = -1; + return true; } + + w->as.textArea.selAnchor = -1; + w->as.textArea.selCursor = -1; + } else if (w->type == WidgetComboBoxE) { + if (w->as.comboBox.selStart != w->as.comboBox.selEnd) { + w->as.comboBox.selStart = -1; + w->as.comboBox.selEnd = -1; + return true; + } + + 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; + 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; } - return cleared; + return false; } @@ -189,24 +186,45 @@ void clearOtherSelections(WidgetT *except) { return; } + WidgetT *prev = sLastSelectedWidget; + sLastSelectedWidget = except; + + if (!prev || prev == except) { + return; + } + + // Verify the widget is still alive (its window still in the stack) + WindowT *prevWin = prev->window; + + if (!prevWin) { + return; + } + AppContextT *ctx = (AppContextT *)except->window->widgetRoot->userData; if (!ctx) { return; } - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; + bool found = false; - if (win && win->widgetRoot) { - if (clearSelectionsInTree(win->widgetRoot, except) && win != except->window) { - RectT fullRect = {0, 0, win->contentW, win->contentH}; - widgetOnPaint(win, &fullRect); - win->contentDirty = true; - dvxInvalidateWindow(ctx, win); - } + for (int32_t i = 0; i < ctx->stack.count; i++) { + if (ctx->stack.windows[i] == prevWin) { + found = true; + break; } } + + if (!found) { + return; + } + + if (clearSelectionOnWidget(prev) && prevWin != except->window) { + RectT fullRect = {0, 0, prevWin->contentW, prevWin->contentH}; + widgetOnPaint(prevWin, &fullRect); + prevWin->contentDirty = true; + dvxInvalidateWindow(ctx, prevWin); + } } @@ -257,6 +275,58 @@ static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, } +// ============================================================ +// wordBoundaryLeft +// ============================================================ +// +// From position pos, skip non-word chars left, then skip word chars left. +// Returns the position at the start of the word (or 0). + +static int32_t wordBoundaryLeft(const char *buf, int32_t pos) { + if (pos <= 0) { + return 0; + } + + // Skip non-word characters + while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') { + pos--; + } + + // Skip word characters + while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) { + pos--; + } + + return pos; +} + + +// ============================================================ +// wordBoundaryRight +// ============================================================ +// +// From position pos, skip word chars right, then skip non-word chars right. +// Returns the position at the end of the word (or len). + +static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) { + if (pos >= len) { + return len; + } + + // Skip word characters + while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) { + pos++; + } + + // Skip non-word characters + while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') { + pos++; + } + + return pos; +} + + // ============================================================ // wgtTextArea // ============================================================ @@ -1217,6 +1287,32 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { return; } + // Ctrl+Left — word left + if (key == (0x73 | 0x100)) { + SEL_BEGIN(); + int32_t off = CUR_OFF(); + int32_t newOff = wordBoundaryLeft(buf, off); + textAreaOffToRowCol(buf, newOff, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidatePaint(w); + return; + } + + // Ctrl+Right — word right + if (key == (0x74 | 0x100)) { + SEL_BEGIN(); + int32_t off = CUR_OFF(); + int32_t newOff = wordBoundaryRight(buf, *pLen, off); + textAreaOffToRowCol(buf, newOff, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidatePaint(w); + return; + } + // Up arrow if (key == (0x48 | 0x100)) { SEL_BEGIN(); @@ -1791,35 +1887,70 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } int32_t drawY = textY + i * font->charHeight; - for (int32_t j = 0; j < visCols; j++) { - int32_t col = w->as.textArea.scrollCol + j; - int32_t charOff = lineOff + col; - int32_t drawX = textX + j * font->charWidth; + // Visible range within line + int32_t scrollCol = w->as.textArea.scrollCol; + int32_t visStart = scrollCol; + int32_t visEnd = scrollCol + visCols; + int32_t textEnd = lineL; // chars in this line - uint32_t cfgc = fg; - uint32_t cbgc = bg; + // Clamp visible range to actual line content for text drawing + int32_t drawStart = visStart < textEnd ? visStart : textEnd; + int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd; - // Check selection — past end of line, test the newline byte - // instead of lineOff+col (which aliases into subsequent lines) - bool inSel = false; - if (selLo >= 0) { - if (col < lineL) { - inSel = (charOff >= selLo && charOff < selHi); - } else { - int32_t nlOff = lineOff + lineL; - inSel = (nlOff >= selLo && nlOff < selHi); + // Determine selection intersection with this line + int32_t lineSelLo = -1; + int32_t lineSelHi = -1; + + if (selLo >= 0) { + // Selection range in column-space for this line + if (selLo < lineOff + lineL + 1 && selHi > lineOff) { + lineSelLo = selLo - lineOff; + lineSelHi = selHi - lineOff; + if (lineSelLo < 0) { lineSelLo = 0; } + // selHi can extend past line (newline selected) + } + } + + if (lineSelLo >= 0 && lineSelLo < lineSelHi) { + // Clamp selection to visible columns for text runs + int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo; + int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd; + + if (vSelLo > vSelHi) { vSelLo = vSelHi; } + + // Before selection + if (drawStart < vSelLo) { + drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, vSelLo - drawStart, fg, bg, true); + } + + // Selection (text portion) + if (vSelLo < vSelHi) { + drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); + } + + // After selection + if (vSelHi < drawEnd) { + drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, buf + lineOff + vSelHi, drawEnd - vSelHi, fg, bg, true); + } + + // Past end of text: fill selected area with highlight bg + int32_t nlOff = lineOff + lineL; + bool pastEolSelected = (nlOff >= selLo && nlOff < selHi); + + if (pastEolSelected && drawEnd < visEnd) { + int32_t selPastStart = drawEnd < lineSelLo ? lineSelLo : drawEnd; + int32_t selPastEnd = visEnd; + + if (selPastStart < visStart) { selPastStart = visStart; } + + if (selPastStart < selPastEnd) { + rectFill(d, ops, textX + (selPastStart - scrollCol) * font->charWidth, drawY, (selPastEnd - selPastStart) * font->charWidth, font->charHeight, colors->menuHighlightBg); } } - - if (inSel) { - cfgc = colors->menuHighlightFg; - cbgc = colors->menuHighlightBg; - } - - if (col < lineL) { - drawChar(d, ops, font, drawX, drawY, buf[charOff], cfgc, cbgc, true); - } else if (inSel) { - rectFill(d, ops, drawX, drawY, font->charWidth, font->charHeight, cbgc); + } else { + // No selection on this line — single run + if (drawStart < drawEnd) { + drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true); } } @@ -2127,24 +2258,39 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi bool isPassword = (w->as.textInput.inputMode == InputPasswordE); - for (int32_t i = 0; i < len; i++) { - int32_t charIdx = off + i; - uint32_t cfgc = fg; - uint32_t cbgc = bg; + // Build display buffer (password masking) + char dispBuf[256]; + int32_t dispLen = len > 255 ? 255 : len; - if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) { - cfgc = colors->menuHighlightFg; - cbgc = colors->menuHighlightBg; + if (isPassword) { + memset(dispBuf, '\xF9', dispLen); // CP437 bullet + } else { + memcpy(dispBuf, w->as.textInput.buf + off, dispLen); + } + + // Draw up to 3 runs: before selection, selection, after selection + int32_t visSelLo = selLo - off; + int32_t visSelHi = selHi - off; + + if (visSelLo < 0) { visSelLo = 0; } + if (visSelHi > dispLen) { visSelHi = dispLen; } + + if (selLo >= 0 && visSelLo < visSelHi) { + // Before selection + if (visSelLo > 0) { + drawTextN(d, ops, font, textX, textY, dispBuf, visSelLo, fg, bg, true); } - char displayCh = w->as.textInput.buf[charIdx]; + // Selection + drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, dispBuf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); - if (isPassword) { - displayCh = '\xF9'; // CP437 bullet + // After selection + if (visSelHi < dispLen) { + drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, dispBuf + visSelHi, dispLen - visSelHi, fg, bg, true); } - - drawChar(d, ops, font, textX + i * font->charWidth, textY, - displayCh, cfgc, cbgc, true); + } else { + // No selection — single run + drawTextN(d, ops, font, textX, textY, dispBuf, dispLen, fg, bg, true); } // Draw cursor @@ -2427,6 +2573,52 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_ (*pCursor)++; } } + } else if (key == (0x73 | 0x100)) { + // Ctrl+Left — word left + int32_t newPos = wordBoundaryLeft(buf, *pCursor); + + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = newPos; + *pSelEnd = newPos; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = newPos; + } + } else if (key == (0x74 | 0x100)) { + // Ctrl+Right — word right + int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor); + + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = newPos; + *pSelEnd = newPos; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = newPos; + } } else if (key == (0x47 | 0x100)) { // Home if (shift && pSelStart && pSelEnd) {