(mostly) Decoupled widget code from GUI code.

This commit is contained in:
Scott Duensing 2026-03-12 21:48:42 -05:00
parent 9fc21c2f83
commit 291eaf0c35
24 changed files with 1325 additions and 886 deletions

View file

@ -14,6 +14,7 @@ LIBDIR = ../lib
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c
WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetClass.c \
widgets/widgetCore.c \
widgets/widgetLayout.c \
widgets/widgetEvent.c \
@ -77,6 +78,7 @@ $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvx
# Widget file dependencies
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
$(WOBJDIR)/widgetClass.o: widgets/widgetClass.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetAnsiTerm.o: widgets/widgetAnsiTerm.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)

View file

@ -329,15 +329,13 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
case WidgetCheckboxE:
widgetClearFocus(win->widgetRoot);
target->focused = true;
widgetCheckboxOnMouse(target);
widgetCheckboxOnMouse(target, win->widgetRoot, 0, 0);
wgtInvalidate(target);
return true;
case WidgetRadioE:
widgetClearFocus(win->widgetRoot);
target->focused = true;
widgetRadioOnMouse(target);
widgetRadioOnMouse(target, win->widgetRoot, 0, 0);
wgtInvalidate(target);
return true;

View file

@ -6,8 +6,9 @@
#include <time.h>
// Forward declaration
// Forward declarations
struct AppContextT;
struct WidgetClassT;
// ============================================================
// Size specifications
@ -101,6 +102,7 @@ typedef enum {
typedef struct WidgetT {
WidgetTypeE type;
const struct WidgetClassT *wclass;
char name[MAX_WIDGET_NAME];
// Tree linkage

View file

@ -1303,6 +1303,16 @@ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) {
}
// ============================================================
// widgetAnsiTermDestroy
// ============================================================
void widgetAnsiTermDestroy(WidgetT *w) {
free(w->as.ansiTerm.cells);
free(w->as.ansiTerm.scrollback);
}
// ============================================================
// widgetAnsiTermCalcMinSize
// ============================================================
@ -1389,6 +1399,8 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key) {
if (len > 0) {
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len);
}
wgtInvalidate(w);
}
@ -1398,7 +1410,9 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key) {
//
// Handle mouse clicks: scrollbar interaction and focus.
void widgetAnsiTermOnMouse(WidgetT *hit, int32_t vx, int32_t vy, const BitmapFontT *font) {
void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
AppContextT *actx = (AppContextT *)root->userData;
const BitmapFontT *font = &actx->font;
hit->focused = true;
int32_t sbCount = hit->as.ansiTerm.scrollbackCount;

View file

@ -20,6 +20,25 @@ WidgetT *wgtButton(WidgetT *parent, const char *text) {
}
// ============================================================
// widgetButtonGetText
// ============================================================
const char *widgetButtonGetText(const WidgetT *w) {
return w->as.button.text;
}
// ============================================================
// widgetButtonSetText
// ============================================================
void widgetButtonSetText(WidgetT *w, const char *text) {
w->as.button.text = text;
w->accelKey = accelParse(text);
}
// ============================================================
// widgetButtonCalcMinSize
// ============================================================
@ -30,13 +49,30 @@ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetButtonOnKey
// ============================================================
void widgetButtonOnKey(WidgetT *w, int32_t key) {
if (key == ' ' || key == 0x0D) {
w->as.button.pressed = true;
sKeyPressedBtn = w;
wgtInvalidate(w);
}
}
// ============================================================
// widgetButtonOnMouse
// ============================================================
void widgetButtonOnMouse(WidgetT *hit) {
hit->as.button.pressed = true;
sPressedButton = hit;
void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->focused = true;
w->as.button.pressed = true;
sPressedButton = w;
}

View file

@ -594,6 +594,15 @@ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) {
}
// ============================================================
// widgetCanvasDestroy
// ============================================================
void widgetCanvasDestroy(WidgetT *w) {
free(w->as.canvas.data);
}
// ============================================================
// widgetCanvasCalcMinSize
// ============================================================
@ -609,7 +618,8 @@ void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetCanvasOnMouse
// ============================================================
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy) {
void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
// Convert widget coords to canvas coords
int32_t cx = vx - hit->x - CANVAS_BORDER;
int32_t cy = vy - hit->y - CANVAS_BORDER;

View file

@ -20,6 +20,25 @@ WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
}
// ============================================================
// widgetCheckboxGetText
// ============================================================
const char *widgetCheckboxGetText(const WidgetT *w) {
return w->as.checkbox.text;
}
// ============================================================
// widgetCheckboxSetText
// ============================================================
void widgetCheckboxSetText(WidgetT *w, const char *text) {
w->as.checkbox.text = text;
w->accelKey = accelParse(text);
}
// ============================================================
// widgetCheckboxCalcMinSize
// ============================================================
@ -31,15 +50,36 @@ void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetCheckboxOnKey
// ============================================================
void widgetCheckboxOnKey(WidgetT *w, int32_t key) {
if (key == ' ' || key == 0x0D) {
w->as.checkbox.checked = !w->as.checkbox.checked;
if (w->onChange) {
w->onChange(w);
}
wgtInvalidate(w);
}
}
// ============================================================
// widgetCheckboxOnMouse
// ============================================================
void widgetCheckboxOnMouse(WidgetT *hit) {
hit->as.checkbox.checked = !hit->as.checkbox.checked;
void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->focused = true;
w->as.checkbox.checked = !w->as.checkbox.checked;
if (hit->onChange) {
hit->onChange(hit);
if (w->onChange) {
w->onChange(w);
}
}

392
dvx/widgets/widgetClass.c Normal file
View file

@ -0,0 +1,392 @@
// widgetClass.c — Widget class vtable definitions
#include "widgetInternal.h"
// ============================================================
// Per-type class definitions
// ============================================================
static const WidgetClassT sClassVBox = {
.flags = WCLASS_BOX_CONTAINER,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassHBox = {
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassLabel = {
.flags = 0,
.paint = widgetLabelPaint,
.paintOverlay = NULL,
.calcMinSize = widgetLabelCalcMinSize,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = widgetLabelGetText,
.setText = widgetLabelSetText
};
static const WidgetClassT sClassButton = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetButtonPaint,
.paintOverlay = NULL,
.calcMinSize = widgetButtonCalcMinSize,
.layout = NULL,
.onMouse = widgetButtonOnMouse,
.onKey = widgetButtonOnKey,
.destroy = NULL,
.getText = widgetButtonGetText,
.setText = widgetButtonSetText
};
static const WidgetClassT sClassCheckbox = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetCheckboxPaint,
.paintOverlay = NULL,
.calcMinSize = widgetCheckboxCalcMinSize,
.layout = NULL,
.onMouse = widgetCheckboxOnMouse,
.onKey = widgetCheckboxOnKey,
.destroy = NULL,
.getText = widgetCheckboxGetText,
.setText = widgetCheckboxSetText
};
static const WidgetClassT sClassRadioGroup = {
.flags = WCLASS_BOX_CONTAINER,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassRadio = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetRadioPaint,
.paintOverlay = NULL,
.calcMinSize = widgetRadioCalcMinSize,
.layout = NULL,
.onMouse = widgetRadioOnMouse,
.onKey = widgetRadioOnKey,
.destroy = NULL,
.getText = widgetRadioGetText,
.setText = widgetRadioSetText
};
static const WidgetClassT sClassTextInput = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetTextInputPaint,
.paintOverlay = NULL,
.calcMinSize = widgetTextInputCalcMinSize,
.layout = NULL,
.onMouse = widgetTextInputOnMouse,
.onKey = widgetTextInputOnKey,
.destroy = widgetTextInputDestroy,
.getText = widgetTextInputGetText,
.setText = widgetTextInputSetText
};
static const WidgetClassT sClassTextArea = {
.flags = 0,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = widgetTextAreaDestroy,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassListBox = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetListBoxPaint,
.paintOverlay = NULL,
.calcMinSize = widgetListBoxCalcMinSize,
.layout = NULL,
.onMouse = widgetListBoxOnMouse,
.onKey = widgetListBoxOnKey,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassSpacer = {
.flags = 0,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = widgetSpacerCalcMinSize,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassSeparator = {
.flags = 0,
.paint = widgetSeparatorPaint,
.paintOverlay = NULL,
.calcMinSize = widgetSeparatorCalcMinSize,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassFrame = {
.flags = WCLASS_BOX_CONTAINER,
.paint = widgetFramePaint,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassDropdown = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetDropdownPaint,
.paintOverlay = widgetDropdownPaintPopup,
.calcMinSize = widgetDropdownCalcMinSize,
.layout = NULL,
.onMouse = widgetDropdownOnMouse,
.onKey = widgetDropdownOnKey,
.destroy = NULL,
.getText = widgetDropdownGetText,
.setText = NULL
};
static const WidgetClassT sClassComboBox = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetComboBoxPaint,
.paintOverlay = widgetComboBoxPaintPopup,
.calcMinSize = widgetComboBoxCalcMinSize,
.layout = NULL,
.onMouse = widgetComboBoxOnMouse,
.onKey = widgetComboBoxOnKey,
.destroy = widgetComboBoxDestroy,
.getText = widgetComboBoxGetText,
.setText = widgetComboBoxSetText
};
static const WidgetClassT sClassProgressBar = {
.flags = 0,
.paint = widgetProgressBarPaint,
.paintOverlay = NULL,
.calcMinSize = widgetProgressBarCalcMinSize,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassSlider = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetSliderPaint,
.paintOverlay = NULL,
.calcMinSize = widgetSliderCalcMinSize,
.layout = NULL,
.onMouse = widgetSliderOnMouse,
.onKey = widgetSliderOnKey,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassTabControl = {
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN,
.paint = widgetTabControlPaint,
.paintOverlay = NULL,
.calcMinSize = widgetTabControlCalcMinSize,
.layout = widgetTabControlLayout,
.onMouse = widgetTabControlOnMouse,
.onKey = widgetTabControlOnKey,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassTabPage = {
.flags = WCLASS_BOX_CONTAINER,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassStatusBar = {
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
.paint = widgetStatusBarPaint,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassToolbar = {
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
.paint = widgetToolbarPaint,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassTreeView = {
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
.paint = widgetTreeViewPaint,
.paintOverlay = NULL,
.calcMinSize = widgetTreeViewCalcMinSize,
.layout = widgetTreeViewLayout,
.onMouse = widgetTreeViewOnMouse,
.onKey = widgetTreeViewOnKey,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassTreeItem = {
.flags = 0,
.paint = NULL,
.paintOverlay = NULL,
.calcMinSize = NULL,
.layout = NULL,
.onMouse = NULL,
.onKey = NULL,
.destroy = NULL,
.getText = widgetTreeItemGetText,
.setText = widgetTreeItemSetText
};
static const WidgetClassT sClassImage = {
.flags = 0,
.paint = widgetImagePaint,
.paintOverlay = NULL,
.calcMinSize = widgetImageCalcMinSize,
.layout = NULL,
.onMouse = widgetImageOnMouse,
.onKey = NULL,
.destroy = widgetImageDestroy,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassImageButton = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetImageButtonPaint,
.paintOverlay = NULL,
.calcMinSize = widgetImageButtonCalcMinSize,
.layout = NULL,
.onMouse = widgetImageButtonOnMouse,
.onKey = widgetImageButtonOnKey,
.destroy = widgetImageButtonDestroy,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassCanvas = {
.flags = 0,
.paint = widgetCanvasPaint,
.paintOverlay = NULL,
.calcMinSize = widgetCanvasCalcMinSize,
.layout = NULL,
.onMouse = widgetCanvasOnMouse,
.onKey = NULL,
.destroy = widgetCanvasDestroy,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassAnsiTerm = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetAnsiTermPaint,
.paintOverlay = NULL,
.calcMinSize = widgetAnsiTermCalcMinSize,
.layout = NULL,
.onMouse = widgetAnsiTermOnMouse,
.onKey = widgetAnsiTermOnKey,
.destroy = widgetAnsiTermDestroy,
.getText = NULL,
.setText = NULL
};
// ============================================================
// Class table — indexed by WidgetTypeE
// ============================================================
const WidgetClassT *widgetClassTable[] = {
[WidgetVBoxE] = &sClassVBox,
[WidgetHBoxE] = &sClassHBox,
[WidgetLabelE] = &sClassLabel,
[WidgetButtonE] = &sClassButton,
[WidgetCheckboxE] = &sClassCheckbox,
[WidgetRadioGroupE] = &sClassRadioGroup,
[WidgetRadioE] = &sClassRadio,
[WidgetTextInputE] = &sClassTextInput,
[WidgetTextAreaE] = &sClassTextArea,
[WidgetListBoxE] = &sClassListBox,
[WidgetSpacerE] = &sClassSpacer,
[WidgetSeparatorE] = &sClassSeparator,
[WidgetFrameE] = &sClassFrame,
[WidgetDropdownE] = &sClassDropdown,
[WidgetComboBoxE] = &sClassComboBox,
[WidgetProgressBarE] = &sClassProgressBar,
[WidgetSliderE] = &sClassSlider,
[WidgetTabControlE] = &sClassTabControl,
[WidgetTabPageE] = &sClassTabPage,
[WidgetStatusBarE] = &sClassStatusBar,
[WidgetToolbarE] = &sClassToolbar,
[WidgetTreeViewE] = &sClassTreeView,
[WidgetTreeItemE] = &sClassTreeItem,
[WidgetImageE] = &sClassImage,
[WidgetImageButtonE] = &sClassImageButton,
[WidgetCanvasE] = &sClassCanvas,
[WidgetAnsiTermE] = &sClassAnsiTerm
};

View file

@ -80,6 +80,39 @@ void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
}
// ============================================================
// widgetComboBoxDestroy
// ============================================================
void widgetComboBoxDestroy(WidgetT *w) {
free(w->as.comboBox.buf);
}
// ============================================================
// widgetComboBoxGetText
// ============================================================
const char *widgetComboBoxGetText(const WidgetT *w) {
return w->as.comboBox.buf ? w->as.comboBox.buf : "";
}
// ============================================================
// widgetComboBoxSetText
// ============================================================
void widgetComboBoxSetText(WidgetT *w, const char *text) {
if (w->as.comboBox.buf) {
strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
}
}
// ============================================================
// widgetComboBoxCalcMinSize
// ============================================================
@ -100,36 +133,130 @@ void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetComboBoxOnKey
// ============================================================
void widgetComboBoxOnKey(WidgetT *w, int32_t key) {
if (w->as.comboBox.open) {
if (key == (0x48 | 0x100)) {
if (w->as.comboBox.hoverIdx > 0) {
w->as.comboBox.hoverIdx--;
if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
}
}
wgtInvalidate(w);
return;
}
if (key == (0x50 | 0x100)) {
if (w->as.comboBox.hoverIdx < w->as.comboBox.itemCount - 1) {
w->as.comboBox.hoverIdx++;
if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
wgtInvalidate(w);
return;
}
if (key == 0x0D) {
int32_t idx = w->as.comboBox.hoverIdx;
if (idx >= 0 && idx < w->as.comboBox.itemCount) {
w->as.comboBox.selectedIdx = idx;
const char *itemText = w->as.comboBox.items[idx];
strncpy(w->as.comboBox.buf, itemText, w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
}
w->as.comboBox.open = false;
sOpenPopup = NULL;
if (w->onChange) {
w->onChange(w);
}
wgtInvalidate(w);
return;
}
}
// Down arrow on closed combobox opens the popup
if (!w->as.comboBox.open && key == (0x50 | 0x100)) {
w->as.comboBox.open = true;
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
sOpenPopup = w;
if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
}
wgtInvalidate(w);
return;
}
// Text editing (when popup is closed, or non-navigation keys with popup open)
if (!w->as.comboBox.buf) {
return;
}
widgetTextEditOnKey(w, key, w->as.comboBox.buf, w->as.comboBox.bufSize,
&w->as.comboBox.len, &w->as.comboBox.cursorPos,
&w->as.comboBox.scrollOff);
}
// ============================================================
// widgetComboBoxOnMouse
// ============================================================
void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx) {
// Check if click is on the button area
int32_t textAreaW = hit->w - DROPDOWN_BTN_WIDTH;
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vy;
w->focused = true;
// Check if click is on the button area
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
if (vx >= w->x + textAreaW) {
// If this combobox's popup was just closed by click-outside, don't re-open
if (w == sClosedPopup) {
return;
}
if (vx >= hit->x + textAreaW) {
// Button click — toggle popup
hit->as.comboBox.open = !hit->as.comboBox.open;
hit->as.comboBox.hoverIdx = hit->as.comboBox.selectedIdx;
sOpenPopup = hit->as.comboBox.open ? hit : NULL;
w->as.comboBox.open = !w->as.comboBox.open;
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
sOpenPopup = w->as.comboBox.open ? w : NULL;
} else {
// Text area click — focus for editing
hit->focused = true;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t relX = vx - hit->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + hit->as.comboBox.scrollOff;
int32_t relX = vx - w->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > hit->as.comboBox.len) {
charPos = hit->as.comboBox.len;
if (charPos > w->as.comboBox.len) {
charPos = w->as.comboBox.len;
}
hit->as.comboBox.cursorPos = charPos;
w->as.comboBox.cursorPos = charPos;
}
}

View file

@ -45,6 +45,7 @@ WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
memset(w, 0, sizeof(*w));
w->type = type;
w->wclass = widgetClassTable[type];
w->visible = true;
w->enabled = true;
@ -102,21 +103,8 @@ void widgetDestroyChildren(WidgetT *w) {
WidgetT *next = child->nextSibling;
widgetDestroyChildren(child);
if (child->type == WidgetTextInputE) {
free(child->as.textInput.buf);
} else if (child->type == WidgetTextAreaE) {
free(child->as.textArea.buf);
} else if (child->type == WidgetComboBoxE) {
free(child->as.comboBox.buf);
} else if (child->type == WidgetImageE) {
free(child->as.image.data);
} else if (child->type == WidgetCanvasE) {
free(child->as.canvas.data);
} else if (child->type == WidgetImageButtonE) {
free(child->as.imageButton.data);
} else if (child->type == WidgetAnsiTermE) {
free(child->as.ansiTerm.cells);
free(child->as.ansiTerm.scrollback);
if (child->wclass && child->wclass->destroy) {
child->wclass->destroy(child);
}
// Clear popup/drag references if they point to destroyed widgets
@ -366,8 +354,8 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
return NULL;
}
// TreeView manages its own children — don't recurse
if (w->type == WidgetTreeViewE) {
// Widgets with WCLASS_NO_HIT_RECURSE manage their own children
if (w->wclass && (w->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
return w;
}
@ -391,12 +379,7 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
// ============================================================
bool widgetIsFocusable(WidgetTypeE type) {
return type == WidgetTextInputE || type == WidgetComboBoxE ||
type == WidgetDropdownE || type == WidgetCheckboxE ||
type == WidgetRadioE || type == WidgetButtonE ||
type == WidgetImageButtonE || type == WidgetSliderE ||
type == WidgetListBoxE || type == WidgetTreeViewE ||
type == WidgetAnsiTermE || type == WidgetTabControlE;
return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
}
@ -407,9 +390,7 @@ bool widgetIsFocusable(WidgetTypeE type) {
// Returns true for widget types that use the generic box layout.
bool widgetIsBoxContainer(WidgetTypeE type) {
return type == WidgetVBoxE || type == WidgetHBoxE || type == WidgetFrameE ||
type == WidgetRadioGroupE || type == WidgetTabPageE ||
type == WidgetStatusBarE || type == WidgetToolbarE;
return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0;
}
@ -420,7 +401,7 @@ bool widgetIsBoxContainer(WidgetTypeE type) {
// Returns true for container types that lay out children horizontally.
bool widgetIsHorizContainer(WidgetTypeE type) {
return type == WidgetHBoxE || type == WidgetStatusBarE || type == WidgetToolbarE;
return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0;
}

View file

@ -63,6 +63,19 @@ void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
}
// ============================================================
// widgetDropdownGetText
// ============================================================
const char *widgetDropdownGetText(const WidgetT *w) {
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
return w->as.dropdown.items[w->as.dropdown.selectedIdx];
}
return "";
}
// ============================================================
// widgetDropdownCalcMinSize
// ============================================================
@ -84,14 +97,85 @@ void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetDropdownOnKey
// ============================================================
void widgetDropdownOnKey(WidgetT *w, int32_t key) {
if (w->as.dropdown.open) {
// Popup is open — navigate items
if (key == (0x48 | 0x100)) {
if (w->as.dropdown.hoverIdx > 0) {
w->as.dropdown.hoverIdx--;
if (w->as.dropdown.hoverIdx < w->as.dropdown.scrollPos) {
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx;
}
}
} else if (key == (0x50 | 0x100)) {
if (w->as.dropdown.hoverIdx < w->as.dropdown.itemCount - 1) {
w->as.dropdown.hoverIdx++;
if (w->as.dropdown.hoverIdx >= w->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
} else if (key == 0x0D || key == ' ') {
w->as.dropdown.selectedIdx = w->as.dropdown.hoverIdx;
w->as.dropdown.open = false;
sOpenPopup = NULL;
if (w->onChange) {
w->onChange(w);
}
}
} else {
// Popup is closed
if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) {
w->as.dropdown.open = true;
w->as.dropdown.hoverIdx = w->as.dropdown.selectedIdx;
sOpenPopup = w;
if (w->as.dropdown.hoverIdx >= w->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (w->as.dropdown.hoverIdx < w->as.dropdown.scrollPos) {
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx;
}
} else if (key == (0x48 | 0x100)) {
if (w->as.dropdown.selectedIdx > 0) {
w->as.dropdown.selectedIdx--;
if (w->onChange) {
w->onChange(w);
}
}
}
}
wgtInvalidate(w);
}
// ============================================================
// widgetDropdownOnMouse
// ============================================================
void widgetDropdownOnMouse(WidgetT *hit) {
hit->as.dropdown.open = !hit->as.dropdown.open;
hit->as.dropdown.hoverIdx = hit->as.dropdown.selectedIdx;
sOpenPopup = hit->as.dropdown.open ? hit : NULL;
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->focused = true;
// If this dropdown's popup was just closed by click-outside, don't re-open
if (w == sClosedPopup) {
return;
}
w->as.dropdown.open = !w->as.dropdown.open;
w->as.dropdown.hoverIdx = w->as.dropdown.selectedIdx;
sOpenPopup = w->as.dropdown.open ? w : NULL;
}

View file

@ -2,6 +2,10 @@
#include "widgetInternal.h"
// Widget whose popup was just closed by click-outside — prevents
// immediate re-open on the same click.
WidgetT *sClosedPopup = NULL;
// ============================================================
// widgetManageScrollbars
@ -137,476 +141,10 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return;
}
// Handle ANSI terminal key input
if (focus->type == WidgetAnsiTermE) {
widgetAnsiTermOnKey(focus, key);
wgtInvalidate(focus);
return;
// Dispatch to per-widget onKey handler via vtable
if (focus->wclass && focus->wclass->onKey) {
focus->wclass->onKey(focus, key);
}
// Handle tree view keyboard navigation
if (focus->type == WidgetTreeViewE) {
widgetTreeViewOnKey(focus, key);
return;
}
// Handle list box keyboard navigation
if (focus->type == WidgetListBoxE) {
widgetListBoxOnKey(focus, key);
return;
}
// Handle button keyboard activation
if (focus->type == WidgetButtonE) {
if (key == ' ' || key == 0x0D) {
focus->as.button.pressed = true;
sKeyPressedBtn = focus;
wgtInvalidate(focus);
}
return;
}
// Handle image button keyboard activation
if (focus->type == WidgetImageButtonE) {
if (key == ' ' || key == 0x0D) {
focus->as.imageButton.pressed = true;
sKeyPressedBtn = focus;
wgtInvalidate(focus);
}
return;
}
// Handle checkbox keyboard toggle
if (focus->type == WidgetCheckboxE) {
if (key == ' ' || key == 0x0D) {
focus->as.checkbox.checked = !focus->as.checkbox.checked;
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
}
return;
}
// Handle radio button keyboard navigation
if (focus->type == WidgetRadioE) {
if (key == ' ' || key == 0x0D) {
// Select this radio
if (focus->parent && focus->parent->type == WidgetRadioGroupE) {
focus->parent->as.radioGroup.selectedIdx = focus->as.radio.index;
if (focus->parent->onChange) {
focus->parent->onChange(focus->parent);
}
}
wgtInvalidate(focus);
} else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) {
// Down or Right — next radio in group
if (focus->parent && focus->parent->type == WidgetRadioGroupE) {
WidgetT *next = NULL;
for (WidgetT *s = focus->nextSibling; s; s = s->nextSibling) {
if (s->type == WidgetRadioE && s->visible && s->enabled) {
next = s;
break;
}
}
if (next) {
focus->focused = false;
next->focused = true;
next->parent->as.radioGroup.selectedIdx = next->as.radio.index;
if (next->parent->onChange) {
next->parent->onChange(next->parent);
}
wgtInvalidate(next);
}
}
} else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) {
// Up or Left — previous radio in group
if (focus->parent && focus->parent->type == WidgetRadioGroupE) {
WidgetT *prev = NULL;
for (WidgetT *s = focus->parent->firstChild; s && s != focus; s = s->nextSibling) {
if (s->type == WidgetRadioE && s->visible && s->enabled) {
prev = s;
}
}
if (prev) {
focus->focused = false;
prev->focused = true;
prev->parent->as.radioGroup.selectedIdx = prev->as.radio.index;
if (prev->parent->onChange) {
prev->parent->onChange(prev->parent);
}
wgtInvalidate(prev);
}
}
}
return;
}
// Handle slider keyboard adjustment
if (focus->type == WidgetSliderE) {
int32_t step = 1;
int32_t range = focus->as.slider.maxValue - focus->as.slider.minValue;
if (range > 100) {
step = range / 100;
}
if (focus->as.slider.vertical) {
if (key == (0x48 | 0x100)) {
// Up — decrease value
focus->as.slider.value -= step;
} else if (key == (0x50 | 0x100)) {
// Down — increase value
focus->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
// Home — minimum
focus->as.slider.value = focus->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
// End — maximum
focus->as.slider.value = focus->as.slider.maxValue;
} else {
return;
}
} else {
if (key == (0x4B | 0x100)) {
// Left — decrease value
focus->as.slider.value -= step;
} else if (key == (0x4D | 0x100)) {
// Right — increase value
focus->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
// Home — minimum
focus->as.slider.value = focus->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
// End — maximum
focus->as.slider.value = focus->as.slider.maxValue;
} else {
return;
}
}
// Clamp
focus->as.slider.value = clampInt(focus->as.slider.value, focus->as.slider.minValue, focus->as.slider.maxValue);
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
return;
}
// Handle tab control keyboard navigation
if (focus->type == WidgetTabControlE) {
int32_t tabCount = 0;
for (WidgetT *c = focus->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTabPageE) {
tabCount++;
}
}
if (tabCount > 1) {
int32_t active = focus->as.tabControl.activeTab;
if (key == (0x4D | 0x100)) {
// Right — next tab (wrap)
active = (active + 1) % tabCount;
} else if (key == (0x4B | 0x100)) {
// Left — previous tab (wrap)
active = (active - 1 + tabCount) % tabCount;
} else if (key == (0x47 | 0x100)) {
// Home — first tab
active = 0;
} else if (key == (0x4F | 0x100)) {
// End — last tab
active = tabCount - 1;
} else {
return;
}
if (active != focus->as.tabControl.activeTab) {
if (sOpenPopup) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
focus->as.tabControl.activeTab = active;
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
}
}
return;
}
// Handle dropdown keyboard navigation
if (focus->type == WidgetDropdownE) {
if (focus->as.dropdown.open) {
// Popup is open — navigate items
if (key == (0x48 | 0x100)) {
// Up arrow
if (focus->as.dropdown.hoverIdx > 0) {
focus->as.dropdown.hoverIdx--;
if (focus->as.dropdown.hoverIdx < focus->as.dropdown.scrollPos) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx;
}
}
} else if (key == (0x50 | 0x100)) {
// Down arrow
if (focus->as.dropdown.hoverIdx < focus->as.dropdown.itemCount - 1) {
focus->as.dropdown.hoverIdx++;
if (focus->as.dropdown.hoverIdx >= focus->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
} else if (key == 0x0D || key == ' ') {
// Enter or Space — select item and close
focus->as.dropdown.selectedIdx = focus->as.dropdown.hoverIdx;
focus->as.dropdown.open = false;
sOpenPopup = NULL;
if (focus->onChange) {
focus->onChange(focus);
}
}
} else {
// Popup is closed
if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) {
// Down arrow, Space, or Enter — open popup
focus->as.dropdown.open = true;
focus->as.dropdown.hoverIdx = focus->as.dropdown.selectedIdx;
sOpenPopup = focus;
// Ensure scroll position shows the selected item
if (focus->as.dropdown.hoverIdx >= focus->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (focus->as.dropdown.hoverIdx < focus->as.dropdown.scrollPos) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx;
}
} else if (key == (0x48 | 0x100)) {
// Up arrow — select previous item without opening
if (focus->as.dropdown.selectedIdx > 0) {
focus->as.dropdown.selectedIdx--;
if (focus->onChange) {
focus->onChange(focus);
}
}
}
}
wgtInvalidate(focus);
return;
}
// Handle combobox popup keyboard navigation
if (focus->type == WidgetComboBoxE && focus->as.comboBox.open) {
if (key == (0x48 | 0x100)) {
// Up arrow
if (focus->as.comboBox.hoverIdx > 0) {
focus->as.comboBox.hoverIdx--;
if (focus->as.comboBox.hoverIdx < focus->as.comboBox.listScrollPos) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx;
}
}
wgtInvalidate(focus);
return;
}
if (key == (0x50 | 0x100)) {
// Down arrow
if (focus->as.comboBox.hoverIdx < focus->as.comboBox.itemCount - 1) {
focus->as.comboBox.hoverIdx++;
if (focus->as.comboBox.hoverIdx >= focus->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
wgtInvalidate(focus);
return;
}
if (key == 0x0D) {
// Enter — select item, copy text, close
int32_t idx = focus->as.comboBox.hoverIdx;
if (idx >= 0 && idx < focus->as.comboBox.itemCount) {
focus->as.comboBox.selectedIdx = idx;
const char *itemText = focus->as.comboBox.items[idx];
strncpy(focus->as.comboBox.buf, itemText, focus->as.comboBox.bufSize - 1);
focus->as.comboBox.buf[focus->as.comboBox.bufSize - 1] = '\0';
focus->as.comboBox.len = (int32_t)strlen(focus->as.comboBox.buf);
focus->as.comboBox.cursorPos = focus->as.comboBox.len;
focus->as.comboBox.scrollOff = 0;
}
focus->as.comboBox.open = false;
sOpenPopup = NULL;
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
return;
}
}
// Down arrow on closed combobox opens the popup
if (focus->type == WidgetComboBoxE && !focus->as.comboBox.open && key == (0x50 | 0x100)) {
focus->as.comboBox.open = true;
focus->as.comboBox.hoverIdx = focus->as.comboBox.selectedIdx;
sOpenPopup = focus;
if (focus->as.comboBox.hoverIdx >= focus->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (focus->as.comboBox.hoverIdx < focus->as.comboBox.listScrollPos) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx;
}
wgtInvalidate(focus);
return;
}
// Handle text input for TextInput and ComboBox
char *buf = NULL;
int32_t bufSize = 0;
int32_t *pLen = NULL;
int32_t *pCursor = NULL;
int32_t *pScrollOff = NULL;
if (focus->type == WidgetTextInputE) {
buf = focus->as.textInput.buf;
bufSize = focus->as.textInput.bufSize;
pLen = &focus->as.textInput.len;
pCursor = &focus->as.textInput.cursorPos;
pScrollOff = &focus->as.textInput.scrollOff;
} else if (focus->type == WidgetComboBoxE) {
buf = focus->as.comboBox.buf;
bufSize = focus->as.comboBox.bufSize;
pLen = &focus->as.comboBox.len;
pCursor = &focus->as.comboBox.cursorPos;
pScrollOff = &focus->as.comboBox.scrollOff;
}
if (!buf) {
return;
}
if (key >= 32 && key < 127) {
// Printable character
if (*pLen < bufSize - 1) {
int32_t pos = *pCursor;
memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
buf[pos] = (char)key;
(*pLen)++;
(*pCursor)++;
if (focus->onChange) {
focus->onChange(focus);
}
}
} else if (key == 8) {
// Backspace
if (*pCursor > 0) {
int32_t pos = *pCursor;
memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
(*pLen)--;
(*pCursor)--;
if (focus->onChange) {
focus->onChange(focus);
}
}
} else if (key == (0x4B | 0x100)) {
// Left arrow
if (*pCursor > 0) {
(*pCursor)--;
}
} else if (key == (0x4D | 0x100)) {
// Right arrow
if (*pCursor < *pLen) {
(*pCursor)++;
}
} else if (key == (0x47 | 0x100)) {
// Home
*pCursor = 0;
} else if (key == (0x4F | 0x100)) {
// End
*pCursor = *pLen;
} else if (key == (0x53 | 0x100)) {
// Delete
if (*pCursor < *pLen) {
int32_t pos = *pCursor;
memmove(buf + pos, buf + pos + 1, *pLen - pos);
(*pLen)--;
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 fieldW = focus->w;
if (focus->type == WidgetComboBoxE) {
fieldW -= DROPDOWN_BTN_WIDTH;
}
int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth;
if (*pCursor < *pScrollOff) {
*pScrollOff = *pCursor;
}
if (*pCursor >= *pScrollOff + visibleChars) {
*pScrollOff = *pCursor - visibleChars + 1;
}
// Repaint the window
wgtInvalidate(focus);
}
@ -616,7 +154,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
WidgetT *root = win->widgetRoot;
WidgetT *closedPopup = NULL;
sClosedPopup = NULL;
if (!root) {
return;
@ -644,7 +182,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
// Handle canvas drawing (mouse move while pressed)
if (sDrawingCanvas && (buttons & 1)) {
widgetCanvasOnMouse(sDrawingCanvas, x, y);
widgetCanvasOnMouse(sDrawingCanvas, root, x, y);
wgtInvalidate(root);
return;
}
@ -813,7 +351,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
}
// Click outside popup — close it and remember which widget it was
closedPopup = sOpenPopup;
sClosedPopup = sOpenPopup;
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
@ -858,73 +396,9 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
}
}
// Dispatch to per-widget mouse handlers
if (hit->type == WidgetTextInputE) {
widgetTextInputOnMouse(hit, root, vx);
}
if (hit->type == WidgetButtonE && hit->enabled) {
hit->focused = true;
widgetButtonOnMouse(hit);
}
if (hit->type == WidgetCheckboxE && hit->enabled) {
hit->focused = true;
widgetCheckboxOnMouse(hit);
}
if (hit->type == WidgetRadioE && hit->enabled) {
hit->focused = true;
widgetRadioOnMouse(hit);
}
if (hit->type == WidgetImageE && hit->enabled) {
widgetImageOnMouse(hit);
}
if (hit->type == WidgetImageButtonE && hit->enabled) {
hit->focused = true;
widgetImageButtonOnMouse(hit);
}
if (hit->type == WidgetCanvasE && hit->enabled) {
widgetCanvasOnMouse(hit, vx, vy);
}
if (hit->type == WidgetListBoxE && hit->enabled) {
widgetListBoxOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetDropdownE && hit->enabled) {
if (hit != closedPopup) {
widgetDropdownOnMouse(hit);
}
}
if (hit->type == WidgetComboBoxE && hit->enabled) {
if (hit != closedPopup || vx < hit->x + hit->w - DROPDOWN_BTN_WIDTH) {
widgetComboBoxOnMouse(hit, root, vx);
}
}
if (hit->type == WidgetSliderE && hit->enabled) {
hit->focused = true;
widgetSliderOnMouse(hit, vx, vy);
}
if (hit->type == WidgetTabControlE && hit->enabled) {
hit->focused = true;
widgetTabControlOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetTreeViewE && hit->enabled) {
hit->focused = true;
widgetTreeViewOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetAnsiTermE && hit->enabled) {
AppContextT *actx = (AppContextT *)root->userData;
widgetAnsiTermOnMouse(hit, vx, vy, &actx->font);
// Dispatch to per-widget mouse handler via vtable
if (hit->enabled && hit->wclass && hit->wclass->onMouse) {
hit->wclass->onMouse(hit, root, vx, vy);
}
wgtInvalidate(root);

View file

@ -112,6 +112,15 @@ void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int3
}
// ============================================================
// widgetImageDestroy
// ============================================================
void widgetImageDestroy(WidgetT *w) {
free(w->as.image.data);
}
// ============================================================
// widgetImageCalcMinSize
// ============================================================
@ -127,15 +136,18 @@ void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetImageOnMouse
// ============================================================
void widgetImageOnMouse(WidgetT *hit) {
hit->as.image.pressed = true;
wgtInvalidate(hit);
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->as.image.pressed = true;
wgtInvalidate(w);
if (hit->onClick) {
hit->onClick(hit);
if (w->onClick) {
w->onClick(w);
}
hit->as.image.pressed = false;
w->as.image.pressed = false;
}

View file

@ -45,6 +45,15 @@ void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH
}
// ============================================================
// widgetImageButtonDestroy
// ============================================================
void widgetImageButtonDestroy(WidgetT *w) {
free(w->as.imageButton.data);
}
// ============================================================
// widgetImageButtonCalcMinSize
// ============================================================
@ -58,13 +67,30 @@ void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetImageButtonOnKey
// ============================================================
void widgetImageButtonOnKey(WidgetT *w, int32_t key) {
if (key == ' ' || key == 0x0D) {
w->as.imageButton.pressed = true;
sKeyPressedBtn = w;
wgtInvalidate(w);
}
}
// ============================================================
// widgetImageButtonOnMouse
// ============================================================
void widgetImageButtonOnMouse(WidgetT *hit) {
hit->as.imageButton.pressed = true;
sPressedButton = hit;
void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->focused = true;
w->as.imageButton.pressed = true;
sPressedButton = w;
}

View file

@ -12,6 +12,31 @@
#include <string.h>
#include <stdio.h>
// ============================================================
// Widget class vtable
// ============================================================
#define WCLASS_FOCUSABLE 0x0001
#define WCLASS_BOX_CONTAINER 0x0002
#define WCLASS_HORIZ_CONTAINER 0x0004
#define WCLASS_PAINTS_CHILDREN 0x0008
#define WCLASS_NO_HIT_RECURSE 0x0010
typedef struct WidgetClassT {
uint32_t flags;
void (*paint)(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void (*paintOverlay)(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void (*calcMinSize)(WidgetT *w, const BitmapFontT *font);
void (*layout)(WidgetT *w, const BitmapFontT *font);
void (*onMouse)(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void (*onKey)(WidgetT *w, int32_t key);
void (*destroy)(WidgetT *w);
const char *(*getText)(const WidgetT *w);
void (*setText)(WidgetT *w, const char *text);
} WidgetClassT;
extern const WidgetClassT *widgetClassTable[];
// ============================================================
// Constants
// ============================================================
@ -56,6 +81,7 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
// ============================================================
extern bool sDebugLayout;
extern WidgetT *sClosedPopup;
extern WidgetT *sKeyPressedBtn;
extern WidgetT *sOpenPopup;
extern WidgetT *sPressedButton;
@ -154,6 +180,7 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font);
@ -168,25 +195,67 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
// ============================================================
// Per-widget mouse functions
// Per-widget getText/setText functions
// ============================================================
const char *widgetButtonGetText(const WidgetT *w);
void widgetButtonSetText(WidgetT *w, const char *text);
const char *widgetCheckboxGetText(const WidgetT *w);
void widgetCheckboxSetText(WidgetT *w, const char *text);
const char *widgetComboBoxGetText(const WidgetT *w);
void widgetComboBoxSetText(WidgetT *w, const char *text);
const char *widgetDropdownGetText(const WidgetT *w);
const char *widgetLabelGetText(const WidgetT *w);
void widgetLabelSetText(WidgetT *w, const char *text);
const char *widgetRadioGetText(const WidgetT *w);
void widgetRadioSetText(WidgetT *w, const char *text);
const char *widgetTextInputGetText(const WidgetT *w);
void widgetTextInputSetText(WidgetT *w, const char *text);
const char *widgetTreeItemGetText(const WidgetT *w);
void widgetTreeItemSetText(WidgetT *w, const char *text);
// ============================================================
// Per-widget destroy functions
// ============================================================
void widgetAnsiTermDestroy(WidgetT *w);
void widgetCanvasDestroy(WidgetT *w);
void widgetComboBoxDestroy(WidgetT *w);
void widgetImageButtonDestroy(WidgetT *w);
void widgetImageDestroy(WidgetT *w);
void widgetTextAreaDestroy(WidgetT *w);
void widgetTextInputDestroy(WidgetT *w);
// ============================================================
// Per-widget mouse/key functions
// ============================================================
void widgetAnsiTermOnMouse(WidgetT *hit, int32_t vx, int32_t vy, const BitmapFontT *font);
void widgetAnsiTermOnKey(WidgetT *w, int32_t key);
void widgetButtonOnMouse(WidgetT *hit);
void widgetImageButtonOnMouse(WidgetT *hit);
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
void widgetCheckboxOnMouse(WidgetT *hit);
void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
void widgetDropdownOnMouse(WidgetT *hit);
void widgetImageOnMouse(WidgetT *hit);
void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetButtonOnKey(WidgetT *w, int32_t key);
void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetCanvasOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetCheckboxOnKey(WidgetT *w, int32_t key);
void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetComboBoxOnKey(WidgetT *w, int32_t key);
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetDropdownOnKey(WidgetT *w, int32_t key);
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetImageButtonOnKey(WidgetT *w, int32_t key);
void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetListBoxOnKey(WidgetT *w, int32_t key);
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void widgetRadioOnMouse(WidgetT *hit);
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
void widgetListBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetRadioOnKey(WidgetT *w, int32_t key);
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetSliderOnKey(WidgetT *w, int32_t key);
void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetTabControlOnKey(WidgetT *w, int32_t key);
void widgetTabControlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetTextEditOnKey(WidgetT *w, int32_t key, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff);
void widgetTextInputOnKey(WidgetT *w, int32_t key);
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetTreeViewOnKey(WidgetT *w, int32_t key);
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
#endif // WIDGET_INTERNAL_H

View file

@ -19,6 +19,25 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text) {
}
// ============================================================
// widgetLabelGetText
// ============================================================
const char *widgetLabelGetText(const WidgetT *w) {
return w->as.label.text;
}
// ============================================================
// widgetLabelSetText
// ============================================================
void widgetLabelSetText(WidgetT *w, const char *text) {
w->as.label.text = text;
w->accelKey = accelParse(text);
}
// ============================================================
// widgetLabelCalcMinSize
// ============================================================

View file

@ -90,76 +90,11 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
if (widgetIsBoxContainer(w->type)) {
widgetCalcMinSizeBox(w, font);
} else if (w->type == WidgetTabControlE) {
widgetTabControlCalcMinSize(w, font);
} else if (w->type == WidgetTreeViewE) {
widgetTreeViewCalcMinSize(w, font);
} else {
// Leaf widgets
switch (w->type) {
case WidgetLabelE:
widgetLabelCalcMinSize(w, font);
break;
case WidgetButtonE:
widgetButtonCalcMinSize(w, font);
break;
case WidgetCheckboxE:
widgetCheckboxCalcMinSize(w, font);
break;
case WidgetRadioE:
widgetRadioCalcMinSize(w, font);
break;
case WidgetTextInputE:
widgetTextInputCalcMinSize(w, font);
break;
case WidgetSpacerE:
widgetSpacerCalcMinSize(w, font);
break;
case WidgetDropdownE:
widgetDropdownCalcMinSize(w, font);
break;
case WidgetImageE:
widgetImageCalcMinSize(w, font);
break;
case WidgetListBoxE:
widgetListBoxCalcMinSize(w, font);
break;
case WidgetImageButtonE:
widgetImageButtonCalcMinSize(w, font);
break;
case WidgetCanvasE:
widgetCanvasCalcMinSize(w, font);
break;
case WidgetComboBoxE:
widgetComboBoxCalcMinSize(w, font);
break;
case WidgetProgressBarE:
widgetProgressBarCalcMinSize(w, font);
break;
case WidgetSliderE:
widgetSliderCalcMinSize(w, font);
break;
case WidgetAnsiTermE:
widgetAnsiTermCalcMinSize(w, font);
break;
case WidgetSeparatorE:
if (w->as.separator.vertical) {
w->calcMinW = SEPARATOR_THICKNESS;
w->calcMinH = 0;
} else if (w->wclass && w->wclass->calcMinSize) {
w->wclass->calcMinSize(w, font);
} else {
w->calcMinW = 0;
w->calcMinH = SEPARATOR_THICKNESS;
}
break;
case WidgetTreeItemE:
w->calcMinW = 0;
w->calcMinH = 0;
break;
default:
w->calcMinW = 0;
w->calcMinH = 0;
break;
}
}
// Apply size hints (override calculated minimum)
@ -339,10 +274,8 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
if (widgetIsBoxContainer(w->type)) {
widgetLayoutBox(w, font);
} else if (w->type == WidgetTabControlE) {
widgetTabControlLayout(w, font);
} else if (w->type == WidgetTreeViewE) {
widgetTreeViewLayout(w, font);
} else if (w->wclass && w->wclass->layout) {
w->wclass->layout(w, font);
}
}

View file

@ -49,107 +49,21 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
return;
}
switch (w->type) {
case WidgetVBoxE:
case WidgetHBoxE:
// Containers are transparent — just paint children
break;
// Paint this widget via vtable
if (w->wclass && w->wclass->paint) {
w->wclass->paint(w, d, ops, font, colors);
}
case WidgetFrameE:
widgetFramePaint(w, d, ops, font, colors);
break;
case WidgetImageE:
widgetImagePaint(w, d, ops, font, colors);
break;
case WidgetImageButtonE:
widgetImageButtonPaint(w, d, ops, font, colors);
break;
case WidgetCanvasE:
widgetCanvasPaint(w, d, ops, font, colors);
break;
case WidgetLabelE:
widgetLabelPaint(w, d, ops, font, colors);
break;
case WidgetButtonE:
widgetButtonPaint(w, d, ops, font, colors);
break;
case WidgetCheckboxE:
widgetCheckboxPaint(w, d, ops, font, colors);
break;
case WidgetRadioE:
widgetRadioPaint(w, d, ops, font, colors);
break;
case WidgetTextInputE:
widgetTextInputPaint(w, d, ops, font, colors);
break;
case WidgetSpacerE:
// Invisible — draws nothing
break;
case WidgetSeparatorE:
widgetSeparatorPaint(w, d, ops, font, colors);
break;
case WidgetDropdownE:
widgetDropdownPaint(w, d, ops, font, colors);
break;
case WidgetComboBoxE:
widgetComboBoxPaint(w, d, ops, font, colors);
break;
case WidgetListBoxE:
widgetListBoxPaint(w, d, ops, font, colors);
break;
case WidgetProgressBarE:
widgetProgressBarPaint(w, d, ops, font, colors);
break;
case WidgetSliderE:
widgetSliderPaint(w, d, ops, font, colors);
break;
case WidgetTabControlE:
widgetTabControlPaint(w, d, ops, font, colors);
// Widgets that paint their own children return early
if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
if (sDebugLayout) {
debugContainerBorder(w, d, ops);
}
return; // handles its own children
case WidgetStatusBarE:
widgetStatusBarPaint(w, d, ops, font, colors);
break;
case WidgetToolbarE:
widgetToolbarPaint(w, d, ops, font, colors);
break;
case WidgetTreeViewE:
widgetTreeViewPaint(w, d, ops, font, colors);
if (sDebugLayout) {
debugContainerBorder(w, d, ops);
}
return; // handles its own children
case WidgetAnsiTermE:
widgetAnsiTermPaint(w, d, ops, font, colors);
break;
default:
break;
return;
}
// Paint children (TabControl and TreeView return early above)
// Paint children
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
widgetPaintOne(c, d, ops, font, colors);
}
@ -184,10 +98,8 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const
return;
}
if (sOpenPopup->type == WidgetDropdownE) {
widgetDropdownPaintPopup(sOpenPopup, d, ops, font, colors);
} else if (sOpenPopup->type == WidgetComboBoxE) {
widgetComboBoxPaintPopup(sOpenPopup, d, ops, font, colors);
if (sOpenPopup->wclass && sOpenPopup->wclass->paintOverlay) {
sOpenPopup->wclass->paintOverlay(sOpenPopup, d, ops, font, colors);
}
}
@ -207,19 +119,8 @@ void wgtDestroy(WidgetT *w) {
widgetDestroyChildren(w);
if (w->type == WidgetTextInputE) {
free(w->as.textInput.buf);
} else if (w->type == WidgetTextAreaE) {
free(w->as.textArea.buf);
} else if (w->type == WidgetComboBoxE) {
free(w->as.comboBox.buf);
} else if (w->type == WidgetImageE) {
free(w->as.image.data);
} else if (w->type == WidgetCanvasE) {
free(w->as.canvas.data);
} else if (w->type == WidgetAnsiTermE) {
free(w->as.ansiTerm.cells);
free(w->as.ansiTerm.scrollback);
if (w->wclass && w->wclass->destroy) {
w->wclass->destroy(w);
}
// Clear static references
@ -278,21 +179,11 @@ const char *wgtGetText(const WidgetT *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 : "";
case WidgetComboBoxE: return w->as.comboBox.buf ? w->as.comboBox.buf : "";
case WidgetTreeItemE: return w->as.treeItem.text ? w->as.treeItem.text : "";
case WidgetDropdownE:
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
return w->as.dropdown.items[w->as.dropdown.selectedIdx];
if (w->wclass && w->wclass->getText) {
return w->wclass->getText(w);
}
return "";
default: return "";
}
}
@ -397,53 +288,8 @@ void wgtSetText(WidgetT *w, const char *text) {
return;
}
switch (w->type) {
case WidgetLabelE:
w->as.label.text = text;
w->accelKey = accelParse(text);
break;
case WidgetButtonE:
w->as.button.text = text;
w->accelKey = accelParse(text);
break;
case WidgetCheckboxE:
w->as.checkbox.text = text;
w->accelKey = accelParse(text);
break;
case WidgetRadioE:
w->as.radio.text = text;
w->accelKey = accelParse(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;
case WidgetComboBoxE:
if (w->as.comboBox.buf) {
strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
}
break;
case WidgetTreeItemE:
w->as.treeItem.text = text;
break;
default:
break;
if (w->wclass && w->wclass->setText) {
w->wclass->setText(w, text);
}
}

View file

@ -45,6 +45,25 @@ WidgetT *wgtRadioGroup(WidgetT *parent) {
}
// ============================================================
// 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
// ============================================================
@ -56,16 +75,88 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetRadioOnKey
// ============================================================
void widgetRadioOnKey(WidgetT *w, int32_t key) {
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);
}
}
wgtInvalidate(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;
next->parent->as.radioGroup.selectedIdx = next->as.radio.index;
if (next->parent->onChange) {
next->parent->onChange(next->parent);
}
wgtInvalidate(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;
prev->parent->as.radioGroup.selectedIdx = prev->as.radio.index;
if (prev->parent->onChange) {
prev->parent->onChange(prev->parent);
}
wgtInvalidate(prev);
}
}
}
}
// ============================================================
// widgetRadioOnMouse
// ============================================================
void widgetRadioOnMouse(WidgetT *hit) {
if (hit->parent && hit->parent->type == WidgetRadioGroupE) {
hit->parent->as.radioGroup.selectedIdx = hit->as.radio.index;
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->focused = true;
if (hit->parent->onChange) {
hit->parent->onChange(hit->parent);
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);
}
}
}

View file

@ -33,6 +33,23 @@ WidgetT *wgtVSeparator(WidgetT *parent) {
}
// ============================================================
// widgetSeparatorCalcMinSize
// ============================================================
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
if (w->as.separator.vertical) {
w->calcMinW = SEPARATOR_THICKNESS;
w->calcMinH = 0;
} else {
w->calcMinW = 0;
w->calcMinH = SEPARATOR_THICKNESS;
}
}
// ============================================================
// widgetSeparatorPaint
// ============================================================

View file

@ -73,11 +73,61 @@ void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetSliderOnKey
// ============================================================
void widgetSliderOnKey(WidgetT *w, int32_t key) {
int32_t step = 1;
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
if (range > 100) {
step = range / 100;
}
if (w->as.slider.vertical) {
if (key == (0x48 | 0x100)) {
w->as.slider.value -= step;
} else if (key == (0x50 | 0x100)) {
w->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
w->as.slider.value = w->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
w->as.slider.value = w->as.slider.maxValue;
} else {
return;
}
} else {
if (key == (0x4B | 0x100)) {
w->as.slider.value -= step;
} else if (key == (0x4D | 0x100)) {
w->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
w->as.slider.value = w->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
w->as.slider.value = w->as.slider.maxValue;
} else {
return;
}
}
w->as.slider.value = clampInt(w->as.slider.value, w->as.slider.minValue, w->as.slider.maxValue);
if (w->onChange) {
w->onChange(w);
}
wgtInvalidate(w);
}
// ============================================================
// widgetSliderOnMouse
// ============================================================
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy) {
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
hit->focused = true;
int32_t range = hit->as.slider.maxValue - hit->as.slider.minValue;
if (range <= 0) {

View file

@ -127,11 +127,65 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
}
// ============================================================
// widgetTabControlOnKey
// ============================================================
void widgetTabControlOnKey(WidgetT *w, int32_t key) {
int32_t tabCount = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTabPageE) {
tabCount++;
}
}
if (tabCount <= 1) {
return;
}
int32_t active = w->as.tabControl.activeTab;
if (key == (0x4D | 0x100)) {
active = (active + 1) % tabCount;
} else if (key == (0x4B | 0x100)) {
active = (active - 1 + tabCount) % tabCount;
} else if (key == (0x47 | 0x100)) {
active = 0;
} else if (key == (0x4F | 0x100)) {
active = tabCount - 1;
} else {
return;
}
if (active != w->as.tabControl.activeTab) {
if (sOpenPopup) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
w->as.tabControl.activeTab = active;
if (w->onChange) {
w->onChange(w);
}
wgtInvalidate(w);
}
}
// ============================================================
// widgetTabControlOnMouse
// ============================================================
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->focused = true;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t tabH = font->charHeight + TAB_PAD_V * 2;

View file

@ -24,6 +24,15 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
}
// ============================================================
// widgetTextAreaDestroy
// ============================================================
void widgetTextAreaDestroy(WidgetT *w) {
free(w->as.textArea.buf);
}
// ============================================================
// wgtTextInput
// ============================================================
@ -47,6 +56,39 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) {
}
// ============================================================
// widgetTextInputDestroy
// ============================================================
void widgetTextInputDestroy(WidgetT *w) {
free(w->as.textInput.buf);
}
// ============================================================
// widgetTextInputGetText
// ============================================================
const char *widgetTextInputGetText(const WidgetT *w) {
return w->as.textInput.buf ? w->as.textInput.buf : "";
}
// ============================================================
// widgetTextInputSetText
// ============================================================
void widgetTextInputSetText(WidgetT *w, const char *text) {
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;
}
}
// ============================================================
// widgetTextInputCalcMinSize
// ============================================================
@ -61,24 +103,125 @@ void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetTextInputOnMouse
// ============================================================
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx) {
hit->focused = true;
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vy;
w->focused = true;
// Place cursor at click position
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t relX = vx - hit->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + hit->as.textInput.scrollOff;
int32_t relX = vx - w->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > hit->as.textInput.len) {
charPos = hit->as.textInput.len;
if (charPos > w->as.textInput.len) {
charPos = w->as.textInput.len;
}
hit->as.textInput.cursorPos = charPos;
w->as.textInput.cursorPos = charPos;
}
// ============================================================
// widgetTextEditOnKey — shared text editing logic
// ============================================================
void widgetTextEditOnKey(WidgetT *w, int32_t key, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff) {
if (key >= 32 && key < 127) {
// Printable character
if (*pLen < bufSize - 1) {
int32_t pos = *pCursor;
memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
buf[pos] = (char)key;
(*pLen)++;
(*pCursor)++;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == 8) {
// Backspace
if (*pCursor > 0) {
int32_t pos = *pCursor;
memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
(*pLen)--;
(*pCursor)--;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == (0x4B | 0x100)) {
// Left arrow
if (*pCursor > 0) {
(*pCursor)--;
}
} else if (key == (0x4D | 0x100)) {
// Right arrow
if (*pCursor < *pLen) {
(*pCursor)++;
}
} else if (key == (0x47 | 0x100)) {
// Home
*pCursor = 0;
} else if (key == (0x4F | 0x100)) {
// End
*pCursor = *pLen;
} else if (key == (0x53 | 0x100)) {
// Delete
if (*pCursor < *pLen) {
int32_t pos = *pCursor;
memmove(buf + pos, buf + pos + 1, *pLen - pos);
(*pLen)--;
if (w->onChange) {
w->onChange(w);
}
}
}
// Adjust scroll offset to keep cursor visible
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t fieldW = w->w;
if (w->type == WidgetComboBoxE) {
fieldW -= DROPDOWN_BTN_WIDTH;
}
int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth;
if (*pCursor < *pScrollOff) {
*pScrollOff = *pCursor;
}
if (*pCursor >= *pScrollOff + visibleChars) {
*pScrollOff = *pCursor - visibleChars + 1;
}
wgtInvalidate(w);
}
// ============================================================
// widgetTextInputOnKey
// ============================================================
void widgetTextInputOnKey(WidgetT *w, int32_t key) {
if (!w->as.textInput.buf) {
return;
}
widgetTextEditOnKey(w, key, w->as.textInput.buf, w->as.textInput.bufSize,
&w->as.textInput.len, &w->as.textInput.cursorPos,
&w->as.textInput.scrollOff);
}

View file

@ -548,6 +548,24 @@ WidgetT *wgtTreeItem(WidgetT *parent, const char *text) {
}
// ============================================================
// widgetTreeItemGetText
// ============================================================
const char *widgetTreeItemGetText(const WidgetT *w) {
return w->as.treeItem.text ? w->as.treeItem.text : "";
}
// ============================================================
// widgetTreeItemSetText
// ============================================================
void widgetTreeItemSetText(WidgetT *w, const char *text) {
w->as.treeItem.text = text;
}
// ============================================================
// wgtTreeItemIsExpanded
// ============================================================
@ -791,6 +809,7 @@ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
// ============================================================
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->focused = true;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;