1634 lines
45 KiB
C
1634 lines
45 KiB
C
// dvxWidget.c — Widget system for DV/X GUI
|
|
|
|
#include "dvxWidget.h"
|
|
#include "dvxApp.h"
|
|
#include "dvxDraw.h"
|
|
#include "dvxWm.h"
|
|
#include "dvxVideo.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define DEFAULT_SPACING 4
|
|
#define DEFAULT_PADDING 4
|
|
#define SEPARATOR_THICKNESS 2
|
|
#define BUTTON_PAD_H 8
|
|
#define BUTTON_PAD_V 4
|
|
#define CHECKBOX_BOX_SIZE 12
|
|
#define CHECKBOX_GAP 4
|
|
#define FRAME_BORDER 2
|
|
#define FRAME_TITLE_GAP 4
|
|
#define TEXT_INPUT_PAD 3
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void addChild(WidgetT *parent, WidgetT *child);
|
|
static WidgetT *allocWidget(WidgetT *parent, WidgetTypeE type);
|
|
static void calcMinSizeBox(WidgetT *w, const BitmapFontT *font);
|
|
static void calcMinSizeLeaf(WidgetT *w, const BitmapFontT *font);
|
|
static void calcMinSizeTree(WidgetT *w, const BitmapFontT *font);
|
|
static int32_t countVisibleChildren(const WidgetT *w);
|
|
static void destroyChildren(WidgetT *w);
|
|
static WidgetT *hitTest(WidgetT *w, int32_t x, int32_t y);
|
|
static void layoutBox(WidgetT *w, const BitmapFontT *font);
|
|
static void layoutChildren(WidgetT *w, const BitmapFontT *font);
|
|
static void paintWidget(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
|
|
const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void removeChild(WidgetT *parent, WidgetT *child);
|
|
static void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
|
|
static void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
|
|
static void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
|
static void widgetOnPaint(WindowT *win, RectT *dirtyArea);
|
|
static void widgetOnResize(WindowT *win, int32_t newW, int32_t newH);
|
|
static void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value);
|
|
|
|
|
|
// ============================================================
|
|
// addChild
|
|
// ============================================================
|
|
|
|
static void addChild(WidgetT *parent, WidgetT *child) {
|
|
child->parent = parent;
|
|
child->nextSibling = NULL;
|
|
|
|
if (parent->lastChild) {
|
|
parent->lastChild->nextSibling = child;
|
|
parent->lastChild = child;
|
|
} else {
|
|
parent->firstChild = child;
|
|
parent->lastChild = child;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// allocWidget
|
|
// ============================================================
|
|
|
|
static WidgetT *allocWidget(WidgetT *parent, WidgetTypeE type) {
|
|
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
|
|
|
|
if (!w) {
|
|
return NULL;
|
|
}
|
|
|
|
memset(w, 0, sizeof(*w));
|
|
w->type = type;
|
|
w->visible = true;
|
|
w->enabled = true;
|
|
|
|
if (parent) {
|
|
w->window = parent->window;
|
|
addChild(parent, w);
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// calcMinSizeBox
|
|
// ============================================================
|
|
|
|
static void calcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
|
bool horiz = (w->type == WidgetHBoxE); // RadioGroupE and VBoxE are vertical
|
|
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
|
|
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
|
|
int32_t mainSize = 0;
|
|
int32_t crossSize = 0;
|
|
int32_t count = 0;
|
|
|
|
if (pad == 0) {
|
|
pad = DEFAULT_PADDING;
|
|
}
|
|
|
|
if (gap == 0) {
|
|
gap = DEFAULT_SPACING;
|
|
}
|
|
|
|
// Frame adds title height and border
|
|
int32_t frameExtraTop = 0;
|
|
|
|
if (w->type == WidgetFrameE) {
|
|
frameExtraTop = font->charHeight + FRAME_TITLE_GAP;
|
|
pad = DEFAULT_PADDING;
|
|
}
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (!c->visible) {
|
|
continue;
|
|
}
|
|
|
|
calcMinSizeTree(c, font);
|
|
|
|
if (horiz) {
|
|
mainSize += c->calcMinW;
|
|
crossSize = DVX_MAX(crossSize, c->calcMinH);
|
|
} else {
|
|
mainSize += c->calcMinH;
|
|
crossSize = DVX_MAX(crossSize, c->calcMinW);
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
// Add spacing between children
|
|
if (count > 1) {
|
|
mainSize += gap * (count - 1);
|
|
}
|
|
|
|
// Add padding
|
|
mainSize += pad * 2;
|
|
crossSize += pad * 2;
|
|
|
|
if (horiz) {
|
|
w->calcMinW = mainSize;
|
|
w->calcMinH = crossSize + frameExtraTop;
|
|
} else {
|
|
w->calcMinW = crossSize;
|
|
w->calcMinH = mainSize + frameExtraTop;
|
|
}
|
|
|
|
// Frame border
|
|
if (w->type == WidgetFrameE) {
|
|
w->calcMinW += FRAME_BORDER * 2;
|
|
w->calcMinH += FRAME_BORDER * 2;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// calcMinSizeLeaf
|
|
// ============================================================
|
|
|
|
static void calcMinSizeLeaf(WidgetT *w, const BitmapFontT *font) {
|
|
switch (w->type) {
|
|
case WidgetLabelE:
|
|
w->calcMinW = (int32_t)strlen(w->as.label.text) * font->charWidth;
|
|
w->calcMinH = font->charHeight;
|
|
break;
|
|
|
|
case WidgetButtonE:
|
|
w->calcMinW = (int32_t)strlen(w->as.button.text) * font->charWidth + BUTTON_PAD_H * 2;
|
|
w->calcMinH = font->charHeight + BUTTON_PAD_V * 2;
|
|
break;
|
|
|
|
case WidgetCheckboxE:
|
|
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
|
(int32_t)strlen(w->as.checkbox.text) * font->charWidth;
|
|
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
|
|
break;
|
|
|
|
case WidgetRadioE:
|
|
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
|
|
(int32_t)strlen(w->as.radio.text) * font->charWidth;
|
|
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
|
|
break;
|
|
|
|
case WidgetTextInputE:
|
|
w->calcMinW = font->charWidth * 8 + TEXT_INPUT_PAD * 2;
|
|
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
|
|
break;
|
|
|
|
case WidgetSpacerE:
|
|
w->calcMinW = 0;
|
|
w->calcMinH = 0;
|
|
break;
|
|
|
|
case WidgetSeparatorE:
|
|
if (w->as.separator.vertical) {
|
|
w->calcMinW = SEPARATOR_THICKNESS;
|
|
w->calcMinH = 0;
|
|
} else {
|
|
w->calcMinW = 0;
|
|
w->calcMinH = SEPARATOR_THICKNESS;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
w->calcMinW = 0;
|
|
w->calcMinH = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// calcMinSizeTree
|
|
// ============================================================
|
|
|
|
static void calcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
|
if (w->type == WidgetVBoxE || w->type == WidgetHBoxE || w->type == WidgetFrameE || w->type == WidgetRadioGroupE) {
|
|
calcMinSizeBox(w, font);
|
|
} else {
|
|
calcMinSizeLeaf(w, font);
|
|
}
|
|
|
|
// Apply size hints (override calculated minimum)
|
|
if (w->minW) {
|
|
int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);
|
|
|
|
if (hintW > w->calcMinW) {
|
|
w->calcMinW = hintW;
|
|
}
|
|
}
|
|
|
|
if (w->minH) {
|
|
int32_t hintH = wgtResolveSize(w->minH, 0, font->charWidth);
|
|
|
|
if (hintH > w->calcMinH) {
|
|
w->calcMinH = hintH;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// countVisibleChildren
|
|
// ============================================================
|
|
|
|
static int32_t countVisibleChildren(const WidgetT *w) {
|
|
int32_t count = 0;
|
|
|
|
for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->visible) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// destroyChildren
|
|
// ============================================================
|
|
|
|
static void destroyChildren(WidgetT *w) {
|
|
WidgetT *child = w->firstChild;
|
|
|
|
while (child) {
|
|
WidgetT *next = child->nextSibling;
|
|
destroyChildren(child);
|
|
|
|
if (child->type == WidgetTextInputE) {
|
|
free(child->as.textInput.buf);
|
|
} else if (child->type == WidgetTextAreaE) {
|
|
free(child->as.textArea.buf);
|
|
}
|
|
|
|
free(child);
|
|
child = next;
|
|
}
|
|
|
|
w->firstChild = NULL;
|
|
w->lastChild = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// hitTest
|
|
// ============================================================
|
|
|
|
static WidgetT *hitTest(WidgetT *w, int32_t x, int32_t y) {
|
|
if (!w->visible) {
|
|
return NULL;
|
|
}
|
|
|
|
if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) {
|
|
return NULL;
|
|
}
|
|
|
|
// Check children in reverse order (last child is on top)
|
|
// Walk to last visible child, then check backwards
|
|
// Since we use a singly-linked list, just check all and take the last match
|
|
WidgetT *hit = NULL;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
WidgetT *childHit = hitTest(c, x, y);
|
|
|
|
if (childHit) {
|
|
hit = childHit;
|
|
}
|
|
}
|
|
|
|
return hit ? hit : w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// layoutBox
|
|
// ============================================================
|
|
|
|
static void layoutBox(WidgetT *w, const BitmapFontT *font) {
|
|
bool horiz = (w->type == WidgetHBoxE);
|
|
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
|
|
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
|
|
|
|
if (pad == 0) {
|
|
pad = DEFAULT_PADDING;
|
|
}
|
|
|
|
if (gap == 0) {
|
|
gap = DEFAULT_SPACING;
|
|
}
|
|
|
|
// Frame adjustments
|
|
int32_t frameExtraTop = 0;
|
|
int32_t frameBorder = 0;
|
|
|
|
if (w->type == WidgetFrameE) {
|
|
frameExtraTop = font->charHeight + FRAME_TITLE_GAP;
|
|
frameBorder = FRAME_BORDER;
|
|
pad = DEFAULT_PADDING;
|
|
}
|
|
|
|
int32_t innerX = w->x + pad + frameBorder;
|
|
int32_t innerY = w->y + pad + frameBorder + frameExtraTop;
|
|
int32_t innerW = w->w - pad * 2 - frameBorder * 2;
|
|
int32_t innerH = w->h - pad * 2 - frameBorder * 2 - frameExtraTop;
|
|
|
|
if (innerW < 0) { innerW = 0; }
|
|
if (innerH < 0) { innerH = 0; }
|
|
|
|
int32_t count = countVisibleChildren(w);
|
|
|
|
if (count == 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t totalGap = gap * (count - 1);
|
|
int32_t availMain = horiz ? (innerW - totalGap) : (innerH - totalGap);
|
|
int32_t availCross = horiz ? innerH : innerW;
|
|
|
|
// First pass: sum minimum sizes and total weight
|
|
int32_t totalMin = 0;
|
|
int32_t totalWeight = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (!c->visible) {
|
|
continue;
|
|
}
|
|
|
|
int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
|
|
totalMin += cmin;
|
|
totalWeight += c->weight;
|
|
}
|
|
|
|
int32_t extraSpace = availMain - totalMin;
|
|
|
|
if (extraSpace < 0) {
|
|
extraSpace = 0;
|
|
}
|
|
|
|
// Compute alignment offset for main axis
|
|
int32_t alignOffset = 0;
|
|
|
|
if (totalWeight == 0 && extraSpace > 0) {
|
|
if (w->align == AlignCenterE) {
|
|
alignOffset = extraSpace / 2;
|
|
} else if (w->align == AlignEndE) {
|
|
alignOffset = extraSpace;
|
|
}
|
|
}
|
|
|
|
// Second pass: assign positions and sizes
|
|
int32_t pos = (horiz ? innerX : innerY) + alignOffset;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (!c->visible) {
|
|
continue;
|
|
}
|
|
|
|
int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
|
|
int32_t mainSize = cmin;
|
|
|
|
// Distribute extra space by weight
|
|
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
|
|
mainSize += (extraSpace * c->weight) / totalWeight;
|
|
}
|
|
|
|
// Apply max size constraint
|
|
if (horiz && c->maxW) {
|
|
int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
|
|
|
|
if (mainSize > maxPx) {
|
|
mainSize = maxPx;
|
|
}
|
|
} else if (!horiz && c->maxH) {
|
|
int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
|
|
|
|
if (mainSize > maxPx) {
|
|
mainSize = maxPx;
|
|
}
|
|
}
|
|
|
|
// Assign geometry
|
|
if (horiz) {
|
|
c->x = pos;
|
|
c->y = innerY;
|
|
c->w = mainSize;
|
|
c->h = availCross;
|
|
} else {
|
|
c->x = innerX;
|
|
c->y = pos;
|
|
c->w = availCross;
|
|
c->h = mainSize;
|
|
}
|
|
|
|
// Apply preferred/max on cross axis
|
|
if (horiz && c->maxH) {
|
|
int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
|
|
|
|
if (c->h > maxPx) {
|
|
c->h = maxPx;
|
|
}
|
|
} else if (!horiz && c->maxW) {
|
|
int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
|
|
|
|
if (c->w > maxPx) {
|
|
c->w = maxPx;
|
|
}
|
|
}
|
|
|
|
pos += mainSize + gap;
|
|
|
|
// Recurse into child containers
|
|
layoutChildren(c, font);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// layoutChildren
|
|
// ============================================================
|
|
|
|
static void layoutChildren(WidgetT *w, const BitmapFontT *font) {
|
|
if (w->type == WidgetVBoxE || w->type == WidgetHBoxE || w->type == WidgetFrameE || w->type == WidgetRadioGroupE) {
|
|
layoutBox(w, font);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// paintWidget
|
|
// ============================================================
|
|
|
|
static void paintWidget(WidgetT *w, DisplayT *d, const BlitOpsT *ops,
|
|
const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
if (!w->visible) {
|
|
return;
|
|
}
|
|
|
|
switch (w->type) {
|
|
case WidgetVBoxE:
|
|
case WidgetHBoxE:
|
|
// Containers are transparent — just paint children
|
|
break;
|
|
|
|
case WidgetFrameE: {
|
|
// Draw beveled border
|
|
BevelStyleT bevel;
|
|
bevel.highlight = colors->windowHighlight;
|
|
bevel.shadow = colors->windowShadow;
|
|
bevel.face = 0;
|
|
bevel.width = FRAME_BORDER;
|
|
drawBevel(d, ops, w->x, w->y + font->charHeight / 2,
|
|
w->w, w->h - font->charHeight / 2, &bevel);
|
|
|
|
// Draw title over the top border
|
|
if (w->as.frame.title && w->as.frame.title[0]) {
|
|
int32_t titleW = (int32_t)strlen(w->as.frame.title) * font->charWidth;
|
|
int32_t titleX = w->x + DEFAULT_PADDING + FRAME_BORDER;
|
|
|
|
rectFill(d, ops, titleX - 2, w->y,
|
|
titleW + 4, font->charHeight, colors->windowFace);
|
|
drawText(d, ops, font, titleX, w->y,
|
|
w->as.frame.title, colors->contentFg, colors->windowFace, true);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case WidgetLabelE:
|
|
drawText(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2,
|
|
w->as.label.text, colors->contentFg, colors->contentBg, false);
|
|
break;
|
|
|
|
case WidgetButtonE: {
|
|
BevelStyleT bevel;
|
|
bevel.highlight = w->as.button.pressed ? colors->windowShadow : colors->windowHighlight;
|
|
bevel.shadow = w->as.button.pressed ? colors->windowHighlight : colors->windowShadow;
|
|
bevel.face = colors->buttonFace;
|
|
bevel.width = 2;
|
|
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
|
|
|
int32_t textW = (int32_t)strlen(w->as.button.text) * font->charWidth;
|
|
int32_t textX = w->x + (w->w - textW) / 2;
|
|
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
|
|
|
if (w->as.button.pressed) {
|
|
textX++;
|
|
textY++;
|
|
}
|
|
|
|
drawText(d, ops, font, textX, textY,
|
|
w->as.button.text,
|
|
w->enabled ? colors->contentFg : colors->windowShadow,
|
|
colors->buttonFace, true);
|
|
break;
|
|
}
|
|
|
|
case WidgetCheckboxE: {
|
|
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
|
|
|
// Draw checkbox box
|
|
BevelStyleT bevel;
|
|
bevel.highlight = colors->windowShadow;
|
|
bevel.shadow = colors->windowHighlight;
|
|
bevel.face = colors->contentBg;
|
|
bevel.width = 1;
|
|
drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel);
|
|
|
|
// Draw check mark if checked
|
|
if (w->as.checkbox.checked) {
|
|
int32_t cx = w->x + 3;
|
|
int32_t cy = boxY + 3;
|
|
int32_t cs = CHECKBOX_BOX_SIZE - 6;
|
|
|
|
for (int32_t i = 0; i < cs; i++) {
|
|
drawHLine(d, ops, cx + i, cy + i, 1, colors->contentFg);
|
|
drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
// Draw label
|
|
drawText(d, ops, font,
|
|
w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP,
|
|
w->y + (w->h - font->charHeight) / 2,
|
|
w->as.checkbox.text, colors->contentFg, colors->contentBg, false);
|
|
break;
|
|
}
|
|
|
|
case WidgetRadioE: {
|
|
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
|
|
|
// Draw radio box (same as checkbox for now, could use circle later)
|
|
BevelStyleT bevel;
|
|
bevel.highlight = colors->windowShadow;
|
|
bevel.shadow = colors->windowHighlight;
|
|
bevel.face = colors->contentBg;
|
|
bevel.width = 1;
|
|
drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel);
|
|
|
|
// Draw filled dot if selected
|
|
if (w->parent && w->parent->type == WidgetRadioGroupE &&
|
|
w->parent->as.radioGroup.selectedIdx == w->as.radio.index) {
|
|
rectFill(d, ops, w->x + 3, boxY + 3,
|
|
CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, colors->contentFg);
|
|
}
|
|
|
|
drawText(d, ops, font,
|
|
w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP,
|
|
w->y + (w->h - font->charHeight) / 2,
|
|
w->as.radio.text, colors->contentFg, colors->contentBg, false);
|
|
break;
|
|
}
|
|
|
|
case WidgetTextInputE: {
|
|
// Sunken border
|
|
BevelStyleT bevel;
|
|
bevel.highlight = colors->windowShadow;
|
|
bevel.shadow = colors->windowHighlight;
|
|
bevel.face = colors->contentBg;
|
|
bevel.width = 2;
|
|
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
|
|
|
// Draw text
|
|
if (w->as.textInput.buf) {
|
|
int32_t textX = w->x + TEXT_INPUT_PAD;
|
|
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
|
int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth;
|
|
int32_t off = w->as.textInput.scrollOff;
|
|
int32_t len = w->as.textInput.len - off;
|
|
|
|
if (len > maxChars) {
|
|
len = maxChars;
|
|
}
|
|
|
|
for (int32_t i = 0; i < len; i++) {
|
|
drawChar(d, ops, font, textX + i * font->charWidth, textY,
|
|
w->as.textInput.buf[off + i],
|
|
colors->contentFg, colors->contentBg, true);
|
|
}
|
|
|
|
// Draw cursor
|
|
if (w->focused) {
|
|
int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth;
|
|
|
|
if (cursorX >= w->x + TEXT_INPUT_PAD &&
|
|
cursorX < w->x + w->w - TEXT_INPUT_PAD) {
|
|
drawVLine(d, ops, cursorX, textY, font->charHeight, colors->contentFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case WidgetSpacerE:
|
|
// Invisible — draws nothing
|
|
break;
|
|
|
|
case WidgetSeparatorE:
|
|
if (w->as.separator.vertical) {
|
|
int32_t cx = w->x + w->w / 2;
|
|
drawVLine(d, ops, cx, w->y, w->h, colors->windowShadow);
|
|
drawVLine(d, ops, cx + 1, w->y, w->h, colors->windowHighlight);
|
|
} else {
|
|
int32_t cy = w->y + w->h / 2;
|
|
drawHLine(d, ops, w->x, cy, w->w, colors->windowShadow);
|
|
drawHLine(d, ops, w->x, cy + 1, w->w, colors->windowHighlight);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Paint children
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
paintWidget(c, d, ops, font, colors);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// removeChild
|
|
// ============================================================
|
|
|
|
static void removeChild(WidgetT *parent, WidgetT *child) {
|
|
WidgetT *prev = NULL;
|
|
|
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
|
if (c == child) {
|
|
if (prev) {
|
|
prev->nextSibling = c->nextSibling;
|
|
} else {
|
|
parent->firstChild = c->nextSibling;
|
|
}
|
|
|
|
if (parent->lastChild == child) {
|
|
parent->lastChild = prev;
|
|
}
|
|
|
|
child->nextSibling = NULL;
|
|
child->parent = NULL;
|
|
return;
|
|
}
|
|
|
|
prev = c;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetManageScrollbars
|
|
// ============================================================
|
|
//
|
|
// Checks whether the widget tree's minimum size exceeds the
|
|
// window content area. Adds or removes WM scrollbars as needed,
|
|
// then relayouts the widget tree at the virtual content size.
|
|
|
|
static void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
// Measure the tree without any layout pass
|
|
calcMinSizeTree(root, &ctx->font);
|
|
|
|
// Save old scroll positions before removing scrollbars
|
|
int32_t oldVValue = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t oldHValue = win->hScroll ? win->hScroll->value : 0;
|
|
bool hadVScroll = (win->vScroll != NULL);
|
|
bool hadHScroll = (win->hScroll != NULL);
|
|
|
|
// Remove existing scrollbars to measure full available area
|
|
if (hadVScroll) {
|
|
free(win->vScroll);
|
|
win->vScroll = NULL;
|
|
}
|
|
|
|
if (hadHScroll) {
|
|
free(win->hScroll);
|
|
win->hScroll = NULL;
|
|
}
|
|
|
|
wmUpdateContentRect(win);
|
|
|
|
int32_t availW = win->contentW;
|
|
int32_t availH = win->contentH;
|
|
int32_t minW = root->calcMinW;
|
|
int32_t minH = root->calcMinH;
|
|
|
|
bool needV = (minH > availH);
|
|
bool needH = (minW > availW);
|
|
|
|
// Adding one scrollbar reduces space, which may require the other
|
|
if (needV && !needH) {
|
|
needH = (minW > availW - SCROLLBAR_WIDTH);
|
|
}
|
|
|
|
if (needH && !needV) {
|
|
needV = (minH > availH - SCROLLBAR_WIDTH);
|
|
}
|
|
|
|
bool changed = (needV != hadVScroll) || (needH != hadHScroll);
|
|
|
|
if (needV) {
|
|
int32_t pageV = needH ? availH - SCROLLBAR_WIDTH : availH;
|
|
int32_t maxV = minH - pageV;
|
|
|
|
if (maxV < 0) {
|
|
maxV = 0;
|
|
}
|
|
|
|
wmAddVScrollbar(win, 0, maxV, pageV);
|
|
win->vScroll->value = DVX_MIN(oldVValue, maxV);
|
|
}
|
|
|
|
if (needH) {
|
|
int32_t pageH = needV ? availW - SCROLLBAR_WIDTH : availW;
|
|
int32_t maxH = minW - pageH;
|
|
|
|
if (maxH < 0) {
|
|
maxH = 0;
|
|
}
|
|
|
|
wmAddHScrollbar(win, 0, maxH, pageH);
|
|
win->hScroll->value = DVX_MIN(oldHValue, maxH);
|
|
}
|
|
|
|
if (changed) {
|
|
// wmAddVScrollbar/wmAddHScrollbar already call wmUpdateContentRect
|
|
wmReallocContentBuf(win, &ctx->display);
|
|
}
|
|
|
|
// Install scroll handler
|
|
win->onScroll = widgetOnScroll;
|
|
|
|
// Layout at the virtual content size (the larger of content area and min size)
|
|
int32_t layoutW = DVX_MAX(win->contentW, minW);
|
|
int32_t layoutH = DVX_MAX(win->contentH, minH);
|
|
|
|
wgtLayout(root, layoutW, layoutH, &ctx->font);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnKey
|
|
// ============================================================
|
|
|
|
static void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
// Find the focused widget
|
|
// For now, simple: find a focused text input and send keys to it
|
|
// TODO: proper focus chain / tab navigation
|
|
WidgetT *focus = NULL;
|
|
|
|
// Simple linear scan for focused widget
|
|
WidgetT *stack[64];
|
|
int32_t top = 0;
|
|
stack[top++] = root;
|
|
|
|
while (top > 0) {
|
|
WidgetT *w = stack[--top];
|
|
|
|
if (w->focused && w->type == WidgetTextInputE) {
|
|
focus = w;
|
|
break;
|
|
}
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (top < 64) {
|
|
stack[top++] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!focus || focus->type != WidgetTextInputE) {
|
|
return;
|
|
}
|
|
|
|
// Handle key input for text input widget
|
|
if (key >= 32 && key < 127) {
|
|
// Printable character
|
|
if (focus->as.textInput.len < focus->as.textInput.bufSize - 1) {
|
|
int32_t pos = focus->as.textInput.cursorPos;
|
|
|
|
memmove(focus->as.textInput.buf + pos + 1,
|
|
focus->as.textInput.buf + pos,
|
|
focus->as.textInput.len - pos + 1);
|
|
focus->as.textInput.buf[pos] = (char)key;
|
|
focus->as.textInput.len++;
|
|
focus->as.textInput.cursorPos++;
|
|
|
|
if (focus->onChange) {
|
|
focus->onChange(focus);
|
|
}
|
|
}
|
|
} else if (key == 8) {
|
|
// Backspace
|
|
if (focus->as.textInput.cursorPos > 0) {
|
|
int32_t pos = focus->as.textInput.cursorPos;
|
|
|
|
memmove(focus->as.textInput.buf + pos - 1,
|
|
focus->as.textInput.buf + pos,
|
|
focus->as.textInput.len - pos + 1);
|
|
focus->as.textInput.len--;
|
|
focus->as.textInput.cursorPos--;
|
|
|
|
if (focus->onChange) {
|
|
focus->onChange(focus);
|
|
}
|
|
}
|
|
} else if (key == (0x4B | 0x100)) {
|
|
// Left arrow
|
|
if (focus->as.textInput.cursorPos > 0) {
|
|
focus->as.textInput.cursorPos--;
|
|
}
|
|
} else if (key == (0x4D | 0x100)) {
|
|
// Right arrow
|
|
if (focus->as.textInput.cursorPos < focus->as.textInput.len) {
|
|
focus->as.textInput.cursorPos++;
|
|
}
|
|
} else if (key == (0x47 | 0x100)) {
|
|
// Home
|
|
focus->as.textInput.cursorPos = 0;
|
|
} else if (key == (0x4F | 0x100)) {
|
|
// End
|
|
focus->as.textInput.cursorPos = focus->as.textInput.len;
|
|
} else if (key == (0x53 | 0x100)) {
|
|
// Delete
|
|
if (focus->as.textInput.cursorPos < focus->as.textInput.len) {
|
|
int32_t pos = focus->as.textInput.cursorPos;
|
|
|
|
memmove(focus->as.textInput.buf + pos,
|
|
focus->as.textInput.buf + pos + 1,
|
|
focus->as.textInput.len - pos);
|
|
focus->as.textInput.len--;
|
|
|
|
if (focus->onChange) {
|
|
focus->onChange(focus);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adjust scroll offset to keep cursor visible
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t visibleChars = (focus->w - TEXT_INPUT_PAD * 2) / font->charWidth;
|
|
|
|
if (focus->as.textInput.cursorPos < focus->as.textInput.scrollOff) {
|
|
focus->as.textInput.scrollOff = focus->as.textInput.cursorPos;
|
|
}
|
|
|
|
if (focus->as.textInput.cursorPos >= focus->as.textInput.scrollOff + visibleChars) {
|
|
focus->as.textInput.scrollOff = focus->as.textInput.cursorPos - visibleChars + 1;
|
|
}
|
|
|
|
// Repaint the window
|
|
wgtInvalidate(focus);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnMouse
|
|
// ============================================================
|
|
|
|
static void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root || !(buttons & 1)) {
|
|
return;
|
|
}
|
|
|
|
// Adjust mouse coordinates for scroll offset
|
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t vx = x + scrollX;
|
|
int32_t vy = y + scrollY;
|
|
|
|
WidgetT *hit = hitTest(root, vx, vy);
|
|
|
|
if (!hit) {
|
|
return;
|
|
}
|
|
|
|
// Clear focus from all text inputs, set focus on clicked text input
|
|
WidgetT *stack[64];
|
|
int32_t top = 0;
|
|
stack[top++] = root;
|
|
|
|
while (top > 0) {
|
|
WidgetT *w = stack[--top];
|
|
w->focused = false;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (top < 64) {
|
|
stack[top++] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hit->type == WidgetTextInputE) {
|
|
hit->focused = true;
|
|
|
|
// Place cursor at click position
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t relX = x - hit->x - TEXT_INPUT_PAD;
|
|
int32_t charPos = relX / font->charWidth + hit->as.textInput.scrollOff;
|
|
|
|
if (charPos < 0) {
|
|
charPos = 0;
|
|
}
|
|
|
|
if (charPos > hit->as.textInput.len) {
|
|
charPos = hit->as.textInput.len;
|
|
}
|
|
|
|
hit->as.textInput.cursorPos = charPos;
|
|
}
|
|
|
|
if (hit->type == WidgetButtonE && hit->enabled) {
|
|
hit->as.button.pressed = true;
|
|
wgtInvalidate(hit);
|
|
|
|
// The button release will be handled by the next mouse event
|
|
// For now, just fire onClick on press
|
|
if (hit->onClick) {
|
|
hit->onClick(hit);
|
|
}
|
|
|
|
hit->as.button.pressed = false;
|
|
}
|
|
|
|
if (hit->type == WidgetCheckboxE && hit->enabled) {
|
|
hit->as.checkbox.checked = !hit->as.checkbox.checked;
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
}
|
|
|
|
if (hit->type == WidgetRadioE && hit->enabled && hit->parent &&
|
|
hit->parent->type == WidgetRadioGroupE) {
|
|
hit->parent->as.radioGroup.selectedIdx = hit->as.radio.index;
|
|
|
|
if (hit->parent->onChange) {
|
|
hit->parent->onChange(hit->parent);
|
|
}
|
|
}
|
|
|
|
wgtInvalidate(root);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnPaint
|
|
// ============================================================
|
|
|
|
static void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
|
(void)dirtyArea;
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
// Get context from root's userData
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
// Set up a display context pointing at the content buffer
|
|
DisplayT cd = ctx->display;
|
|
cd.backBuf = win->contentBuf;
|
|
cd.width = win->contentW;
|
|
cd.height = win->contentH;
|
|
cd.pitch = win->contentPitch;
|
|
cd.clipX = 0;
|
|
cd.clipY = 0;
|
|
cd.clipW = win->contentW;
|
|
cd.clipH = win->contentH;
|
|
|
|
// Clear background
|
|
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg);
|
|
|
|
// Apply scroll offset — layout at virtual size, positioned at -scroll
|
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t layoutW = DVX_MAX(win->contentW, root->calcMinW);
|
|
int32_t layoutH = DVX_MAX(win->contentH, root->calcMinH);
|
|
|
|
root->x = -scrollX;
|
|
root->y = -scrollY;
|
|
root->w = layoutW;
|
|
root->h = layoutH;
|
|
layoutChildren(root, &ctx->font);
|
|
|
|
// Paint widget tree (clip rect limits drawing to visible area)
|
|
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnResize
|
|
// ============================================================
|
|
|
|
static void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) {
|
|
(void)newW;
|
|
(void)newH;
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
widgetManageScrollbars(win, ctx);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnScroll
|
|
// ============================================================
|
|
|
|
static void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
|
(void)orient;
|
|
(void)value;
|
|
|
|
// Repaint with new scroll position
|
|
if (win->onPaint) {
|
|
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
|
win->onPaint(win, &fullRect);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtButton
|
|
// ============================================================
|
|
|
|
WidgetT *wgtButton(WidgetT *parent, const char *text) {
|
|
WidgetT *w = allocWidget(parent, WidgetButtonE);
|
|
|
|
if (w) {
|
|
w->as.button.text = text;
|
|
w->as.button.pressed = false;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtCheckbox
|
|
// ============================================================
|
|
|
|
WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
|
|
WidgetT *w = allocWidget(parent, WidgetCheckboxE);
|
|
|
|
if (w) {
|
|
w->as.checkbox.text = text;
|
|
w->as.checkbox.checked = false;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDestroy
|
|
// ============================================================
|
|
|
|
void wgtDestroy(WidgetT *w) {
|
|
if (!w) {
|
|
return;
|
|
}
|
|
|
|
if (w->parent) {
|
|
removeChild(w->parent, w);
|
|
}
|
|
|
|
destroyChildren(w);
|
|
|
|
if (w->type == WidgetTextInputE) {
|
|
free(w->as.textInput.buf);
|
|
} else if (w->type == WidgetTextAreaE) {
|
|
free(w->as.textArea.buf);
|
|
}
|
|
|
|
// If this is the root, clear the window's reference
|
|
if (w->window && w->window->widgetRoot == w) {
|
|
w->window->widgetRoot = NULL;
|
|
}
|
|
|
|
free(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtFind
|
|
// ============================================================
|
|
|
|
WidgetT *wgtFind(WidgetT *root, const char *name) {
|
|
if (!root || !name) {
|
|
return NULL;
|
|
}
|
|
|
|
if (root->name[0] && strcmp(root->name, name) == 0) {
|
|
return root;
|
|
}
|
|
|
|
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
|
|
WidgetT *found = wgtFind(c, name);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtFrame
|
|
// ============================================================
|
|
|
|
WidgetT *wgtFrame(WidgetT *parent, const char *title) {
|
|
WidgetT *w = allocWidget(parent, WidgetFrameE);
|
|
|
|
if (w) {
|
|
w->as.frame.title = title;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtGetText
|
|
// ============================================================
|
|
|
|
const char *wgtGetText(const WidgetT *w) {
|
|
if (!w) {
|
|
return "";
|
|
}
|
|
|
|
switch (w->type) {
|
|
case WidgetLabelE: return w->as.label.text;
|
|
case WidgetButtonE: return w->as.button.text;
|
|
case WidgetCheckboxE: return w->as.checkbox.text;
|
|
case WidgetRadioE: return w->as.radio.text;
|
|
case WidgetTextInputE: return w->as.textInput.buf ? w->as.textInput.buf : "";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtHBox
|
|
// ============================================================
|
|
|
|
WidgetT *wgtHBox(WidgetT *parent) {
|
|
return allocWidget(parent, WidgetHBoxE);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtHSeparator
|
|
// ============================================================
|
|
|
|
WidgetT *wgtHSeparator(WidgetT *parent) {
|
|
WidgetT *w = allocWidget(parent, WidgetSeparatorE);
|
|
|
|
if (w) {
|
|
w->as.separator.vertical = false;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtInitWindow
|
|
// ============================================================
|
|
|
|
WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
|
|
WidgetT *root = allocWidget(NULL, WidgetVBoxE);
|
|
|
|
if (!root) {
|
|
return NULL;
|
|
}
|
|
|
|
root->window = win;
|
|
root->userData = ctx;
|
|
|
|
win->widgetRoot = root;
|
|
win->onPaint = widgetOnPaint;
|
|
win->onMouse = widgetOnMouse;
|
|
win->onKey = widgetOnKey;
|
|
win->onResize = widgetOnResize;
|
|
|
|
// Layout and paint are deferred until the caller adds widgets
|
|
// and calls wgtInvalidate(root) or until the first resize/paint event.
|
|
|
|
return root;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtInvalidate
|
|
// ============================================================
|
|
|
|
void wgtInvalidate(WidgetT *w) {
|
|
if (!w || !w->window) {
|
|
return;
|
|
}
|
|
|
|
// Find the root
|
|
WidgetT *root = w;
|
|
|
|
while (root->parent) {
|
|
root = root->parent;
|
|
}
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
// Manage scrollbars (measures, adds/removes scrollbars, relayouts)
|
|
widgetManageScrollbars(w->window, ctx);
|
|
|
|
// Repaint
|
|
RectT fullRect = {0, 0, w->window->contentW, w->window->contentH};
|
|
widgetOnPaint(w->window, &fullRect);
|
|
|
|
// Dirty the window on screen
|
|
dvxInvalidateWindow(ctx, w->window);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtLabel
|
|
// ============================================================
|
|
|
|
WidgetT *wgtLabel(WidgetT *parent, const char *text) {
|
|
WidgetT *w = allocWidget(parent, WidgetLabelE);
|
|
|
|
if (w) {
|
|
w->as.label.text = text;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtLayout
|
|
// ============================================================
|
|
|
|
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH,
|
|
const BitmapFontT *font) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
// Measure pass
|
|
calcMinSizeTree(root, font);
|
|
|
|
// Layout pass
|
|
root->x = 0;
|
|
root->y = 0;
|
|
root->w = availW;
|
|
root->h = availH;
|
|
|
|
layoutChildren(root, font);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListBox
|
|
// ============================================================
|
|
|
|
WidgetT *wgtListBox(WidgetT *parent) {
|
|
WidgetT *w = allocWidget(parent, WidgetListBoxE);
|
|
|
|
if (w) {
|
|
w->as.listBox.selectedIdx = -1;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListBoxGetSelected
|
|
// ============================================================
|
|
|
|
int32_t wgtListBoxGetSelected(const WidgetT *w) {
|
|
if (!w || w->type != WidgetListBoxE) {
|
|
return -1;
|
|
}
|
|
|
|
return w->as.listBox.selectedIdx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListBoxSetItems
|
|
// ============================================================
|
|
|
|
void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
|
|
if (!w || w->type != WidgetListBoxE) {
|
|
return;
|
|
}
|
|
|
|
w->as.listBox.items = items;
|
|
w->as.listBox.itemCount = count;
|
|
|
|
if (w->as.listBox.selectedIdx >= count) {
|
|
w->as.listBox.selectedIdx = -1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListBoxSetSelected
|
|
// ============================================================
|
|
|
|
void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
|
|
if (!w || w->type != WidgetListBoxE) {
|
|
return;
|
|
}
|
|
|
|
w->as.listBox.selectedIdx = idx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtPaint
|
|
// ============================================================
|
|
|
|
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops,
|
|
const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
paintWidget(root, d, ops, font, colors);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtRadio
|
|
// ============================================================
|
|
|
|
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
|
WidgetT *w = allocWidget(parent, WidgetRadioE);
|
|
|
|
if (w) {
|
|
w->as.radio.text = 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 = allocWidget(parent, WidgetRadioGroupE);
|
|
|
|
if (w) {
|
|
w->as.radioGroup.selectedIdx = 0;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtResolveSize
|
|
// ============================================================
|
|
|
|
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
|
|
if (taggedSize == 0) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK;
|
|
int32_t value = taggedSize & WGT_SIZE_VAL_MASK;
|
|
|
|
switch (sizeType) {
|
|
case WGT_SIZE_PIXELS:
|
|
return value;
|
|
|
|
case WGT_SIZE_CHARS:
|
|
return value * charWidth;
|
|
|
|
case WGT_SIZE_PERCENT:
|
|
return (parentSize * value) / 100;
|
|
|
|
default:
|
|
return value;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtSetEnabled
|
|
// ============================================================
|
|
|
|
void wgtSetEnabled(WidgetT *w, bool enabled) {
|
|
if (w) {
|
|
w->enabled = enabled;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtSetText
|
|
// ============================================================
|
|
|
|
void wgtSetText(WidgetT *w, const char *text) {
|
|
if (!w) {
|
|
return;
|
|
}
|
|
|
|
switch (w->type) {
|
|
case WidgetLabelE:
|
|
w->as.label.text = text;
|
|
break;
|
|
|
|
case WidgetButtonE:
|
|
w->as.button.text = text;
|
|
break;
|
|
|
|
case WidgetCheckboxE:
|
|
w->as.checkbox.text = text;
|
|
break;
|
|
|
|
case WidgetRadioE:
|
|
w->as.radio.text = text;
|
|
break;
|
|
|
|
case WidgetTextInputE:
|
|
if (w->as.textInput.buf) {
|
|
strncpy(w->as.textInput.buf, text, w->as.textInput.bufSize - 1);
|
|
w->as.textInput.buf[w->as.textInput.bufSize - 1] = '\0';
|
|
w->as.textInput.len = (int32_t)strlen(w->as.textInput.buf);
|
|
w->as.textInput.cursorPos = w->as.textInput.len;
|
|
w->as.textInput.scrollOff = 0;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtSetVisible
|
|
// ============================================================
|
|
|
|
void wgtSetVisible(WidgetT *w, bool visible) {
|
|
if (w) {
|
|
w->visible = visible;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtSpacer
|
|
// ============================================================
|
|
|
|
WidgetT *wgtSpacer(WidgetT *parent) {
|
|
WidgetT *w = allocWidget(parent, WidgetSpacerE);
|
|
|
|
if (w) {
|
|
w->weight = 100; // spacers stretch by default
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtTextArea
|
|
// ============================================================
|
|
|
|
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
|
WidgetT *w = allocWidget(parent, WidgetTextAreaE);
|
|
|
|
if (w) {
|
|
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
|
|
w->as.textArea.buf = (char *)malloc(bufSize);
|
|
w->as.textArea.bufSize = bufSize;
|
|
|
|
if (w->as.textArea.buf) {
|
|
w->as.textArea.buf[0] = '\0';
|
|
}
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtTextInput
|
|
// ============================================================
|
|
|
|
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) {
|
|
WidgetT *w = allocWidget(parent, WidgetTextInputE);
|
|
|
|
if (w) {
|
|
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
|
|
w->as.textInput.buf = (char *)malloc(bufSize);
|
|
w->as.textInput.bufSize = bufSize;
|
|
|
|
if (w->as.textInput.buf) {
|
|
w->as.textInput.buf[0] = '\0';
|
|
}
|
|
|
|
w->weight = 100; // text inputs stretch by default
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtVBox
|
|
// ============================================================
|
|
|
|
WidgetT *wgtVBox(WidgetT *parent) {
|
|
return allocWidget(parent, WidgetVBoxE);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtVSeparator
|
|
// ============================================================
|
|
|
|
WidgetT *wgtVSeparator(WidgetT *parent) {
|
|
WidgetT *w = allocWidget(parent, WidgetSeparatorE);
|
|
|
|
if (w) {
|
|
w->as.separator.vertical = true;
|
|
}
|
|
|
|
return w;
|
|
}
|