840 lines
25 KiB
C
840 lines
25 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetSpinner.c -- Spinner (numeric up/down) widget
|
|
//
|
|
// A hybrid widget combining a single-line text editor with up/down
|
|
// arrow buttons for numeric value entry. The user can either click
|
|
// the arrows, use Up/Down keys, or type a number directly.
|
|
//
|
|
// Supports two numeric modes:
|
|
// Integer mode (default): int32_t value, step, min, max
|
|
// Real mode (useReal=true): double value, step, min, max with
|
|
// configurable decimal places
|
|
//
|
|
// Design: the widget has two modes -- display mode (showing the
|
|
// formatted value) and edit mode (allowing free-form text input).
|
|
// Edit mode is entered on the first text-modifying keystroke and
|
|
// committed on Enter or when arrows are clicked. Escape cancels
|
|
// the edit and reverts to the pre-edit value. This two-mode design
|
|
// keeps the display clean (always showing a properly formatted
|
|
// number) while still allowing direct keyboard entry.
|
|
//
|
|
// The text editing delegates to widgetTextEditOnKey() -- the same
|
|
// shared single-line editing logic used by TextInput. This gives
|
|
// the spinner cursor movement, selection, cut/copy/paste, and undo
|
|
// for free. Input validation filters non-numeric characters before
|
|
// they reach the editor.
|
|
//
|
|
// Undo uses a single-level swap buffer (same as TextInput): the
|
|
// current state is copied to undoBuf before each mutation, and
|
|
// Ctrl+Z swaps current<->undo. This is simpler and cheaper than
|
|
// a multi-level undo stack for the small buffers involved.
|
|
//
|
|
// Rendering: sunken border enclosing the text area + two stacked
|
|
// raised-bevel arrow buttons on the right. The buttons extend to
|
|
// the widget's right edge (including the border width) so they
|
|
// look like they're part of the border chrome. The up/down buttons
|
|
// split the widget height evenly.
|
|
|
|
#include "dvxWgtP.h"
|
|
#include "../texthelp/textHelp.h"
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define SPINNER_BTN_W 14
|
|
#define SPINNER_BORDER 2
|
|
#define SPINNER_PAD 3
|
|
#define SPINNER_BUF_SIZE 32
|
|
#define SPINNER_DEFAULT_DECIMALS 2
|
|
|
|
// ============================================================
|
|
// Per-instance data
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
// Integer mode
|
|
int32_t value;
|
|
int32_t minValue;
|
|
int32_t maxValue;
|
|
int32_t step;
|
|
|
|
// Real mode
|
|
double realValue;
|
|
double realMin;
|
|
double realMax;
|
|
double realStep;
|
|
int32_t decimals;
|
|
bool useReal;
|
|
|
|
// Text editing state
|
|
char buf[SPINNER_BUF_SIZE];
|
|
int32_t len;
|
|
int32_t cursorPos;
|
|
int32_t scrollOff;
|
|
int32_t selStart;
|
|
int32_t selEnd;
|
|
char undoBuf[SPINNER_BUF_SIZE];
|
|
int32_t undoLen;
|
|
int32_t undoCursor;
|
|
bool editing;
|
|
} SpinnerDataT;
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static bool spinnerAllowMinus(const SpinnerDataT *d);
|
|
static void spinnerClampAndFormat(SpinnerDataT *d);
|
|
static void spinnerCommitEdit(SpinnerDataT *d);
|
|
static void spinnerFormat(SpinnerDataT *d);
|
|
static void spinnerStartEdit(SpinnerDataT *d);
|
|
static bool spinnerValidateBuffer(const SpinnerDataT *d);
|
|
static void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void widgetSpinnerDestroy(WidgetT *w);
|
|
static const char *widgetSpinnerGetText(const WidgetT *w);
|
|
static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod);
|
|
static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
|
static void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void widgetSpinnerSetText(WidgetT *w, const char *text);
|
|
|
|
|
|
// ============================================================
|
|
// spinnerAllowMinus -- can negative values be entered?
|
|
// ============================================================
|
|
|
|
static bool spinnerAllowMinus(const SpinnerDataT *d) {
|
|
if (d->useReal) {
|
|
return d->realMin < 0.0;
|
|
}
|
|
|
|
return d->minValue < 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// spinnerClampAndFormat
|
|
// ============================================================
|
|
|
|
static void spinnerClampAndFormat(SpinnerDataT *d) {
|
|
if (d->useReal) {
|
|
if (d->realValue < d->realMin) {
|
|
d->realValue = d->realMin;
|
|
}
|
|
|
|
if (d->realValue > d->realMax) {
|
|
d->realValue = d->realMax;
|
|
}
|
|
} else {
|
|
if (d->value < d->minValue) {
|
|
d->value = d->minValue;
|
|
}
|
|
|
|
if (d->value > d->maxValue) {
|
|
d->value = d->maxValue;
|
|
}
|
|
}
|
|
|
|
spinnerFormat(d);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// spinnerCommitEdit
|
|
// ============================================================
|
|
|
|
static void spinnerCommitEdit(SpinnerDataT *d) {
|
|
if (!d->editing) {
|
|
return;
|
|
}
|
|
|
|
d->editing = false;
|
|
d->buf[d->len] = '\0';
|
|
|
|
if (d->useReal) {
|
|
d->realValue = strtod(d->buf, NULL);
|
|
} else {
|
|
d->value = (int32_t)strtol(d->buf, NULL, 10);
|
|
}
|
|
|
|
spinnerClampAndFormat(d);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// spinnerFormat
|
|
// ============================================================
|
|
|
|
// Format always places the cursor at the end and resets scroll/selection.
|
|
// This is called after any value change to synchronize the text buffer
|
|
// with the numeric value. The cursor-at-end position matches user
|
|
// expectation after arrow-key increment/decrement.
|
|
static void spinnerFormat(SpinnerDataT *d) {
|
|
if (d->useReal) {
|
|
d->len = snprintf(d->buf, sizeof(d->buf), "%.*f", (int)d->decimals, d->realValue);
|
|
} else {
|
|
d->len = snprintf(d->buf, sizeof(d->buf), "%d", (int)d->value);
|
|
}
|
|
|
|
d->cursorPos = d->len;
|
|
d->scrollOff = 0;
|
|
d->selStart = -1;
|
|
d->selEnd = -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// spinnerStartEdit
|
|
// ============================================================
|
|
|
|
// Entering edit mode snapshots the buffer for undo so the user can
|
|
// revert to the pre-edit formatted value. The snapshot is only taken
|
|
// on the transition to editing, not on every keystroke, so repeated
|
|
// typing within one edit session can be undone all at once.
|
|
static void spinnerStartEdit(SpinnerDataT *d) {
|
|
if (!d->editing) {
|
|
d->editing = true;
|
|
|
|
// Snapshot for undo
|
|
memcpy(d->undoBuf, d->buf, sizeof(d->buf));
|
|
d->undoLen = d->len;
|
|
d->undoCursor = d->cursorPos;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// spinnerValidateBuffer -- check buffer contains valid number
|
|
// ============================================================
|
|
|
|
static bool spinnerValidateBuffer(const SpinnerDataT *d) {
|
|
bool allowMin = spinnerAllowMinus(d);
|
|
bool hadDot = false;
|
|
|
|
for (int32_t i = 0; i < d->len; i++) {
|
|
char c = d->buf[i];
|
|
|
|
if (c == '-' && i == 0 && allowMin) {
|
|
continue;
|
|
}
|
|
|
|
if (c == '.' && d->useReal && !hadDot) {
|
|
hadDot = true;
|
|
continue;
|
|
}
|
|
|
|
if (c < '0' || c > '9') {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// spinnerStep -- apply step in given direction (+1 or -1)
|
|
// ============================================================
|
|
|
|
static void spinnerApplyStep(SpinnerDataT *d, int32_t direction, int32_t multiplier) {
|
|
if (d->useReal) {
|
|
d->realValue += d->realStep * direction * multiplier;
|
|
} else {
|
|
d->value += d->step * direction * multiplier;
|
|
}
|
|
|
|
spinnerClampAndFormat(d);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerCalcMinSize
|
|
// ============================================================
|
|
|
|
static 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerDestroy
|
|
// ============================================================
|
|
|
|
static void widgetSpinnerDestroy(WidgetT *w) {
|
|
free(w->data);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerGetText
|
|
// ============================================================
|
|
|
|
static const char *widgetSpinnerGetText(const WidgetT *w) {
|
|
const SpinnerDataT *d = (const SpinnerDataT *)w->data;
|
|
return d->buf;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerOnKey
|
|
// ============================================================
|
|
|
|
// Key handling has two distinct paths: navigation keys (Up/Down/PgUp/
|
|
// PgDn) always commit any pending edit first, then adjust the numeric
|
|
// value directly. Text keys enter edit mode and are forwarded to the
|
|
// shared text editor. This split ensures arrow-key nudging always
|
|
// operates on the committed value, not on partially typed text.
|
|
//
|
|
// Page Up/Down use step*10 for coarser adjustment, matching the
|
|
// convention used by Windows spin controls.
|
|
static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
// Up arrow -- increment
|
|
if (key == (0x48 | 0x100)) {
|
|
spinnerCommitEdit(d);
|
|
spinnerApplyStep(d, 1, 1);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Down arrow -- decrement
|
|
if (key == (0x50 | 0x100)) {
|
|
spinnerCommitEdit(d);
|
|
spinnerApplyStep(d, -1, 1);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Page Up -- increment by step * 10
|
|
if (key == (0x49 | 0x100)) {
|
|
spinnerCommitEdit(d);
|
|
spinnerApplyStep(d, 1, 10);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Page Down -- decrement by step * 10
|
|
if (key == (0x51 | 0x100)) {
|
|
spinnerCommitEdit(d);
|
|
spinnerApplyStep(d, -1, 10);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Enter -- commit edit
|
|
if (key == '\r' || key == '\n') {
|
|
if (d->editing) {
|
|
spinnerCommitEdit(d);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Escape -- cancel edit, revert to current value
|
|
if (key == 27) {
|
|
if (d->editing) {
|
|
d->editing = false;
|
|
spinnerFormat(d);
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Filter: only allow digits, minus, dot (real mode), and control keys
|
|
bool isDigit = (key >= '0' && key <= '9');
|
|
bool isMinus = (key == '-');
|
|
bool isDot = (key == '.' && d->useReal);
|
|
bool isControl = (key < 0x20) || (key & 0x100);
|
|
|
|
if (!isDigit && !isMinus && !isDot && !isControl) {
|
|
return;
|
|
}
|
|
|
|
// Minus only at position 0 (and only if min is negative)
|
|
if (isMinus && (d->cursorPos != 0 || !spinnerAllowMinus(d))) {
|
|
return;
|
|
}
|
|
|
|
// Dot only once (check if buffer already has one)
|
|
if (isDot) {
|
|
for (int32_t i = 0; i < d->len; i++) {
|
|
if (d->buf[i] == '.') {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enter edit mode on first text-modifying key
|
|
if (isDigit || isMinus || isDot || key == 8 || key == 127 || key == (0x53 | 0x100)) {
|
|
spinnerStartEdit(d);
|
|
}
|
|
|
|
// Delegate to shared text editing logic (handles cursor movement,
|
|
// selection, cut/copy/paste, undo/redo, backspace, delete, etc.)
|
|
widgetTextEditOnKey(w, key, mod,
|
|
d->buf, (int32_t)sizeof(d->buf),
|
|
&d->len, &d->cursorPos,
|
|
&d->scrollOff,
|
|
&d->selStart, &d->selEnd,
|
|
d->undoBuf, &d->undoLen,
|
|
&d->undoCursor,
|
|
w->w - SPINNER_BORDER * 2 - SPINNER_BTN_W);
|
|
|
|
// Validate buffer after paste -- reject non-numeric content.
|
|
if (!spinnerValidateBuffer(d)) {
|
|
// Revert to the undo buffer (pre-paste state)
|
|
memcpy(d->buf, d->undoBuf, sizeof(d->buf));
|
|
d->len = d->undoLen;
|
|
d->cursorPos = d->undoCursor;
|
|
d->selStart = 0;
|
|
d->selEnd = 0;
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerOnMouse
|
|
// ============================================================
|
|
|
|
// Mouse click regions: button area (right side) vs text area (left side).
|
|
// Button area is split vertically at the midpoint -- top half increments,
|
|
// bottom half decrements. Clicking a button commits any pending edit
|
|
// before adjusting the value, same as arrow keys.
|
|
//
|
|
// Text area clicks compute cursor position from pixel offset using the
|
|
// fixed-width font. Double-click selects all text (select-word doesn't
|
|
// make sense for numbers), entering edit mode to allow replacement.
|
|
static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
sFocusedWidget = hit;
|
|
SpinnerDataT *d = (SpinnerDataT *)hit->data;
|
|
|
|
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(d);
|
|
spinnerApplyStep(d, (vy < midY) ? 1 : -1, 1);
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
} else {
|
|
// Click on text area
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
widgetTextEditMouseClick(hit, vx, vy, hit->x + SPINNER_BORDER + SPINNER_PAD, &ctx->font, d->buf, d->len, d->scrollOff, &d->cursorPos, &d->selStart, &d->selEnd, false, false);
|
|
spinnerStartEdit(d);
|
|
}
|
|
|
|
wgtInvalidatePaint(hit);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerPaint
|
|
// ============================================================
|
|
|
|
// Paint uses the same 3-run text rendering approach as TextInput:
|
|
// before-selection, selection (highlighted), after-selection. This
|
|
// avoids overdraw and gives correct selection highlighting with only
|
|
// one pass over the visible text. The scroll offset ensures the
|
|
// cursor is always visible even when the number is wider than the
|
|
// text area.
|
|
//
|
|
// The two buttons (up/down) extend SPINNER_BORDER pixels past the
|
|
// button area into the widget's right border so they visually merge
|
|
// with the outer bevel -- this is why btnW is btnW + SPINNER_BORDER
|
|
// in the drawBevel calls.
|
|
static void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
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(disp, 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 (d->cursorPos < d->scrollOff) {
|
|
d->scrollOff = d->cursorPos;
|
|
} else if (d->cursorPos > d->scrollOff + maxChars) {
|
|
d->scrollOff = d->cursorPos - maxChars;
|
|
}
|
|
|
|
int32_t off = d->scrollOff;
|
|
int32_t len = d->len - off;
|
|
|
|
if (len > maxChars) {
|
|
len = maxChars;
|
|
}
|
|
|
|
if (len < 0) {
|
|
len = 0;
|
|
}
|
|
|
|
widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, &d->buf[off], len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w == sFocusedWidget, w->x + SPINNER_BORDER, btnX - SPINNER_PAD);
|
|
|
|
// Up button (top half)
|
|
int32_t btnTopH = w->h / 2;
|
|
int32_t btnBotH = w->h - btnTopH;
|
|
|
|
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
|
drawBevel(disp, 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(disp, ops, cx - i, cy - 1 + i, 1 + i * 2, fg);
|
|
}
|
|
}
|
|
|
|
// Down button (bottom half)
|
|
drawBevel(disp, 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(disp, ops, cx - i, cy + 1 - i, 1 + i * 2, fg);
|
|
}
|
|
}
|
|
|
|
// Focus rect around entire widget
|
|
if (w == sFocusedWidget) {
|
|
drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSpinnerSetText
|
|
// ============================================================
|
|
|
|
static void widgetSpinnerSetText(WidgetT *w, const char *text) {
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
if (d->useReal) {
|
|
d->realValue = strtod(text, NULL);
|
|
} else {
|
|
d->value = (int32_t)strtol(text, NULL, 10);
|
|
}
|
|
|
|
spinnerClampAndFormat(d);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
static const WidgetClassT sClassSpinner = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetSpinnerPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetSpinnerCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetSpinnerOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetSpinnerOnKey,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetSpinnerDestroy,
|
|
[WGT_METHOD_GET_TEXT] = (void *)widgetSpinnerGetText,
|
|
[WGT_METHOD_SET_TEXT] = (void *)widgetSpinnerSetText,
|
|
}
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// Widget creation functions
|
|
// ============================================================
|
|
|
|
|
|
WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step) {
|
|
WidgetT *w = widgetAlloc(parent, sTypeId);
|
|
|
|
if (w) {
|
|
SpinnerDataT *d = (SpinnerDataT *)calloc(1, sizeof(SpinnerDataT));
|
|
|
|
if (!d) {
|
|
return w;
|
|
}
|
|
|
|
w->data = d;
|
|
d->minValue = minVal;
|
|
d->maxValue = maxVal;
|
|
d->step = step > 0 ? step : 1;
|
|
d->value = minVal;
|
|
d->decimals = SPINNER_DEFAULT_DECIMALS;
|
|
d->realStep = 1.0;
|
|
d->selStart = -1;
|
|
d->selEnd = -1;
|
|
spinnerFormat(d);
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Public API -- integer mode
|
|
// ============================================================
|
|
|
|
|
|
int32_t wgtSpinnerGetValue(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, 0);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
// Commit any in-progress edit so the returned value is current
|
|
if (d->editing) {
|
|
spinnerCommitEdit(d);
|
|
}
|
|
|
|
return d->value;
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->minValue = minVal;
|
|
d->maxValue = maxVal;
|
|
spinnerClampAndFormat(d);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetStep(WidgetT *w, int32_t step) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->step = step > 0 ? step : 1;
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetValue(WidgetT *w, int32_t value) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->value = value;
|
|
spinnerClampAndFormat(d);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Public API -- real mode
|
|
// ============================================================
|
|
|
|
|
|
void wgtSpinnerSetRealMode(WidgetT *w, bool enable) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->useReal = enable;
|
|
spinnerClampAndFormat(d);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
double wgtSpinnerGetRealValue(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, 0.0);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
if (d->editing) {
|
|
spinnerCommitEdit(d);
|
|
}
|
|
|
|
return d->realValue;
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetRealValue(WidgetT *w, double value) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->realValue = value;
|
|
spinnerClampAndFormat(d);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetRealRange(WidgetT *w, double minVal, double maxVal) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->realMin = minVal;
|
|
d->realMax = maxVal;
|
|
spinnerClampAndFormat(d);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetRealStep(WidgetT *w, double step) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->realStep = step > 0.0 ? step : 0.01;
|
|
}
|
|
|
|
|
|
void wgtSpinnerSetDecimals(WidgetT *w, int32_t decimals) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
SpinnerDataT *d = (SpinnerDataT *)w->data;
|
|
|
|
d->decimals = (decimals >= 0 && decimals <= 10) ? decimals : SPINNER_DEFAULT_DECIMALS;
|
|
spinnerFormat(d);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Property getters/setters for WgtIfaceT
|
|
// ============================================================
|
|
|
|
static int32_t ifaceGetValue(const WidgetT *w) {
|
|
return wgtSpinnerGetValue(w);
|
|
}
|
|
|
|
static void ifaceSetValue(WidgetT *w, int32_t v) {
|
|
wgtSpinnerSetValue(w, v);
|
|
}
|
|
|
|
static bool ifaceGetRealMode(const WidgetT *w) {
|
|
return ((SpinnerDataT *)w->data)->useReal;
|
|
}
|
|
|
|
static void ifaceSetRealMode(WidgetT *w, bool v) {
|
|
wgtSpinnerSetRealMode(w, v);
|
|
}
|
|
|
|
static int32_t ifaceGetDecimals(const WidgetT *w) {
|
|
return ((SpinnerDataT *)w->data)->decimals;
|
|
}
|
|
|
|
static void ifaceSetDecimals(WidgetT *w, int32_t v) {
|
|
wgtSpinnerSetDecimals(w, v);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE API and interface registration
|
|
// ============================================================
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step);
|
|
void (*setValue)(WidgetT *w, int32_t value);
|
|
int32_t (*getValue)(const WidgetT *w);
|
|
void (*setRange)(WidgetT *w, int32_t minVal, int32_t maxVal);
|
|
void (*setStep)(WidgetT *w, int32_t step);
|
|
void (*setRealMode)(WidgetT *w, bool enable);
|
|
double (*getRealValue)(const WidgetT *w);
|
|
void (*setRealValue)(WidgetT *w, double value);
|
|
void (*setRealRange)(WidgetT *w, double minVal, double maxVal);
|
|
void (*setRealStep)(WidgetT *w, double step);
|
|
void (*setDecimals)(WidgetT *w, int32_t decimals);
|
|
} sApi = {
|
|
.create = wgtSpinner,
|
|
.setValue = wgtSpinnerSetValue,
|
|
.getValue = wgtSpinnerGetValue,
|
|
.setRange = wgtSpinnerSetRange,
|
|
.setStep = wgtSpinnerSetStep,
|
|
.setRealMode = wgtSpinnerSetRealMode,
|
|
.getRealValue = wgtSpinnerGetRealValue,
|
|
.setRealValue = wgtSpinnerSetRealValue,
|
|
.setRealRange = wgtSpinnerSetRealRange,
|
|
.setRealStep = wgtSpinnerSetRealStep,
|
|
.setDecimals = wgtSpinnerSetDecimals,
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "Value", WGT_IFACE_INT, (void *)ifaceGetValue, (void *)ifaceSetValue, NULL },
|
|
{ "RealMode", WGT_IFACE_BOOL, (void *)ifaceGetRealMode, (void *)ifaceSetRealMode, NULL },
|
|
{ "Decimals", WGT_IFACE_INT, (void *)ifaceGetDecimals, (void *)ifaceSetDecimals, NULL },
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "SetRange", WGT_SIG_INT_INT, (void *)wgtSpinnerSetRange },
|
|
{ "SetStep", WGT_SIG_INT, (void *)wgtSpinnerSetStep }
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "SpinButton",
|
|
.props = sProps,
|
|
.propCount = 3,
|
|
.methods = sMethods,
|
|
.methodCount = 2,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT_INT_INT_INT,
|
|
.createArgs = { 0, 100, 1 },
|
|
.defaultEvent = "Change",
|
|
.namePrefix = "Spin",
|
|
};
|
|
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassSpinner);
|
|
wgtRegisterApi("spinner", &sApi);
|
|
wgtRegisterIface("spinner", &sIface);
|
|
}
|