// widgetSpinner.c — Spinner (numeric up/down) widget #include "widgetInternal.h" #include #include #define SPINNER_BTN_W 14 #define SPINNER_BORDER 2 #define SPINNER_PAD 3 // ============================================================ // Prototypes // ============================================================ static void spinnerClampAndFormat(WidgetT *w); static void spinnerCommitEdit(WidgetT *w); static void spinnerFormat(WidgetT *w); static void spinnerStartEdit(WidgetT *w); // ============================================================ // spinnerClampAndFormat // ============================================================ static void spinnerClampAndFormat(WidgetT *w) { if (w->as.spinner.value < w->as.spinner.minValue) { w->as.spinner.value = w->as.spinner.minValue; } if (w->as.spinner.value > w->as.spinner.maxValue) { w->as.spinner.value = w->as.spinner.maxValue; } spinnerFormat(w); } // ============================================================ // spinnerCommitEdit // ============================================================ static void spinnerCommitEdit(WidgetT *w) { if (!w->as.spinner.editing) { return; } w->as.spinner.editing = false; w->as.spinner.buf[w->as.spinner.len] = '\0'; int32_t val = (int32_t)strtol(w->as.spinner.buf, NULL, 10); w->as.spinner.value = val; spinnerClampAndFormat(w); } // ============================================================ // spinnerFormat // ============================================================ static void spinnerFormat(WidgetT *w) { w->as.spinner.len = snprintf(w->as.spinner.buf, sizeof(w->as.spinner.buf), "%d", (int)w->as.spinner.value); w->as.spinner.cursorPos = w->as.spinner.len; w->as.spinner.scrollOff = 0; w->as.spinner.selStart = -1; w->as.spinner.selEnd = -1; } // ============================================================ // spinnerStartEdit // ============================================================ static void spinnerStartEdit(WidgetT *w) { if (!w->as.spinner.editing) { w->as.spinner.editing = true; // Snapshot for undo memcpy(w->as.spinner.undoBuf, w->as.spinner.buf, sizeof(w->as.spinner.buf)); w->as.spinner.undoLen = w->as.spinner.len; w->as.spinner.undoCursor = w->as.spinner.cursorPos; } } // ============================================================ // widgetSpinnerCalcMinSize // ============================================================ void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = font->charWidth * 6 + SPINNER_PAD * 2 + SPINNER_BORDER * 2 + SPINNER_BTN_W; w->calcMinH = font->charHeight + SPINNER_PAD * 2 + SPINNER_BORDER * 2; } // ============================================================ // widgetSpinnerGetText // ============================================================ const char *widgetSpinnerGetText(const WidgetT *w) { return w->as.spinner.buf; } // ============================================================ // widgetSpinnerOnKey // ============================================================ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) { int32_t step = w->as.spinner.step; // Up arrow — increment if (key == (0x48 | 0x100)) { spinnerCommitEdit(w); w->as.spinner.value += step; spinnerClampAndFormat(w); if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); return; } // Down arrow — decrement if (key == (0x50 | 0x100)) { spinnerCommitEdit(w); w->as.spinner.value -= step; spinnerClampAndFormat(w); if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); return; } // Page Up — increment by step * 10 if (key == (0x49 | 0x100)) { spinnerCommitEdit(w); w->as.spinner.value += step * 10; spinnerClampAndFormat(w); if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); return; } // Page Down — decrement by step * 10 if (key == (0x51 | 0x100)) { spinnerCommitEdit(w); w->as.spinner.value -= step * 10; spinnerClampAndFormat(w); if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); return; } // Enter — commit edit if (key == '\r' || key == '\n') { if (w->as.spinner.editing) { spinnerCommitEdit(w); if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); } return; } // Escape — cancel edit, revert to current value if (key == 27) { if (w->as.spinner.editing) { w->as.spinner.editing = false; spinnerFormat(w); wgtInvalidatePaint(w); } return; } // Filter: only allow digits, minus, and control keys through to text editor bool isDigit = (key >= '0' && key <= '9'); bool isMinus = (key == '-'); bool isControl = (key < 0x20) || (key & 0x100); if (!isDigit && !isMinus && !isControl) { return; } // Minus only at position 0 (and only if min is negative) if (isMinus && (w->as.spinner.cursorPos != 0 || w->as.spinner.minValue >= 0)) { return; } // Enter edit mode on first text-modifying key if (isDigit || isMinus || key == 8 || key == 127 || key == (0x53 | 0x100)) { spinnerStartEdit(w); } // Delegate to shared text editing logic (handles cursor movement, // selection, cut/copy/paste, undo/redo, backspace, delete, etc.) widgetTextEditOnKey(w, key, mod, w->as.spinner.buf, (int32_t)sizeof(w->as.spinner.buf), &w->as.spinner.len, &w->as.spinner.cursorPos, &w->as.spinner.scrollOff, &w->as.spinner.selStart, &w->as.spinner.selEnd, w->as.spinner.undoBuf, &w->as.spinner.undoLen, &w->as.spinner.undoCursor); wgtInvalidatePaint(w); } // ============================================================ // widgetSpinnerOnMouse // ============================================================ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { hit->focused = true; int32_t btnX = hit->x + hit->w - SPINNER_BORDER - SPINNER_BTN_W; int32_t midY = hit->y + hit->h / 2; if (vx >= btnX) { // Click on button area spinnerCommitEdit(hit); if (vy < midY) { hit->as.spinner.value += hit->as.spinner.step; } else { hit->as.spinner.value -= hit->as.spinner.step; } spinnerClampAndFormat(hit); if (hit->onChange) { hit->onChange(hit); } } else { // Click on text area AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t textX = hit->x + SPINNER_BORDER + SPINNER_PAD; int32_t relX = vx - textX + hit->as.spinner.scrollOff * font->charWidth; int32_t pos = relX / font->charWidth; if (pos < 0) { pos = 0; } if (pos > hit->as.spinner.len) { pos = hit->as.spinner.len; } int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 2) { // Double/triple-click: select all text hit->as.spinner.selStart = 0; hit->as.spinner.selEnd = hit->as.spinner.len; hit->as.spinner.cursorPos = hit->as.spinner.len; } else { // Single click: place cursor hit->as.spinner.cursorPos = pos; hit->as.spinner.selStart = -1; hit->as.spinner.selEnd = -1; } spinnerStartEdit(hit); } wgtInvalidatePaint(hit); } // ============================================================ // widgetSpinnerPaint // ============================================================ void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; int32_t btnW = SPINNER_BTN_W; int32_t btnX = w->x + w->w - SPINNER_BORDER - btnW; // Sunken border around entire widget BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = bg; bevel.width = SPINNER_BORDER; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Text area int32_t textX = w->x + SPINNER_BORDER + SPINNER_PAD; int32_t textY = w->y + (w->h - font->charHeight) / 2; int32_t textW = btnX - (w->x + SPINNER_BORDER) - SPINNER_PAD * 2; int32_t maxChars = textW / font->charWidth; if (maxChars < 0) { maxChars = 0; } // Scroll to keep cursor visible if (w->as.spinner.cursorPos < w->as.spinner.scrollOff) { w->as.spinner.scrollOff = w->as.spinner.cursorPos; } else if (w->as.spinner.cursorPos > w->as.spinner.scrollOff + maxChars) { w->as.spinner.scrollOff = w->as.spinner.cursorPos - maxChars; } int32_t off = w->as.spinner.scrollOff; int32_t len = w->as.spinner.len - off; if (len > maxChars) { len = maxChars; } if (len < 0) { len = 0; } // Selection range int32_t selLo = -1; int32_t selHi = -1; if (w->as.spinner.selStart >= 0 && w->as.spinner.selEnd >= 0 && w->as.spinner.selStart != w->as.spinner.selEnd) { selLo = w->as.spinner.selStart < w->as.spinner.selEnd ? w->as.spinner.selStart : w->as.spinner.selEnd; selHi = w->as.spinner.selStart > w->as.spinner.selEnd ? w->as.spinner.selStart : w->as.spinner.selEnd; } // Draw text with selection highlighting (3-run approach like textInput) int32_t visSelLo = selLo - off; int32_t visSelHi = selHi - off; if (visSelLo < 0) { visSelLo = 0; } if (visSelHi > len) { visSelHi = len; } if (selLo >= 0 && visSelLo < visSelHi) { // Before selection if (visSelLo > 0) { drawTextN(d, ops, font, textX, textY, &w->as.spinner.buf[off], visSelLo, fg, bg, true); } // Selection drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, &w->as.spinner.buf[off + visSelLo], visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); // After selection if (visSelHi < len) { drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, &w->as.spinner.buf[off + visSelHi], len - visSelHi, fg, bg, true); } } else if (len > 0) { drawTextN(d, ops, font, textX, textY, &w->as.spinner.buf[off], len, fg, bg, true); } // Cursor if (w->focused) { int32_t curX = textX + (w->as.spinner.cursorPos - off) * font->charWidth; if (curX >= w->x + SPINNER_BORDER && curX < btnX - SPINNER_PAD) { drawVLine(d, ops, curX, textY, font->charHeight, fg); } } // Up button (top half) int32_t btnTopH = w->h / 2; int32_t btnBotH = w->h - btnTopH; BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); drawBevel(d, ops, btnX, w->y, btnW + SPINNER_BORDER, btnTopH, &btnBevel); // Up arrow triangle { int32_t cx = btnX + btnW / 2; int32_t cy = w->y + btnTopH / 2; for (int32_t i = 0; i < 3; i++) { drawHLine(d, ops, cx - i, cy - 1 + i, 1 + i * 2, fg); } } // Down button (bottom half) drawBevel(d, ops, btnX, w->y + btnTopH, btnW + SPINNER_BORDER, btnBotH, &btnBevel); // Down arrow triangle { int32_t cx = btnX + btnW / 2; int32_t cy = w->y + btnTopH + btnBotH / 2; for (int32_t i = 0; i < 3; i++) { drawHLine(d, ops, cx - i, cy + 1 - i, 1 + i * 2, fg); } } // Focus rect around entire widget if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } } // ============================================================ // widgetSpinnerSetText // ============================================================ void widgetSpinnerSetText(WidgetT *w, const char *text) { int32_t val = (int32_t)strtol(text, NULL, 10); w->as.spinner.value = val; spinnerClampAndFormat(w); } // ============================================================ // wgtSpinner // ============================================================ WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step) { WidgetT *w = widgetAlloc(parent, WidgetSpinnerE); if (w) { w->as.spinner.minValue = minVal; w->as.spinner.maxValue = maxVal; w->as.spinner.step = step > 0 ? step : 1; w->as.spinner.value = minVal; w->as.spinner.editing = false; w->as.spinner.selStart = -1; w->as.spinner.selEnd = -1; w->as.spinner.undoLen = 0; w->as.spinner.undoCursor = 0; spinnerFormat(w); } return w; } // ============================================================ // wgtSpinnerGetValue // ============================================================ int32_t wgtSpinnerGetValue(const WidgetT *w) { VALIDATE_WIDGET(w, WidgetSpinnerE, 0); return w->as.spinner.value; } // ============================================================ // wgtSpinnerSetRange // ============================================================ void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal) { VALIDATE_WIDGET_VOID(w, WidgetSpinnerE); w->as.spinner.minValue = minVal; w->as.spinner.maxValue = maxVal; spinnerClampAndFormat(w); } // ============================================================ // wgtSpinnerSetStep // ============================================================ void wgtSpinnerSetStep(WidgetT *w, int32_t step) { VALIDATE_WIDGET_VOID(w, WidgetSpinnerE); w->as.spinner.step = step > 0 ? step : 1; } // ============================================================ // wgtSpinnerSetValue // ============================================================ void wgtSpinnerSetValue(WidgetT *w, int32_t value) { VALIDATE_WIDGET_VOID(w, WidgetSpinnerE); w->as.spinner.value = value; spinnerClampAndFormat(w); }