DVX_GUI/dvx/widgets/widgetRadio.c

282 lines
9.8 KiB
C

// 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 "widgetInternal.h"
// ============================================================
// wgtRadio
// ============================================================
// Auto-index is computed by counting RadioE siblings before this one.
// This means radio buttons must be added in order and not reordered
// after construction -- which is the normal pattern for radio groups.
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, WidgetRadioE);
if (w) {
w->as.radio.text = text;
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 == WidgetRadioE) {
idx++;
}
}
w->as.radio.index = idx;
}
return w;
}
// ============================================================
// wgtRadioGroup
// ============================================================
WidgetT *wgtRadioGroup(WidgetT *parent) {
WidgetT *w = widgetAlloc(parent, WidgetRadioGroupE);
if (w) {
w->as.radioGroup.selectedIdx = 0;
}
return w;
}
// ============================================================
// widgetRadioGetText
// ============================================================
const char *widgetRadioGetText(const WidgetT *w) {
return w->as.radio.text;
}
// ============================================================
// widgetRadioSetText
// ============================================================
void widgetRadioSetText(WidgetT *w, const char *text) {
w->as.radio.text = text;
w->accelKey = accelParse(text);
}
// ============================================================
// 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) {
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
textWidthAccel(font, w->as.radio.text);
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
}
// ============================================================
// 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;
if (key == ' ' || key == 0x0D) {
// Select this radio
if (w->parent && w->parent->type == WidgetRadioGroupE) {
w->parent->as.radioGroup.selectedIdx = w->as.radio.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 == WidgetRadioGroupE) {
WidgetT *next = NULL;
for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
if (s->type == WidgetRadioE && s->visible && s->enabled) {
next = s;
break;
}
}
if (next) {
w->focused = false;
next->focused = true;
sFocusedWidget = next;
next->parent->as.radioGroup.selectedIdx = next->as.radio.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 == WidgetRadioGroupE) {
WidgetT *prev = NULL;
for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
if (s->type == WidgetRadioE && s->visible && s->enabled) {
prev = s;
}
}
if (prev) {
w->focused = false;
prev->focused = true;
sFocusedWidget = prev;
prev->parent->as.radioGroup.selectedIdx = prev->as.radio.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;
w->focused = true;
if (w->parent && w->parent->type == WidgetRadioGroupE) {
w->parent->as.radioGroup.selectedIdx = w->as.radio.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) {
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 == WidgetRadioGroupE &&
w->parent->as.radioGroup.selectedIdx == w->as.radio.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, w->as.radio.text);
if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.radio.text, colors);
} else {
drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
}
if (w->focused) {
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
}
}