441 lines
14 KiB
C
441 lines
14 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetRadio.c -- RadioGroup and Radio button widgets
|
|
//
|
|
// Two-level architecture: RadioGroupE is an invisible container that
|
|
// holds the selection state (selectedIdx), while RadioE children are
|
|
// the visible buttons. This separation means the group tracks which
|
|
// index is selected while each radio button only knows its own index,
|
|
// keeping the per-button state minimal.
|
|
//
|
|
// Selection state is stored as an integer index on the parent group
|
|
// rather than a pointer, because indices survive widget reordering
|
|
// and are trivially serializable. The index is auto-assigned at
|
|
// construction time based on sibling order.
|
|
//
|
|
// Rendering: diamond-shaped indicator (Motif-style) using scanline
|
|
// horizontal lines rather than a circle algorithm. This avoids any
|
|
// need for anti-aliasing or trigonometry -- purely integer H/V lines.
|
|
// The selection dot inside uses a hardcoded width table for a 6-row
|
|
// filled diamond, sized to look crisp at the fixed 12px box size.
|
|
//
|
|
// Keyboard: Space/Enter select the current radio. Arrow keys move
|
|
// focus AND selection together (standard radio group behavior --
|
|
// focus and selection are coupled, unlike checkboxes).
|
|
|
|
#include "dvxWgtP.h"
|
|
|
|
#define CHECKBOX_BOX_SIZE 12
|
|
#define CHECKBOX_GAP 4
|
|
|
|
static int32_t sRadioGroupTypeId = -1;
|
|
static int32_t sRadioTypeId = -1;
|
|
|
|
typedef struct {
|
|
const char *text;
|
|
int32_t index;
|
|
} RadioDataT;
|
|
|
|
typedef struct {
|
|
int32_t selectedIdx;
|
|
} RadioGroupDataT;
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void widgetRadioDestroy(WidgetT *w);
|
|
static void widgetRadioGroupDestroy(WidgetT *w);
|
|
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioAccelActivate
|
|
// ============================================================
|
|
|
|
void widgetRadioAccelActivate(WidgetT *w, WidgetT *root) {
|
|
(void)root;
|
|
widgetRadioOnMouse(w, NULL, 0, 0);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioCalcMinSize
|
|
// ============================================================
|
|
|
|
// Shares CHECKBOX_BOX_SIZE/GAP constants with the checkbox widget
|
|
// so radio buttons and checkboxes align when placed in the same
|
|
// column, and the indicator + label layout is visually consistent.
|
|
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
RadioDataT *d = (RadioDataT *)w->data;
|
|
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
|
textWidthAccel(font, d->text);
|
|
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioDestroy
|
|
// ============================================================
|
|
|
|
static void widgetRadioDestroy(WidgetT *w) {
|
|
RadioDataT *d = (RadioDataT *)w->data;
|
|
free((void *)d->text);
|
|
free(w->data);
|
|
w->data = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioGetText
|
|
// ============================================================
|
|
|
|
const char *widgetRadioGetText(const WidgetT *w) {
|
|
RadioDataT *d = (RadioDataT *)w->data;
|
|
return d->text;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioGroupDestroy
|
|
// ============================================================
|
|
|
|
static void widgetRadioGroupDestroy(WidgetT *w) {
|
|
free(w->data);
|
|
w->data = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioOnKey
|
|
// ============================================================
|
|
|
|
// Arrow keys move focus AND selection simultaneously -- this is the
|
|
// standard radio group behavior where navigating with arrows also
|
|
// commits the selection. This differs from checkboxes where arrows
|
|
// just move focus and Space toggles. The coupling is deliberate:
|
|
// radio groups represent a single mutually-exclusive choice, so
|
|
// "looking at" an option means "choosing" it.
|
|
//
|
|
// Key codes use DOS BIOS scancode convention: high byte 0x01 flag
|
|
// ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left.
|
|
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
RadioDataT *rd = (RadioDataT *)w->data;
|
|
|
|
if (key == ' ' || key == 0x0D) {
|
|
// Select this radio
|
|
if (w->parent && w->parent->type == sRadioGroupTypeId) {
|
|
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
|
|
gd->selectedIdx = rd->index;
|
|
|
|
if (w->parent->onChange) {
|
|
w->parent->onChange(w->parent);
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
} else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) {
|
|
// Down or Right -- next radio in group
|
|
if (w->parent && w->parent->type == sRadioGroupTypeId) {
|
|
WidgetT *next = NULL;
|
|
|
|
for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
|
|
if (s->type == sRadioTypeId && s->visible && s->enabled) {
|
|
next = s;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (next) {
|
|
RadioDataT *nd = (RadioDataT *)next->data;
|
|
RadioGroupDataT *gd = (RadioGroupDataT *)next->parent->data;
|
|
sFocusedWidget = next;
|
|
gd->selectedIdx = nd->index;
|
|
|
|
if (next->parent->onChange) {
|
|
next->parent->onChange(next->parent);
|
|
}
|
|
|
|
wgtInvalidatePaint(next);
|
|
}
|
|
}
|
|
} else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) {
|
|
// Up or Left -- previous radio in group
|
|
if (w->parent && w->parent->type == sRadioGroupTypeId) {
|
|
WidgetT *prev = NULL;
|
|
|
|
for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
|
|
if (s->type == sRadioTypeId && s->visible && s->enabled) {
|
|
prev = s;
|
|
}
|
|
}
|
|
|
|
if (prev) {
|
|
RadioDataT *pd = (RadioDataT *)prev->data;
|
|
RadioGroupDataT *gd = (RadioGroupDataT *)prev->parent->data;
|
|
sFocusedWidget = prev;
|
|
gd->selectedIdx = pd->index;
|
|
|
|
if (prev->parent->onChange) {
|
|
prev->parent->onChange(prev->parent);
|
|
}
|
|
|
|
wgtInvalidatePaint(prev);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioOnMouse
|
|
// ============================================================
|
|
|
|
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|
(void)root;
|
|
(void)vx;
|
|
(void)vy;
|
|
sFocusedWidget = w;
|
|
|
|
if (w->parent && w->parent->type == sRadioGroupTypeId) {
|
|
RadioDataT *rd = (RadioDataT *)w->data;
|
|
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
|
|
gd->selectedIdx = rd->index;
|
|
|
|
if (w->parent->onChange) {
|
|
w->parent->onChange(w->parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioPaint
|
|
// ============================================================
|
|
|
|
// The diamond radio indicator is rendered in three passes:
|
|
// 1. Interior fill -- scanline-based diamond fill for the background
|
|
// 2. Border -- upper-left edges in shadow, lower-right in highlight
|
|
// (sunken appearance, same lighting model as bevels)
|
|
// 3. Selection dot -- smaller filled diamond from a hardcoded 6-row
|
|
// width table. The static const avoids recomputation per paint.
|
|
//
|
|
// This approach avoids floating point entirely. Each scanline's
|
|
// left/right extent is computed with simple integer distance from
|
|
// the midpoint, making the diamond perfectly symmetric at any size.
|
|
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
RadioDataT *rd = (RadioDataT *)w->data;
|
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
|
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
|
|
|
// Draw diamond-shaped radio box
|
|
int32_t bx = w->x;
|
|
int32_t mid = CHECKBOX_BOX_SIZE / 2;
|
|
uint32_t hi = colors->windowShadow;
|
|
uint32_t sh = colors->windowHighlight;
|
|
|
|
// Fill interior
|
|
for (int32_t i = 0; i < CHECKBOX_BOX_SIZE; i++) {
|
|
int32_t dist = i < mid ? i : CHECKBOX_BOX_SIZE - 1 - i;
|
|
int32_t left = mid - dist;
|
|
int32_t right = mid + dist;
|
|
|
|
if (right > left) {
|
|
drawHLine(d, ops, bx + left + 1, boxY + i, right - left - 1, bg);
|
|
}
|
|
}
|
|
|
|
// Diamond border -- upper-left edges get highlight, lower-right get shadow
|
|
for (int32_t i = 0; i < mid; i++) {
|
|
int32_t left = mid - i;
|
|
int32_t right = mid + i;
|
|
drawHLine(d, ops, bx + left, boxY + i, 1, hi);
|
|
drawHLine(d, ops, bx + right, boxY + i, 1, sh);
|
|
}
|
|
|
|
for (int32_t i = mid; i < CHECKBOX_BOX_SIZE; i++) {
|
|
int32_t left = mid - (CHECKBOX_BOX_SIZE - 1 - i);
|
|
int32_t right = mid + (CHECKBOX_BOX_SIZE - 1 - i);
|
|
drawHLine(d, ops, bx + left, boxY + i, 1, hi);
|
|
drawHLine(d, ops, bx + right, boxY + i, 1, sh);
|
|
}
|
|
|
|
// Draw filled diamond if selected
|
|
if (w->parent && w->parent->type == sRadioGroupTypeId) {
|
|
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
|
|
|
|
if (gd->selectedIdx == rd->index) {
|
|
uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
|
|
|
|
static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
|
|
|
|
for (int32_t i = 0; i < 6; i++) {
|
|
int32_t dw = dotW[i];
|
|
drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
|
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
|
int32_t labelW = textWidthAccel(font, rd->text);
|
|
|
|
if (!w->enabled) {
|
|
drawTextAccelEmbossed(d, ops, font, labelX, labelY, rd->text, colors);
|
|
} else {
|
|
drawTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false);
|
|
}
|
|
|
|
if (w == sFocusedWidget) {
|
|
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRadioSetText
|
|
// ============================================================
|
|
|
|
void widgetRadioSetText(WidgetT *w, const char *text) {
|
|
RadioDataT *d = (RadioDataT *)w->data;
|
|
free((void *)d->text);
|
|
d->text = text ? strdup(text) : NULL;
|
|
w->accelKey = accelParse(text);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static const WidgetClassT sClassRadioGroup = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_DESTROY] = (void *)widgetRadioGroupDestroy,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox,
|
|
[WGT_METHOD_LAYOUT] = (void *)widgetLayoutBox,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassRadio = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetRadioPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetRadioCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetRadioOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetRadioOnKey,
|
|
[WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetRadioAccelActivate,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetRadioDestroy,
|
|
[WGT_METHOD_GET_TEXT] = (void *)widgetRadioGetText,
|
|
[WGT_METHOD_SET_TEXT] = (void *)widgetRadioSetText,
|
|
}
|
|
};
|
|
|
|
// ============================================================
|
|
// Widget creation functions
|
|
// ============================================================
|
|
|
|
|
|
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
|
WidgetT *w = widgetAlloc(parent, sRadioTypeId);
|
|
|
|
if (w) {
|
|
RadioDataT *d = calloc(1, sizeof(RadioDataT));
|
|
w->data = d;
|
|
d->text = text ? strdup(text) : NULL;
|
|
w->accelKey = accelParse(text);
|
|
|
|
// Auto-assign index based on position in parent
|
|
int32_t idx = 0;
|
|
|
|
for (WidgetT *c = parent->firstChild; c != w; c = c->nextSibling) {
|
|
if (c->type == sRadioTypeId) {
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
d->index = idx;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
WidgetT *wgtRadioGroup(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, sRadioGroupTypeId);
|
|
|
|
if (w) {
|
|
RadioGroupDataT *d = calloc(1, sizeof(RadioGroupDataT));
|
|
w->data = d;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
void wgtRadioGroupSetSelected(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sRadioGroupTypeId);
|
|
RadioGroupDataT *d = (RadioGroupDataT *)w->data;
|
|
|
|
d->selectedIdx = idx;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
int32_t wgtRadioGetIndex(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sRadioTypeId, -1);
|
|
RadioDataT *d = (RadioDataT *)w->data;
|
|
|
|
return d->index;
|
|
}
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*group)(WidgetT *parent);
|
|
WidgetT *(*create)(WidgetT *parent, const char *text);
|
|
void (*groupSetSelected)(WidgetT *group, int32_t index);
|
|
int32_t (*getIndex)(const WidgetT *w);
|
|
} sApi = {
|
|
.group = wgtRadioGroup,
|
|
.create = wgtRadio,
|
|
.groupSetSelected = wgtRadioGroupSetSelected,
|
|
.getIndex = wgtRadioGetIndex
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "Value", WGT_IFACE_INT, (void *)wgtRadioGetIndex, NULL, NULL }
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "SetSelected", WGT_SIG_INT, (void *)wgtRadioGroupSetSelected }
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "OptionButton",
|
|
.props = sProps,
|
|
.propCount = 1,
|
|
.methods = sMethods,
|
|
.methodCount = 1,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT_TEXT,
|
|
.defaultEvent = "Click",
|
|
.namePrefix = "Option"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sRadioGroupTypeId = wgtRegisterClass(&sClassRadioGroup);
|
|
sRadioTypeId = wgtRegisterClass(&sClassRadio);
|
|
wgtRegisterApi("radio", &sApi);
|
|
wgtRegisterIface("radio", &sIface);
|
|
}
|