General widget cleanup. Working on keyboard control of GUI.
This commit is contained in:
parent
14eca6fcd2
commit
458329408f
9 changed files with 533 additions and 25 deletions
34
dvx/dvxApp.c
34
dvx/dvxApp.c
|
|
@ -344,6 +344,17 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
|
||||||
|
|
||||||
case WidgetTabPageE:
|
case WidgetTabPageE:
|
||||||
{
|
{
|
||||||
|
// Close any open dropdown/combobox popup
|
||||||
|
if (sOpenPopup) {
|
||||||
|
if (sOpenPopup->type == WidgetDropdownE) {
|
||||||
|
sOpenPopup->as.dropdown.open = false;
|
||||||
|
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
||||||
|
sOpenPopup->as.comboBox.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sOpenPopup = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
// Activate this tab in its parent TabControl
|
// Activate this tab in its parent TabControl
|
||||||
if (target->parent && target->parent->type == WidgetTabControlE) {
|
if (target->parent && target->parent->type == WidgetTabControlE) {
|
||||||
int32_t tabIdx = 0;
|
int32_t tabIdx = 0;
|
||||||
|
|
@ -1741,14 +1752,25 @@ static void pollKeyboard(AppContextT *ctx) {
|
||||||
|
|
||||||
// ESC closes open dropdown/combobox popup
|
// ESC closes open dropdown/combobox popup
|
||||||
if (sOpenPopup && ascii == 0x1B) {
|
if (sOpenPopup && ascii == 0x1B) {
|
||||||
if (sOpenPopup->type == WidgetDropdownE) {
|
// Dirty the popup list area
|
||||||
sOpenPopup->as.dropdown.open = false;
|
WindowT *popWin = sOpenPopup->window;
|
||||||
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
int32_t popX;
|
||||||
sOpenPopup->as.comboBox.open = false;
|
int32_t popY;
|
||||||
|
int32_t popW;
|
||||||
|
int32_t popH;
|
||||||
|
widgetDropdownPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH);
|
||||||
|
dirtyListAdd(&ctx->dirty, popWin->x + popWin->contentX + popX, popWin->y + popWin->contentY + popY, popW, popH);
|
||||||
|
|
||||||
|
WidgetT *closing = sOpenPopup;
|
||||||
|
sOpenPopup = NULL;
|
||||||
|
|
||||||
|
if (closing->type == WidgetDropdownE) {
|
||||||
|
closing->as.dropdown.open = false;
|
||||||
|
} else if (closing->type == WidgetComboBoxE) {
|
||||||
|
closing->as.comboBox.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
wgtInvalidate(sOpenPopup);
|
wgtInvalidate(closing);
|
||||||
sOpenPopup = NULL;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ typedef struct WidgetT {
|
||||||
struct {
|
struct {
|
||||||
int32_t scrollPos;
|
int32_t scrollPos;
|
||||||
int32_t scrollPosH;
|
int32_t scrollPosH;
|
||||||
|
struct WidgetT *selectedItem;
|
||||||
} treeView;
|
} treeView;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|
@ -430,6 +431,8 @@ WidgetT *wgtToolbar(WidgetT *parent);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
WidgetT *wgtTreeView(WidgetT *parent);
|
WidgetT *wgtTreeView(WidgetT *parent);
|
||||||
|
WidgetT *wgtTreeViewGetSelected(const WidgetT *w);
|
||||||
|
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item);
|
||||||
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
||||||
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
||||||
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,6 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons
|
||||||
// Draw items
|
// Draw items
|
||||||
int32_t itemCount = w->as.comboBox.itemCount;
|
int32_t itemCount = w->as.comboBox.itemCount;
|
||||||
const char **items = w->as.comboBox.items;
|
const char **items = w->as.comboBox.items;
|
||||||
int32_t selIdx = w->as.comboBox.selectedIdx;
|
|
||||||
int32_t hoverIdx = w->as.comboBox.hoverIdx;
|
int32_t hoverIdx = w->as.comboBox.hoverIdx;
|
||||||
int32_t scrollPos = w->as.comboBox.listScrollPos;
|
int32_t scrollPos = w->as.comboBox.listScrollPos;
|
||||||
|
|
||||||
|
|
@ -235,12 +234,12 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons
|
||||||
uint32_t ifg = colors->contentFg;
|
uint32_t ifg = colors->contentFg;
|
||||||
uint32_t ibg = colors->contentBg;
|
uint32_t ibg = colors->contentBg;
|
||||||
|
|
||||||
if (idx == hoverIdx || idx == selIdx) {
|
if (idx == hoverIdx) {
|
||||||
ifg = colors->menuHighlightFg;
|
ifg = colors->menuHighlightFg;
|
||||||
ibg = colors->menuHighlightBg;
|
ibg = colors->menuHighlightBg;
|
||||||
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx || idx == selIdx);
|
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons
|
||||||
// Draw items
|
// Draw items
|
||||||
int32_t itemCount = w->as.dropdown.itemCount;
|
int32_t itemCount = w->as.dropdown.itemCount;
|
||||||
const char **items = w->as.dropdown.items;
|
const char **items = w->as.dropdown.items;
|
||||||
int32_t selIdx = w->as.dropdown.selectedIdx;
|
|
||||||
int32_t hoverIdx = w->as.dropdown.hoverIdx;
|
int32_t hoverIdx = w->as.dropdown.hoverIdx;
|
||||||
int32_t scrollPos = w->as.dropdown.scrollPos;
|
int32_t scrollPos = w->as.dropdown.scrollPos;
|
||||||
|
|
||||||
|
|
@ -175,12 +174,12 @@ void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons
|
||||||
uint32_t ifg = colors->contentFg;
|
uint32_t ifg = colors->contentFg;
|
||||||
uint32_t ibg = colors->contentBg;
|
uint32_t ibg = colors->contentBg;
|
||||||
|
|
||||||
if (idx == hoverIdx || idx == selIdx) {
|
if (idx == hoverIdx) {
|
||||||
ifg = colors->menuHighlightFg;
|
ifg = colors->menuHighlightFg;
|
||||||
ibg = colors->menuHighlightBg;
|
ibg = colors->menuHighlightBg;
|
||||||
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx || idx == selIdx);
|
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,13 +121,13 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
||||||
while (top > 0) {
|
while (top > 0) {
|
||||||
WidgetT *w = stack[--top];
|
WidgetT *w = stack[--top];
|
||||||
|
|
||||||
if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetAnsiTermE)) {
|
if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetDropdownE || w->type == WidgetAnsiTermE || w->type == WidgetTreeViewE)) {
|
||||||
focus = w;
|
focus = w;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
if (top < 64) {
|
if (c->visible && top < 64) {
|
||||||
stack[top++] = c;
|
stack[top++] = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +144,151 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle tree view keyboard navigation
|
||||||
|
if (focus->type == WidgetTreeViewE) {
|
||||||
|
widgetTreeViewOnKey(focus, key);
|
||||||
|
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
|
// Handle text input for TextInput and ComboBox
|
||||||
char *buf = NULL;
|
char *buf = NULL;
|
||||||
int32_t bufSize = 0;
|
int32_t bufSize = 0;
|
||||||
|
|
@ -548,6 +693,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit->type == WidgetTreeViewE && hit->enabled) {
|
if (hit->type == WidgetTreeViewE && hit->enabled) {
|
||||||
|
hit->focused = true;
|
||||||
widgetTreeViewOnMouse(hit, root, vx, vy);
|
widgetTreeViewOnMouse(hit, root, vx, vy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ void widgetRadioOnMouse(WidgetT *hit);
|
||||||
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
|
void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy);
|
||||||
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
|
void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx);
|
||||||
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key);
|
||||||
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
|
||||||
#endif // WIDGET_INTERNAL_H
|
#endif // WIDGET_INTERNAL_H
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,17 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
|
|
||||||
if (vx >= tabX && vx < tabX + tw) {
|
if (vx >= tabX && vx < tabX + tw) {
|
||||||
if (tabIdx != hit->as.tabControl.activeTab) {
|
if (tabIdx != hit->as.tabControl.activeTab) {
|
||||||
|
// Close any open dropdown/combobox popup
|
||||||
|
if (sOpenPopup) {
|
||||||
|
if (sOpenPopup->type == WidgetDropdownE) {
|
||||||
|
sOpenPopup->as.dropdown.open = false;
|
||||||
|
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
||||||
|
sOpenPopup->as.comboBox.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sOpenPopup = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
hit->as.tabControl.activeTab = tabIdx;
|
hit->as.tabControl.activeTab = tabIdx;
|
||||||
|
|
||||||
if (hit->onChange) {
|
if (hit->onChange) {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,15 @@ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font);
|
||||||
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth);
|
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth);
|
||||||
static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb);
|
static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb);
|
||||||
static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH);
|
static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH);
|
||||||
|
static WidgetT *firstVisibleItem(WidgetT *treeView);
|
||||||
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth);
|
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth);
|
||||||
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom);
|
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView);
|
||||||
|
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *selectedItem);
|
||||||
|
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView);
|
||||||
static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb);
|
static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb);
|
||||||
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font);
|
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font);
|
||||||
|
static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font);
|
||||||
|
static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *curY, const BitmapFontT *font);
|
||||||
static void treeScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t innerSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
|
static void treeScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t innerSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -214,6 +219,23 @@ static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, con
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// firstVisibleItem
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Return the first visible tree item (depth-first).
|
||||||
|
|
||||||
|
static WidgetT *firstVisibleItem(WidgetT *treeView) {
|
||||||
|
for (WidgetT *c = treeView->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTreeItemE && c->visible) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// layoutTreeItems
|
// layoutTreeItems
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -237,11 +259,48 @@ static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// nextVisibleItem
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Return the next visible tree item after the given item
|
||||||
|
// (depth-first order: children first, then siblings, then uncle).
|
||||||
|
|
||||||
|
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
|
// If expanded with children, descend
|
||||||
|
if (item->as.treeItem.expanded) {
|
||||||
|
for (WidgetT *c = item->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTreeItemE && c->visible) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try next sibling
|
||||||
|
for (WidgetT *s = item->nextSibling; s; s = s->nextSibling) {
|
||||||
|
if (s->type == WidgetTreeItemE && s->visible) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk up parents, looking for their next sibling
|
||||||
|
for (WidgetT *p = item->parent; p && p != treeView; p = p->parent) {
|
||||||
|
for (WidgetT *s = p->nextSibling; s; s = s->nextSibling) {
|
||||||
|
if (s->type == WidgetTreeItemE && s->visible) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// paintTreeItems
|
// paintTreeItems
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom) {
|
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *selectedItem) {
|
||||||
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
||||||
if (c->type != WidgetTreeItemE || !c->visible) {
|
if (c->type != WidgetTreeItemE || !c->visible) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -254,12 +313,26 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
|
||||||
// Skip items outside visible area
|
// Skip items outside visible area
|
||||||
if (iy + font->charHeight <= clipTop || iy >= clipBottom) {
|
if (iy + font->charHeight <= clipTop || iy >= clipBottom) {
|
||||||
if (c->as.treeItem.expanded && c->firstChild) {
|
if (c->as.treeItem.expanded && c->firstChild) {
|
||||||
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom);
|
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, selectedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlight selected item
|
||||||
|
bool isSelected = (c == selectedItem);
|
||||||
|
uint32_t fg;
|
||||||
|
uint32_t bg;
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
fg = colors->menuHighlightFg;
|
||||||
|
bg = colors->menuHighlightBg;
|
||||||
|
rectFill(d, ops, baseX, iy, d->clipW, font->charHeight, bg);
|
||||||
|
} else {
|
||||||
|
fg = c->fgColor ? c->fgColor : colors->contentFg;
|
||||||
|
bg = c->bgColor ? c->bgColor : colors->contentBg;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw expand/collapse icon if item has children
|
// Draw expand/collapse icon if item has children
|
||||||
bool hasChildren = false;
|
bool hasChildren = false;
|
||||||
|
|
||||||
|
|
@ -297,18 +370,66 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw text
|
// Draw text
|
||||||
uint32_t fg = c->fgColor ? c->fgColor : colors->contentFg;
|
drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected);
|
||||||
uint32_t bg = c->bgColor ? c->bgColor : colors->contentBg;
|
|
||||||
drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, false);
|
|
||||||
|
|
||||||
// Recurse into expanded children
|
// Recurse into expanded children
|
||||||
if (c->as.treeItem.expanded && c->firstChild) {
|
if (c->as.treeItem.expanded && c->firstChild) {
|
||||||
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom);
|
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, selectedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// prevVisibleItem
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Return the previous visible tree item before the given item
|
||||||
|
// (depth-first order: last visible descendant of previous sibling,
|
||||||
|
// or parent).
|
||||||
|
|
||||||
|
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
|
// Find previous sibling
|
||||||
|
WidgetT *prevSib = NULL;
|
||||||
|
|
||||||
|
for (WidgetT *s = item->parent->firstChild; s && s != item; s = s->nextSibling) {
|
||||||
|
if (s->type == WidgetTreeItemE && s->visible) {
|
||||||
|
prevSib = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevSib) {
|
||||||
|
// Descend to last visible descendant of previous sibling
|
||||||
|
WidgetT *node = prevSib;
|
||||||
|
|
||||||
|
while (node->as.treeItem.expanded) {
|
||||||
|
WidgetT *last = NULL;
|
||||||
|
|
||||||
|
for (WidgetT *c = node->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTreeItemE && c->visible) {
|
||||||
|
last = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = last;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No previous sibling — go to parent (if it's a tree item)
|
||||||
|
if (item->parent && item->parent != treeView && item->parent->type == WidgetTreeItemE) {
|
||||||
|
return item->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// treeCalcScrollbarNeeds
|
// treeCalcScrollbarNeeds
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -383,6 +504,51 @@ static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, con
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// treeItemYPos
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Return the Y offset (from tree top, 0-based) of a given item,
|
||||||
|
// or -1 if not found/visible.
|
||||||
|
|
||||||
|
static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font) {
|
||||||
|
int32_t curY = 0;
|
||||||
|
|
||||||
|
if (treeItemYPosHelper(treeView, target, &curY, font)) {
|
||||||
|
return curY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// treeItemYPosHelper
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static int32_t treeItemYPosHelper(WidgetT *parent, WidgetT *target, int32_t *curY, const BitmapFontT *font) {
|
||||||
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type != WidgetTreeItemE || !c->visible) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == target) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*curY += font->charHeight;
|
||||||
|
|
||||||
|
if (c->as.treeItem.expanded && c->firstChild) {
|
||||||
|
if (treeItemYPosHelper(c, target, curY, font)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// treeScrollbarThumb
|
// treeScrollbarThumb
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -467,6 +633,32 @@ WidgetT *wgtTreeView(WidgetT *parent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtTreeViewGetSelected
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
WidgetT *wgtTreeViewGetSelected(const WidgetT *w) {
|
||||||
|
if (!w || w->type != WidgetTreeViewE) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return w->as.treeView.selectedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtTreeViewSetSelected
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
|
||||||
|
if (!w || w->type != WidgetTreeViewE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.treeView.selectedItem = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetTreeViewCalcMinSize
|
// widgetTreeViewCalcMinSize
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -479,6 +671,138 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetTreeViewOnKey
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key) {
|
||||||
|
if (!w || w->type != WidgetTreeViewE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetT *sel = w->as.treeView.selectedItem;
|
||||||
|
|
||||||
|
if (key == (0x50 | 0x100)) {
|
||||||
|
// Down arrow — next visible item
|
||||||
|
if (!sel) {
|
||||||
|
w->as.treeView.selectedItem = firstVisibleItem(w);
|
||||||
|
} else {
|
||||||
|
WidgetT *next = nextVisibleItem(sel, w);
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
w->as.treeView.selectedItem = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key == (0x48 | 0x100)) {
|
||||||
|
// Up arrow — previous visible item
|
||||||
|
if (!sel) {
|
||||||
|
w->as.treeView.selectedItem = firstVisibleItem(w);
|
||||||
|
} else {
|
||||||
|
WidgetT *prev = prevVisibleItem(sel, w);
|
||||||
|
|
||||||
|
if (prev) {
|
||||||
|
w->as.treeView.selectedItem = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key == (0x4D | 0x100)) {
|
||||||
|
// Right arrow — expand if collapsed, else move to first child
|
||||||
|
if (sel) {
|
||||||
|
bool hasChildren = false;
|
||||||
|
|
||||||
|
for (WidgetT *c = sel->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTreeItemE) {
|
||||||
|
hasChildren = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
if (!sel->as.treeItem.expanded) {
|
||||||
|
sel->as.treeItem.expanded = true;
|
||||||
|
|
||||||
|
if (sel->onChange) {
|
||||||
|
sel->onChange(sel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
WidgetT *next = nextVisibleItem(sel, w);
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
w->as.treeView.selectedItem = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key == (0x4B | 0x100)) {
|
||||||
|
// Left arrow — collapse if expanded, else move to parent
|
||||||
|
if (sel) {
|
||||||
|
if (sel->as.treeItem.expanded) {
|
||||||
|
sel->as.treeItem.expanded = false;
|
||||||
|
|
||||||
|
if (sel->onChange) {
|
||||||
|
sel->onChange(sel);
|
||||||
|
}
|
||||||
|
} else if (sel->parent && sel->parent != w && sel->parent->type == WidgetTreeItemE) {
|
||||||
|
w->as.treeView.selectedItem = sel->parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key == 0x0D) {
|
||||||
|
// Enter — toggle expand/collapse for parents, onClick for leaves
|
||||||
|
if (sel) {
|
||||||
|
bool hasChildren = false;
|
||||||
|
|
||||||
|
for (WidgetT *c = sel->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTreeItemE) {
|
||||||
|
hasChildren = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
sel->as.treeItem.expanded = !sel->as.treeItem.expanded;
|
||||||
|
|
||||||
|
if (sel->onChange) {
|
||||||
|
sel->onChange(sel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sel->onClick) {
|
||||||
|
sel->onClick(sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to keep selected item visible
|
||||||
|
sel = w->as.treeView.selectedItem;
|
||||||
|
|
||||||
|
if (sel) {
|
||||||
|
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
|
||||||
|
const BitmapFontT *font = &ctx->font;
|
||||||
|
int32_t itemY = treeItemYPos(w, sel, font);
|
||||||
|
|
||||||
|
if (itemY >= 0) {
|
||||||
|
int32_t totalH;
|
||||||
|
int32_t totalW;
|
||||||
|
int32_t innerH;
|
||||||
|
int32_t innerW;
|
||||||
|
bool needVSb;
|
||||||
|
bool needHSb;
|
||||||
|
|
||||||
|
treeCalcScrollbarNeeds(w, font, &totalH, &totalW, &innerH, &innerW, &needVSb, &needHSb);
|
||||||
|
|
||||||
|
if (itemY < w->as.treeView.scrollPos) {
|
||||||
|
w->as.treeView.scrollPos = itemY;
|
||||||
|
} else if (itemY + font->charHeight > w->as.treeView.scrollPos + innerH) {
|
||||||
|
w->as.treeView.scrollPos = itemY + font->charHeight - innerH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtInvalidate(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// widgetTreeViewLayout
|
// widgetTreeViewLayout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -663,6 +987,9 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set selection
|
||||||
|
hit->as.treeView.selectedItem = item;
|
||||||
|
|
||||||
// Check if click is on expand/collapse icon
|
// Check if click is on expand/collapse icon
|
||||||
bool hasChildren = false;
|
bool hasChildren = false;
|
||||||
|
|
||||||
|
|
@ -794,7 +1121,8 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
|
|
||||||
paintTreeItems(w, d, ops, font, colors,
|
paintTreeItems(w, d, ops, font, colors,
|
||||||
baseX, &itemY, 0,
|
baseX, &itemY, 0,
|
||||||
w->y + TREE_BORDER, w->y + TREE_BORDER + innerH);
|
w->y + TREE_BORDER, w->y + TREE_BORDER + innerH,
|
||||||
|
w->as.treeView.selectedItem);
|
||||||
|
|
||||||
// Restore clip rect
|
// Restore clip rect
|
||||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||||
|
|
|
||||||
|
|
@ -505,7 +505,6 @@ static void setupTerminalWindow(AppContextT *ctx) {
|
||||||
"========================================\x1B[0m\r\n"
|
"========================================\x1B[0m\r\n"
|
||||||
"\r\n"
|
"\r\n"
|
||||||
"\x1B[1mBold text\x1B[0m, "
|
"\x1B[1mBold text\x1B[0m, "
|
||||||
"\x1B[4mUnderlined\x1B[0m, "
|
|
||||||
"\x1B[7mReverse\x1B[0m, "
|
"\x1B[7mReverse\x1B[0m, "
|
||||||
"\x1B[5mBlinking\x1B[0m\r\n"
|
"\x1B[5mBlinking\x1B[0m\r\n"
|
||||||
"\r\n"
|
"\r\n"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue