(mostly) Decoupled widget code from GUI code.
This commit is contained in:
parent
9fc21c2f83
commit
291eaf0c35
24 changed files with 1325 additions and 886 deletions
|
|
@ -14,6 +14,7 @@ LIBDIR = ../lib
|
||||||
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c
|
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c
|
||||||
|
|
||||||
WSRCS = widgets/widgetAnsiTerm.c \
|
WSRCS = widgets/widgetAnsiTerm.c \
|
||||||
|
widgets/widgetClass.c \
|
||||||
widgets/widgetCore.c \
|
widgets/widgetCore.c \
|
||||||
widgets/widgetLayout.c \
|
widgets/widgetLayout.c \
|
||||||
widgets/widgetEvent.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 file dependencies
|
||||||
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
|
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)/widgetAnsiTerm.o: widgets/widgetAnsiTerm.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetCore.o: widgets/widgetCore.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetLayout.o: widgets/widgetLayout.c $(WIDGET_DEPS)
|
||||||
|
|
|
||||||
|
|
@ -329,15 +329,13 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
|
||||||
|
|
||||||
case WidgetCheckboxE:
|
case WidgetCheckboxE:
|
||||||
widgetClearFocus(win->widgetRoot);
|
widgetClearFocus(win->widgetRoot);
|
||||||
target->focused = true;
|
widgetCheckboxOnMouse(target, win->widgetRoot, 0, 0);
|
||||||
widgetCheckboxOnMouse(target);
|
|
||||||
wgtInvalidate(target);
|
wgtInvalidate(target);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case WidgetRadioE:
|
case WidgetRadioE:
|
||||||
widgetClearFocus(win->widgetRoot);
|
widgetClearFocus(win->widgetRoot);
|
||||||
target->focused = true;
|
widgetRadioOnMouse(target, win->widgetRoot, 0, 0);
|
||||||
widgetRadioOnMouse(target);
|
|
||||||
wgtInvalidate(target);
|
wgtInvalidate(target);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declarations
|
||||||
struct AppContextT;
|
struct AppContextT;
|
||||||
|
struct WidgetClassT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Size specifications
|
// Size specifications
|
||||||
|
|
@ -100,8 +101,9 @@ typedef enum {
|
||||||
#define MAX_WIDGET_NAME 32
|
#define MAX_WIDGET_NAME 32
|
||||||
|
|
||||||
typedef struct WidgetT {
|
typedef struct WidgetT {
|
||||||
WidgetTypeE type;
|
WidgetTypeE type;
|
||||||
char name[MAX_WIDGET_NAME];
|
const struct WidgetClassT *wclass;
|
||||||
|
char name[MAX_WIDGET_NAME];
|
||||||
|
|
||||||
// Tree linkage
|
// Tree linkage
|
||||||
struct WidgetT *parent;
|
struct WidgetT *parent;
|
||||||
|
|
|
||||||
|
|
@ -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
|
// widgetAnsiTermCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1389,6 +1399,8 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key) {
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, buf, len);
|
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.
|
// 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;
|
hit->focused = true;
|
||||||
|
|
||||||
int32_t sbCount = hit->as.ansiTerm.scrollbackCount;
|
int32_t sbCount = hit->as.ansiTerm.scrollbackCount;
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// widgetButtonOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetButtonOnMouse(WidgetT *hit) {
|
void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->as.button.pressed = true;
|
(void)root;
|
||||||
sPressedButton = hit;
|
(void)vx;
|
||||||
|
(void)vy;
|
||||||
|
w->focused = true;
|
||||||
|
w->as.button.pressed = true;
|
||||||
|
sPressedButton = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// widgetCanvasCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -609,7 +618,8 @@ void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetCanvasOnMouse
|
// 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
|
// Convert widget coords to canvas coords
|
||||||
int32_t cx = vx - hit->x - CANVAS_BORDER;
|
int32_t cx = vx - hit->x - CANVAS_BORDER;
|
||||||
int32_t cy = vy - hit->y - CANVAS_BORDER;
|
int32_t cy = vy - hit->y - CANVAS_BORDER;
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// widgetCheckboxOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetCheckboxOnMouse(WidgetT *hit) {
|
void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->as.checkbox.checked = !hit->as.checkbox.checked;
|
(void)root;
|
||||||
|
(void)vx;
|
||||||
|
(void)vy;
|
||||||
|
w->focused = true;
|
||||||
|
w->as.checkbox.checked = !w->as.checkbox.checked;
|
||||||
|
|
||||||
if (hit->onChange) {
|
if (w->onChange) {
|
||||||
hit->onChange(hit);
|
w->onChange(w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
392
dvx/widgets/widgetClass.c
Normal file
392
dvx/widgets/widgetClass.c
Normal 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
|
||||||
|
};
|
||||||
|
|
@ -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
|
// 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
|
// widgetComboBoxOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx) {
|
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// Check if click is on the button area
|
(void)vy;
|
||||||
int32_t textAreaW = hit->w - DROPDOWN_BTN_WIDTH;
|
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
|
// Button click — toggle popup
|
||||||
hit->as.comboBox.open = !hit->as.comboBox.open;
|
w->as.comboBox.open = !w->as.comboBox.open;
|
||||||
hit->as.comboBox.hoverIdx = hit->as.comboBox.selectedIdx;
|
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
|
||||||
sOpenPopup = hit->as.comboBox.open ? hit : NULL;
|
sOpenPopup = w->as.comboBox.open ? w : NULL;
|
||||||
} else {
|
} else {
|
||||||
// Text area click — focus for editing
|
// Text area click — focus for editing
|
||||||
hit->focused = true;
|
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
int32_t relX = vx - hit->x - TEXT_INPUT_PAD;
|
int32_t relX = vx - w->x - TEXT_INPUT_PAD;
|
||||||
int32_t charPos = relX / font->charWidth + hit->as.comboBox.scrollOff;
|
int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff;
|
||||||
|
|
||||||
if (charPos < 0) {
|
if (charPos < 0) {
|
||||||
charPos = 0;
|
charPos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charPos > hit->as.comboBox.len) {
|
if (charPos > w->as.comboBox.len) {
|
||||||
charPos = hit->as.comboBox.len;
|
charPos = w->as.comboBox.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
hit->as.comboBox.cursorPos = charPos;
|
w->as.comboBox.cursorPos = charPos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
|
||||||
|
|
||||||
memset(w, 0, sizeof(*w));
|
memset(w, 0, sizeof(*w));
|
||||||
w->type = type;
|
w->type = type;
|
||||||
|
w->wclass = widgetClassTable[type];
|
||||||
w->visible = true;
|
w->visible = true;
|
||||||
w->enabled = true;
|
w->enabled = true;
|
||||||
|
|
||||||
|
|
@ -102,21 +103,8 @@ void widgetDestroyChildren(WidgetT *w) {
|
||||||
WidgetT *next = child->nextSibling;
|
WidgetT *next = child->nextSibling;
|
||||||
widgetDestroyChildren(child);
|
widgetDestroyChildren(child);
|
||||||
|
|
||||||
if (child->type == WidgetTextInputE) {
|
if (child->wclass && child->wclass->destroy) {
|
||||||
free(child->as.textInput.buf);
|
child->wclass->destroy(child);
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear popup/drag references if they point to destroyed widgets
|
// 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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TreeView manages its own children — don't recurse
|
// Widgets with WCLASS_NO_HIT_RECURSE manage their own children
|
||||||
if (w->type == WidgetTreeViewE) {
|
if (w->wclass && (w->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,12 +379,7 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
bool widgetIsFocusable(WidgetTypeE type) {
|
bool widgetIsFocusable(WidgetTypeE type) {
|
||||||
return type == WidgetTextInputE || type == WidgetComboBoxE ||
|
return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
|
||||||
type == WidgetDropdownE || type == WidgetCheckboxE ||
|
|
||||||
type == WidgetRadioE || type == WidgetButtonE ||
|
|
||||||
type == WidgetImageButtonE || type == WidgetSliderE ||
|
|
||||||
type == WidgetListBoxE || type == WidgetTreeViewE ||
|
|
||||||
type == WidgetAnsiTermE || type == WidgetTabControlE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -407,9 +390,7 @@ bool widgetIsFocusable(WidgetTypeE type) {
|
||||||
// Returns true for widget types that use the generic box layout.
|
// Returns true for widget types that use the generic box layout.
|
||||||
|
|
||||||
bool widgetIsBoxContainer(WidgetTypeE type) {
|
bool widgetIsBoxContainer(WidgetTypeE type) {
|
||||||
return type == WidgetVBoxE || type == WidgetHBoxE || type == WidgetFrameE ||
|
return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0;
|
||||||
type == WidgetRadioGroupE || type == WidgetTabPageE ||
|
|
||||||
type == WidgetStatusBarE || type == WidgetToolbarE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -420,7 +401,7 @@ bool widgetIsBoxContainer(WidgetTypeE type) {
|
||||||
// Returns true for container types that lay out children horizontally.
|
// Returns true for container types that lay out children horizontally.
|
||||||
|
|
||||||
bool widgetIsHorizContainer(WidgetTypeE type) {
|
bool widgetIsHorizContainer(WidgetTypeE type) {
|
||||||
return type == WidgetHBoxE || type == WidgetStatusBarE || type == WidgetToolbarE;
|
return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// widgetDropdownOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetDropdownOnMouse(WidgetT *hit) {
|
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->as.dropdown.open = !hit->as.dropdown.open;
|
(void)root;
|
||||||
hit->as.dropdown.hoverIdx = hit->as.dropdown.selectedIdx;
|
(void)vx;
|
||||||
sOpenPopup = hit->as.dropdown.open ? hit : NULL;
|
(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
// Widget whose popup was just closed by click-outside — prevents
|
||||||
|
// immediate re-open on the same click.
|
||||||
|
WidgetT *sClosedPopup = NULL;
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetManageScrollbars
|
// widgetManageScrollbars
|
||||||
|
|
@ -137,476 +141,10 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ANSI terminal key input
|
// Dispatch to per-widget onKey handler via vtable
|
||||||
if (focus->type == WidgetAnsiTermE) {
|
if (focus->wclass && focus->wclass->onKey) {
|
||||||
widgetAnsiTermOnKey(focus, key);
|
focus->wclass->onKey(focus, key);
|
||||||
wgtInvalidate(focus);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
||||||
WidgetT *root = win->widgetRoot;
|
WidgetT *root = win->widgetRoot;
|
||||||
WidgetT *closedPopup = NULL;
|
sClosedPopup = NULL;
|
||||||
|
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return;
|
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)
|
// Handle canvas drawing (mouse move while pressed)
|
||||||
if (sDrawingCanvas && (buttons & 1)) {
|
if (sDrawingCanvas && (buttons & 1)) {
|
||||||
widgetCanvasOnMouse(sDrawingCanvas, x, y);
|
widgetCanvasOnMouse(sDrawingCanvas, root, x, y);
|
||||||
wgtInvalidate(root);
|
wgtInvalidate(root);
|
||||||
return;
|
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
|
// Click outside popup — close it and remember which widget it was
|
||||||
closedPopup = sOpenPopup;
|
sClosedPopup = sOpenPopup;
|
||||||
|
|
||||||
if (sOpenPopup->type == WidgetDropdownE) {
|
if (sOpenPopup->type == WidgetDropdownE) {
|
||||||
sOpenPopup->as.dropdown.open = false;
|
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
|
// Dispatch to per-widget mouse handler via vtable
|
||||||
if (hit->type == WidgetTextInputE) {
|
if (hit->enabled && hit->wclass && hit->wclass->onMouse) {
|
||||||
widgetTextInputOnMouse(hit, root, vx);
|
hit->wclass->onMouse(hit, root, vx, vy);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wgtInvalidate(root);
|
wgtInvalidate(root);
|
||||||
|
|
|
||||||
|
|
@ -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
|
// widgetImageCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -127,15 +136,18 @@ void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetImageOnMouse
|
// widgetImageOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetImageOnMouse(WidgetT *hit) {
|
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->as.image.pressed = true;
|
(void)root;
|
||||||
wgtInvalidate(hit);
|
(void)vx;
|
||||||
|
(void)vy;
|
||||||
|
w->as.image.pressed = true;
|
||||||
|
wgtInvalidate(w);
|
||||||
|
|
||||||
if (hit->onClick) {
|
if (w->onClick) {
|
||||||
hit->onClick(hit);
|
w->onClick(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
hit->as.image.pressed = false;
|
w->as.image.pressed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// widgetImageButtonOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetImageButtonOnMouse(WidgetT *hit) {
|
void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->as.imageButton.pressed = true;
|
(void)root;
|
||||||
sPressedButton = hit;
|
(void)vx;
|
||||||
|
(void)vy;
|
||||||
|
w->focused = true;
|
||||||
|
w->as.imageButton.pressed = true;
|
||||||
|
sPressedButton = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,31 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.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
|
// Constants
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -56,6 +81,7 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
extern bool sDebugLayout;
|
extern bool sDebugLayout;
|
||||||
|
extern WidgetT *sClosedPopup;
|
||||||
extern WidgetT *sKeyPressedBtn;
|
extern WidgetT *sKeyPressedBtn;
|
||||||
extern WidgetT *sOpenPopup;
|
extern WidgetT *sOpenPopup;
|
||||||
extern WidgetT *sPressedButton;
|
extern WidgetT *sPressedButton;
|
||||||
|
|
@ -154,6 +180,7 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetRadioCalcMinSize(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 widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetTabControlCalcMinSize(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);
|
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 widgetAnsiTermOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetButtonOnMouse(WidgetT *hit);
|
void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetImageButtonOnMouse(WidgetT *hit);
|
void widgetButtonOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetCanvasOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
|
void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetCheckboxOnMouse(WidgetT *hit);
|
void widgetCanvasOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
|
void widgetCheckboxOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetDropdownOnMouse(WidgetT *hit);
|
void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetImageOnMouse(WidgetT *hit);
|
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 widgetListBoxOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetListBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetRadioOnMouse(WidgetT *hit);
|
void widgetRadioOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
|
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetSliderOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
|
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 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
|
#endif // WIDGET_INTERNAL_H
|
||||||
|
|
|
||||||
|
|
@ -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
|
// widgetLabelCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -90,76 +90,11 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
||||||
if (widgetIsBoxContainer(w->type)) {
|
if (widgetIsBoxContainer(w->type)) {
|
||||||
widgetCalcMinSizeBox(w, font);
|
widgetCalcMinSizeBox(w, font);
|
||||||
} else if (w->type == WidgetTabControlE) {
|
} else if (w->wclass && w->wclass->calcMinSize) {
|
||||||
widgetTabControlCalcMinSize(w, font);
|
w->wclass->calcMinSize(w, font);
|
||||||
} else if (w->type == WidgetTreeViewE) {
|
|
||||||
widgetTreeViewCalcMinSize(w, font);
|
|
||||||
} else {
|
} else {
|
||||||
// Leaf widgets
|
w->calcMinW = 0;
|
||||||
switch (w->type) {
|
w->calcMinH = 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply size hints (override calculated minimum)
|
// Apply size hints (override calculated minimum)
|
||||||
|
|
@ -339,10 +274,8 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
|
||||||
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
|
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
|
||||||
if (widgetIsBoxContainer(w->type)) {
|
if (widgetIsBoxContainer(w->type)) {
|
||||||
widgetLayoutBox(w, font);
|
widgetLayoutBox(w, font);
|
||||||
} else if (w->type == WidgetTabControlE) {
|
} else if (w->wclass && w->wclass->layout) {
|
||||||
widgetTabControlLayout(w, font);
|
w->wclass->layout(w, font);
|
||||||
} else if (w->type == WidgetTreeViewE) {
|
|
||||||
widgetTreeViewLayout(w, font);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,107 +49,21 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (w->type) {
|
// Paint this widget via vtable
|
||||||
case WidgetVBoxE:
|
if (w->wclass && w->wclass->paint) {
|
||||||
case WidgetHBoxE:
|
w->wclass->paint(w, d, ops, font, colors);
|
||||||
// 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 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) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
widgetPaintOne(c, d, ops, font, colors);
|
widgetPaintOne(c, d, ops, font, colors);
|
||||||
}
|
}
|
||||||
|
|
@ -184,10 +98,8 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sOpenPopup->type == WidgetDropdownE) {
|
if (sOpenPopup->wclass && sOpenPopup->wclass->paintOverlay) {
|
||||||
widgetDropdownPaintPopup(sOpenPopup, d, ops, font, colors);
|
sOpenPopup->wclass->paintOverlay(sOpenPopup, d, ops, font, colors);
|
||||||
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
|
||||||
widgetComboBoxPaintPopup(sOpenPopup, d, ops, font, colors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,19 +119,8 @@ void wgtDestroy(WidgetT *w) {
|
||||||
|
|
||||||
widgetDestroyChildren(w);
|
widgetDestroyChildren(w);
|
||||||
|
|
||||||
if (w->type == WidgetTextInputE) {
|
if (w->wclass && w->wclass->destroy) {
|
||||||
free(w->as.textInput.buf);
|
w->wclass->destroy(w);
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear static references
|
// Clear static references
|
||||||
|
|
@ -278,21 +179,11 @@ const char *wgtGetText(const WidgetT *w) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (w->type) {
|
if (w->wclass && w->wclass->getText) {
|
||||||
case WidgetLabelE: return w->as.label.text;
|
return w->wclass->getText(w);
|
||||||
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 "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -397,53 +288,8 @@ void wgtSetText(WidgetT *w, const char *text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (w->type) {
|
if (w->wclass && w->wclass->setText) {
|
||||||
case WidgetLabelE:
|
w->wclass->setText(w, text);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// widgetRadioOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetRadioOnMouse(WidgetT *hit) {
|
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
if (hit->parent && hit->parent->type == WidgetRadioGroupE) {
|
(void)root;
|
||||||
hit->parent->as.radioGroup.selectedIdx = hit->as.radio.index;
|
(void)vx;
|
||||||
|
(void)vy;
|
||||||
|
w->focused = true;
|
||||||
|
|
||||||
if (hit->parent->onChange) {
|
if (w->parent && w->parent->type == WidgetRadioGroupE) {
|
||||||
hit->parent->onChange(hit->parent);
|
w->parent->as.radioGroup.selectedIdx = w->as.radio.index;
|
||||||
|
|
||||||
|
if (w->parent->onChange) {
|
||||||
|
w->parent->onChange(w->parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
// widgetSeparatorPaint
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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;
|
int32_t range = hit->as.slider.maxValue - hit->as.slider.minValue;
|
||||||
|
|
||||||
if (range <= 0) {
|
if (range <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
// widgetTabControlOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
|
hit->focused = true;
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetTextAreaDestroy
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetTextAreaDestroy(WidgetT *w) {
|
||||||
|
free(w->as.textArea.buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtTextInput
|
// 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
|
// widgetTextInputCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -61,24 +103,125 @@ void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// widgetTextInputOnMouse
|
// widgetTextInputOnMouse
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx) {
|
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
hit->focused = true;
|
(void)vy;
|
||||||
|
w->focused = true;
|
||||||
|
|
||||||
// Place cursor at click position
|
// Place cursor at click position
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
int32_t relX = vx - hit->x - TEXT_INPUT_PAD;
|
int32_t relX = vx - w->x - TEXT_INPUT_PAD;
|
||||||
int32_t charPos = relX / font->charWidth + hit->as.textInput.scrollOff;
|
int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff;
|
||||||
|
|
||||||
if (charPos < 0) {
|
if (charPos < 0) {
|
||||||
charPos = 0;
|
charPos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charPos > hit->as.textInput.len) {
|
if (charPos > w->as.textInput.len) {
|
||||||
charPos = hit->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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// wgtTreeItemIsExpanded
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -791,6 +809,7 @@ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
|
hit->focused = true;
|
||||||
AppContextT *ctx = (AppContextT *)root->userData;
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
const BitmapFontT *font = &ctx->font;
|
const BitmapFontT *font = &ctx->font;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue