From 291eaf0c35fa5fecb1c6be95127eebda09df163c Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 12 Mar 2026 21:48:42 -0500 Subject: [PATCH] (mostly) Decoupled widget code from GUI code. --- dvx/Makefile | 2 + dvx/dvxApp.c | 6 +- dvx/dvxWidget.h | 8 +- dvx/widgets/widgetAnsiTerm.c | 16 +- dvx/widgets/widgetButton.c | 42 ++- dvx/widgets/widgetCanvas.c | 12 +- dvx/widgets/widgetCheckbox.c | 48 ++- dvx/widgets/widgetClass.c | 392 +++++++++++++++++++++++ dvx/widgets/widgetComboBox.c | 153 ++++++++- dvx/widgets/widgetCore.c | 35 +- dvx/widgets/widgetDropdown.c | 92 +++++- dvx/widgets/widgetEvent.c | 552 +------------------------------- dvx/widgets/widgetImage.c | 24 +- dvx/widgets/widgetImageButton.c | 32 +- dvx/widgets/widgetInternal.h | 99 +++++- dvx/widgets/widgetLabel.c | 19 ++ dvx/widgets/widgetLayout.c | 79 +---- dvx/widgets/widgetOps.c | 200 ++---------- dvx/widgets/widgetRadio.c | 101 +++++- dvx/widgets/widgetSeparator.c | 17 + dvx/widgets/widgetSlider.c | 52 ++- dvx/widgets/widgetTabControl.c | 54 ++++ dvx/widgets/widgetTextInput.c | 157 ++++++++- dvx/widgets/widgetTreeView.c | 19 ++ 24 files changed, 1325 insertions(+), 886 deletions(-) create mode 100644 dvx/widgets/widgetClass.c diff --git a/dvx/Makefile b/dvx/Makefile index d93e2ed..1b1bd2b 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -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) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 03f11b8..0c381b5 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -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; diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 724f7b2..5a219f2 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -6,8 +6,9 @@ #include -// Forward declaration +// Forward declarations struct AppContextT; +struct WidgetClassT; // ============================================================ // Size specifications @@ -100,8 +101,9 @@ typedef enum { #define MAX_WIDGET_NAME 32 typedef struct WidgetT { - WidgetTypeE type; - char name[MAX_WIDGET_NAME]; + WidgetTypeE type; + const struct WidgetClassT *wclass; + char name[MAX_WIDGET_NAME]; // Tree linkage struct WidgetT *parent; diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index 18e2456..ed1ad23 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -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; diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index b00d798..6c0108d 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -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; } diff --git a/dvx/widgets/widgetCanvas.c b/dvx/widgets/widgetCanvas.c index 30b6e9b..6ce4d2e 100644 --- a/dvx/widgets/widgetCanvas.c +++ b/dvx/widgets/widgetCanvas.c @@ -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; diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index d0808fc..ef2c1c6 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -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); } } diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c new file mode 100644 index 0000000..12a2219 --- /dev/null +++ b/dvx/widgets/widgetClass.c @@ -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 +}; diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index d6d6a49..0586013 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -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; } } diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 5a3eee7..b621776 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -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; } diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index 5289cf3..61cc8a1 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -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; } diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index a1b08c7..5fb0c21 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -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); diff --git a/dvx/widgets/widgetImage.c b/dvx/widgets/widgetImage.c index 54a88e3..9c1fd10 100644 --- a/dvx/widgets/widgetImage.c +++ b/dvx/widgets/widgetImage.c @@ -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; } diff --git a/dvx/widgets/widgetImageButton.c b/dvx/widgets/widgetImageButton.c index cb2c552..2b8ffd2 100644 --- a/dvx/widgets/widgetImageButton.c +++ b/dvx/widgets/widgetImageButton.c @@ -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; } diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 3d7cd2a..703aee9 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -12,6 +12,31 @@ #include #include +// ============================================================ +// 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 diff --git a/dvx/widgets/widgetLabel.c b/dvx/widgets/widgetLabel.c index e68a19c..81cdc20 100644 --- a/dvx/widgets/widgetLabel.c +++ b/dvx/widgets/widgetLabel.c @@ -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 // ============================================================ diff --git a/dvx/widgets/widgetLayout.c b/dvx/widgets/widgetLayout.c index e1f8336..1a89f2c 100644 --- a/dvx/widgets/widgetLayout.c +++ b/dvx/widgets/widgetLayout.c @@ -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 if (w->wclass && w->wclass->calcMinSize) { + w->wclass->calcMinSize(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 { - 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; - } + w->calcMinW = 0; + w->calcMinH = 0; } // 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); } } diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index 5eff737..cc4b0c9 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -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; - - 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); - 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; + // Paint this widget via vtable + if (w->wclass && w->wclass->paint) { + w->wclass->paint(w, d, ops, font, colors); } - // Paint children (TabControl and TreeView return early above) + // Widgets that paint their own children return early + if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) { + if (sDebugLayout) { + debugContainerBorder(w, d, ops); + } + + return; + } + + // 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]; - } - return ""; - default: return ""; + if (w->wclass && w->wclass->getText) { + return w->wclass->getText(w); } + + 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); } } diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c index 3a9329c..ae3333f 100644 --- a/dvx/widgets/widgetRadio.c +++ b/dvx/widgets/widgetRadio.c @@ -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); } } } diff --git a/dvx/widgets/widgetSeparator.c b/dvx/widgets/widgetSeparator.c index b53eb27..5e18563 100644 --- a/dvx/widgets/widgetSeparator.c +++ b/dvx/widgets/widgetSeparator.c @@ -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 // ============================================================ diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c index ee67bd8..4221bb3 100644 --- a/dvx/widgets/widgetSlider.c +++ b/dvx/widgets/widgetSlider.c @@ -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) { diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index b7823ca..3e38cb2 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -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; diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index 5748b38..e23df2b 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -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); } diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index e3e4931..59584fa 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -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;