From 458329408fcbbe15cfc725e8a6fd087bfa020019 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 12 Mar 2026 19:46:00 -0500 Subject: [PATCH] General widget cleanup. Working on keyboard control of GUI. --- dvx/dvxApp.c | 34 +++- dvx/dvxWidget.h | 7 +- dvx/widgets/widgetComboBox.c | 5 +- dvx/widgets/widgetDropdown.c | 5 +- dvx/widgets/widgetEvent.c | 150 +++++++++++++- dvx/widgets/widgetInternal.h | 1 + dvx/widgets/widgetTabControl.c | 11 ++ dvx/widgets/widgetTreeView.c | 344 ++++++++++++++++++++++++++++++++- dvxdemo/demo.c | 1 - 9 files changed, 533 insertions(+), 25 deletions(-) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 3f3c34a..ce92592 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -344,6 +344,17 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { 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 if (target->parent && target->parent->type == WidgetTabControlE) { int32_t tabIdx = 0; @@ -1741,14 +1752,25 @@ static void pollKeyboard(AppContextT *ctx) { // ESC closes open dropdown/combobox popup if (sOpenPopup && ascii == 0x1B) { - if (sOpenPopup->type == WidgetDropdownE) { - sOpenPopup->as.dropdown.open = false; - } else if (sOpenPopup->type == WidgetComboBoxE) { - sOpenPopup->as.comboBox.open = false; + // Dirty the popup list area + WindowT *popWin = sOpenPopup->window; + int32_t popX; + 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); - sOpenPopup = NULL; + wgtInvalidate(closing); continue; } diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 51a7ab7..724f7b2 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -252,8 +252,9 @@ typedef struct WidgetT { } tabPage; struct { - int32_t scrollPos; - int32_t scrollPosH; + int32_t scrollPos; + int32_t scrollPosH; + struct WidgetT *selectedItem; } treeView; struct { @@ -430,6 +431,8 @@ WidgetT *wgtToolbar(WidgetT *parent); // ============================================================ WidgetT *wgtTreeView(WidgetT *parent); +WidgetT *wgtTreeViewGetSelected(const WidgetT *w); +void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item); WidgetT *wgtTreeItem(WidgetT *parent, const char *text); void wgtTreeItemSetExpanded(WidgetT *w, bool expanded); bool wgtTreeItemIsExpanded(const WidgetT *w); diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index b78ba61..d6d6a49 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -220,7 +220,6 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons // Draw items int32_t itemCount = w->as.comboBox.itemCount; const char **items = w->as.comboBox.items; - int32_t selIdx = w->as.comboBox.selectedIdx; int32_t hoverIdx = w->as.comboBox.hoverIdx; 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 ibg = colors->contentBg; - if (idx == hoverIdx || idx == selIdx) { + if (idx == hoverIdx) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; 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); } } diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index 84bc3a6..860067a 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -160,7 +160,6 @@ void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons // Draw items int32_t itemCount = w->as.dropdown.itemCount; const char **items = w->as.dropdown.items; - int32_t selIdx = w->as.dropdown.selectedIdx; int32_t hoverIdx = w->as.dropdown.hoverIdx; 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 ibg = colors->contentBg; - if (idx == hoverIdx || idx == selIdx) { + if (idx == hoverIdx) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; 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); } } diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 15610a1..f2b0c22 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -121,13 +121,13 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { while (top > 0) { 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; break; } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (top < 64) { + if (c->visible && top < 64) { stack[top++] = c; } } @@ -144,6 +144,151 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { 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 char *buf = NULL; 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) { + hit->focused = true; widgetTreeViewOnMouse(hit, root, vx, vy); } diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index c85d91d..966d09f 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -168,6 +168,7 @@ void widgetRadioOnMouse(WidgetT *hit); void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy); void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); void widgetTextInputOnMouse(WidgetT *hit, WidgetT *root, int32_t vx); +void widgetTreeViewOnKey(WidgetT *w, int32_t key); void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); #endif // WIDGET_INTERNAL_H diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index 6937b19..2fbb6de 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -150,6 +150,17 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy if (vx >= tabX && vx < tabX + tw) { 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; if (hit->onChange) { diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index ae1c853..42e1c68 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -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 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 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 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 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); @@ -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 // ============================================================ @@ -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 // ============================================================ -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) { if (c->type != WidgetTreeItemE || !c->visible) { continue; @@ -254,12 +313,26 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co // Skip items outside visible area if (iy + font->charHeight <= clipTop || iy >= clipBottom) { 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; } + // 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 bool hasChildren = false; @@ -297,18 +370,66 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co } // Draw text - uint32_t fg = c->fgColor ? c->fgColor : colors->contentFg; - uint32_t bg = c->bgColor ? c->bgColor : colors->contentBg; - drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, false); + drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected); // Recurse into expanded children 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 // ============================================================ @@ -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 // ============================================================ @@ -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 // ============================================================ @@ -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 // ============================================================ @@ -663,6 +987,9 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) return; } + // Set selection + hit->as.treeView.selectedItem = item; + // Check if click is on expand/collapse icon bool hasChildren = false; @@ -794,7 +1121,8 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit paintTreeItems(w, d, ops, font, colors, 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 setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index c9ed53d..78182c4 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -505,7 +505,6 @@ static void setupTerminalWindow(AppContextT *ctx) { "========================================\x1B[0m\r\n" "\r\n" "\x1B[1mBold text\x1B[0m, " - "\x1B[4mUnderlined\x1B[0m, " "\x1B[7mReverse\x1B[0m, " "\x1B[5mBlinking\x1B[0m\r\n" "\r\n"