// widgetTreeView.c — TreeView and TreeItem widgets #include "widgetInternal.h" // ============================================================ // Prototypes // ============================================================ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font); static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth); static void clearAllSelections(WidgetT *parent); 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 WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView); static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t innerW); 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 *treeView); static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView); static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to); 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); // ============================================================ // calcTreeItemsHeight // ============================================================ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font) { int32_t totalH = 0; for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTreeItemE || !c->visible) { continue; } totalH += font->charHeight; if (c->as.treeItem.expanded && c->firstChild) { totalH += calcTreeItemsHeight(c, font); } } return totalH; } // ============================================================ // calcTreeItemsMaxWidth // ============================================================ static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth) { int32_t maxW = 0; for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTreeItemE || !c->visible) { continue; } int32_t indent = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP; int32_t textW = (int32_t)strlen(c->as.treeItem.text) * font->charWidth; int32_t itemW = indent + textW; if (itemW > maxW) { maxW = itemW; } if (c->as.treeItem.expanded && c->firstChild) { int32_t childW = calcTreeItemsMaxWidth(c, font, depth + 1); if (childW > maxW) { maxW = childW; } } } return maxW; } // ============================================================ // clearAllSelections // ============================================================ static void clearAllSelections(WidgetT *parent) { for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTreeItemE) { continue; } c->as.treeItem.selected = false; if (c->firstChild) { clearAllSelections(c); } } } // ============================================================ // 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 // ============================================================ static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth) { for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTreeItemE || !c->visible) { continue; } c->x = x; c->y = *y; c->w = width; c->h = font->charHeight; *y += font->charHeight; if (c->as.treeItem.expanded && c->firstChild) { layoutTreeItems(c, font, x, y, width, depth + 1); } } } // ============================================================ // 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; } // ============================================================ // paintReorderIndicator // ============================================================ // // Draw a 2px insertion line at the drop target position. static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t innerW) { if (!w->as.treeView.reorderable || !w->as.treeView.dropTarget || !w->as.treeView.dragItem) { return; } int32_t dropY = treeItemYPos(w, w->as.treeView.dropTarget, font); if (dropY < 0) { return; } int32_t lineY = w->y + TREE_BORDER - w->as.treeView.scrollPos + dropY; if (w->as.treeView.dropAfter) { lineY += font->charHeight; } int32_t lineX = w->x + TREE_BORDER; drawHLine(d, ops, lineX, lineY, innerW, colors->contentFg); drawHLine(d, ops, lineX, lineY + 1, innerW, colors->contentFg); } // ============================================================ // 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, WidgetT *treeView) { bool multi = treeView->as.treeView.multiSelect; for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTreeItemE || !c->visible) { continue; } int32_t ix = baseX + depth * TREE_INDENT; int32_t iy = *itemY; *itemY += font->charHeight; // 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, treeView); } continue; } // Highlight selected item(s) bool isSelected; if (multi) { isSelected = c->as.treeItem.selected; } else { isSelected = (c == treeView->as.treeView.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; for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) { if (gc->type == WidgetTreeItemE) { hasChildren = true; break; } } int32_t textX = ix; if (hasChildren) { int32_t iconX = ix; int32_t iconY = iy + (font->charHeight - TREE_EXPAND_SIZE) / 2; // Draw box rectFill(d, ops, iconX + 1, iconY + 1, TREE_EXPAND_SIZE - 2, TREE_EXPAND_SIZE - 2, colors->contentBg); drawHLine(d, ops, iconX, iconY, TREE_EXPAND_SIZE, colors->windowShadow); drawHLine(d, ops, iconX, iconY + TREE_EXPAND_SIZE - 1, TREE_EXPAND_SIZE, colors->windowShadow); drawVLine(d, ops, iconX, iconY, TREE_EXPAND_SIZE, colors->windowShadow); drawVLine(d, ops, iconX + TREE_EXPAND_SIZE - 1, iconY, TREE_EXPAND_SIZE, colors->windowShadow); // Draw + or - int32_t mid = TREE_EXPAND_SIZE / 2; drawHLine(d, ops, iconX + 2, iconY + mid, TREE_EXPAND_SIZE - 4, colors->contentFg); if (!c->as.treeItem.expanded) { drawVLine(d, ops, iconX + mid, iconY + 2, TREE_EXPAND_SIZE - 4, colors->contentFg); } textX += TREE_EXPAND_SIZE + TREE_ICON_GAP; } else { textX += TREE_EXPAND_SIZE + TREE_ICON_GAP; } // Draw text drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected); // Draw focus rectangle around cursor item in multi-select mode if (multi && c == treeView->as.treeView.selectedItem && treeView->focused) { uint32_t focusFg = isSelected ? colors->menuHighlightFg : colors->contentFg; drawFocusRect(d, ops, baseX, iy, d->clipW, font->charHeight, focusFg); } // Recurse into expanded children if (c->as.treeItem.expanded && c->firstChild) { paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, treeView); } } } // ============================================================ // 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; } // ============================================================ // selectRange // ============================================================ // // Select all visible items between 'from' and 'to' (inclusive). // Direction is auto-detected. static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to) { if (!from || !to) { return; } if (from == to) { from->as.treeItem.selected = true; return; } // Walk forward from 'from'. If we hit 'to', that's the direction. // Otherwise, walk forward from 'to' to find 'from'. WidgetT *start = from; WidgetT *end = to; // Check if 'to' comes after 'from' bool forward = false; for (WidgetT *v = nextVisibleItem(from, treeView); v; v = nextVisibleItem(v, treeView)) { if (v == to) { forward = true; break; } } if (!forward) { start = to; end = from; } for (WidgetT *v = start; v; v = nextVisibleItem(v, treeView)) { v->as.treeItem.selected = true; if (v == end) { break; } } } // ============================================================ // treeCalcScrollbarNeeds // ============================================================ // // Compute content dimensions and determine which scrollbars are // needed, accounting for the mutual space dependency between them. static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb) { int32_t totalH = calcTreeItemsHeight(w, font); int32_t totalW = calcTreeItemsMaxWidth(w, font, 0); int32_t innerH = w->h - TREE_BORDER * 2; int32_t innerW = w->w - TREE_BORDER * 2; bool needVSb = (totalH > innerH); bool needHSb = (totalW > innerW); // V scrollbar reduces available width — may trigger H scrollbar if (needVSb) { innerW -= WGT_SB_W; if (!needHSb && totalW > innerW) { needHSb = true; } } // H scrollbar reduces available height — may trigger V scrollbar if (needHSb) { innerH -= WGT_SB_W; if (!needVSb && totalH > innerH) { needVSb = true; innerW -= WGT_SB_W; } } *outTotalH = totalH; *outTotalW = totalW; *outInnerH = innerH; *outInnerW = innerW; *outNeedVSb = needVSb; *outNeedHSb = needHSb; } // ============================================================ // treeItemAtY // ============================================================ // // Find the tree item at a given Y coordinate. static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font) { for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTreeItemE || !c->visible) { continue; } if (targetY >= *curY && targetY < *curY + font->charHeight) { return c; } *curY += font->charHeight; if (c->as.treeItem.expanded && c->firstChild) { WidgetT *found = treeItemAtY(c, targetY, curY, font); if (found) { return found; } } } return NULL; } // ============================================================ // 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; } // ============================================================ // wgtTreeItem // ============================================================ WidgetT *wgtTreeItem(WidgetT *parent, const char *text) { WidgetT *w = widgetAlloc(parent, WidgetTreeItemE); if (w) { w->as.treeItem.text = text; w->as.treeItem.expanded = false; } return w; } // ============================================================ // 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 // ============================================================ bool wgtTreeItemIsExpanded(const WidgetT *w) { VALIDATE_WIDGET(w, WidgetTreeItemE, false); return w->as.treeItem.expanded; } // ============================================================ // wgtTreeItemSetExpanded // ============================================================ void wgtTreeItemSetExpanded(WidgetT *w, bool expanded) { VALIDATE_WIDGET_VOID(w, WidgetTreeItemE); w->as.treeItem.expanded = expanded; } // ============================================================ // wgtTreeView // ============================================================ WidgetT *wgtTreeView(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, WidgetTreeViewE); if (w) { w->weight = 100; } return w; } // ============================================================ // wgtTreeViewGetSelected // ============================================================ WidgetT *wgtTreeViewGetSelected(const WidgetT *w) { VALIDATE_WIDGET(w, WidgetTreeViewE, NULL); return w->as.treeView.selectedItem; } // ============================================================ // wgtTreeViewSetSelected // ============================================================ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) { VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); w->as.treeView.selectedItem = item; if (w->as.treeView.multiSelect && item) { clearAllSelections(w); item->as.treeItem.selected = true; w->as.treeView.anchorItem = item; } } // ============================================================ // wgtTreeViewSetMultiSelect // ============================================================ void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi) { VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); w->as.treeView.multiSelect = multi; } // ============================================================ // wgtTreeViewSetReorderable // ============================================================ void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable) { VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); w->as.treeView.reorderable = reorderable; } // ============================================================ // widgetTreeViewNextVisible // ============================================================ // // Non-static wrapper around nextVisibleItem for use by // widgetReorderUpdate() in widgetEvent.c. WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) { return nextVisibleItem(item, treeView); } // ============================================================ // wgtTreeItemIsSelected // ============================================================ bool wgtTreeItemIsSelected(const WidgetT *w) { VALIDATE_WIDGET(w, WidgetTreeItemE, false); return w->as.treeItem.selected; } // ============================================================ // wgtTreeItemSetSelected // ============================================================ void wgtTreeItemSetSelected(WidgetT *w, bool selected) { VALIDATE_WIDGET_VOID(w, WidgetTreeItemE); w->as.treeItem.selected = selected; } // ============================================================ // widgetTreeViewCalcMinSize // ============================================================ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t minContentW = TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP + 6 * font->charWidth; w->calcMinW = minContentW + TREE_BORDER * 2 + WGT_SB_W; w->calcMinH = TREE_MIN_ROWS * font->charHeight + TREE_BORDER * 2; } // ============================================================ // widgetTreeViewOnKey // ============================================================ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { VALIDATE_WIDGET_VOID(w, WidgetTreeViewE); bool multi = w->as.treeView.multiSelect; bool shift = (mod & KEY_MOD_SHIFT) != 0; 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 if (key == ' ' && multi) { // Space — toggle selection of current item in multi-select if (sel) { sel->as.treeItem.selected = !sel->as.treeItem.selected; w->as.treeView.anchorItem = sel; } } else { return; } // Update multi-select state after Up/Down navigation WidgetT *newSel = w->as.treeView.selectedItem; if (multi && newSel != sel && newSel) { if (shift && w->as.treeView.anchorItem) { // Shift+arrow: range from anchor to new cursor clearAllSelections(w); selectRange(w, w->as.treeView.anchorItem, newSel); } // Plain arrow: just move cursor, leave selections untouched } // Set anchor on first selection if not set if (multi && !w->as.treeView.anchorItem && newSel) { w->as.treeView.anchorItem = newSel; } // 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 // ============================================================ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) { // Auto-select first item if nothing is selected if (!w->as.treeView.selectedItem) { WidgetT *first = firstVisibleItem(w); w->as.treeView.selectedItem = first; if (w->as.treeView.multiSelect && first) { first->as.treeItem.selected = true; w->as.treeView.anchorItem = first; } } 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); int32_t itemW = innerW > totalW ? innerW : totalW; int32_t innerX = w->x + TREE_BORDER; int32_t innerY = w->y + TREE_BORDER; layoutTreeItems(w, font, innerX, &innerY, itemW, 0); } // ============================================================ // widgetTreeViewOnMouse // ============================================================ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { hit->focused = true; AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t totalH; int32_t totalW; int32_t innerH; int32_t innerW; bool needVSb; bool needHSb; treeCalcScrollbarNeeds(hit, font, &totalH, &totalW, &innerH, &innerW, &needVSb, &needHSb); // Clamp scroll positions int32_t maxScrollV = totalH - innerH; int32_t maxScrollH = totalW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } hit->as.treeView.scrollPos = clampInt(hit->as.treeView.scrollPos, 0, maxScrollV); hit->as.treeView.scrollPosH = clampInt(hit->as.treeView.scrollPosH, 0, maxScrollH); // Check if click is on the vertical scrollbar if (needVSb) { int32_t sbX = hit->x + hit->w - TREE_BORDER - WGT_SB_W; if (vx >= sbX && vy < hit->y + TREE_BORDER + innerH) { int32_t relY = vy - (hit->y + TREE_BORDER); int32_t pageSize = innerH - font->charHeight; if (pageSize < font->charHeight) { pageSize = font->charHeight; } ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, totalH, innerH, hit->as.treeView.scrollPos); if (sh == ScrollHitArrowDecE) { hit->as.treeView.scrollPos -= font->charHeight; } else if (sh == ScrollHitArrowIncE) { hit->as.treeView.scrollPos += font->charHeight; } else if (sh == ScrollHitPageDecE) { hit->as.treeView.scrollPos -= pageSize; } else if (sh == ScrollHitPageIncE) { hit->as.treeView.scrollPos += pageSize; } hit->as.treeView.scrollPos = clampInt(hit->as.treeView.scrollPos, 0, maxScrollV); return; } } // Check if click is on the horizontal scrollbar if (needHSb) { int32_t sbY = hit->y + hit->h - TREE_BORDER - WGT_SB_W; if (vy >= sbY && vx < hit->x + TREE_BORDER + innerW) { int32_t relX = vx - (hit->x + TREE_BORDER); int32_t pageSize = innerW - font->charWidth; if (pageSize < font->charWidth) { pageSize = font->charWidth; } ScrollHitE sh = widgetScrollbarHitTest(innerW, relX, totalW, innerW, hit->as.treeView.scrollPosH); if (sh == ScrollHitArrowDecE) { hit->as.treeView.scrollPosH -= font->charWidth; } else if (sh == ScrollHitArrowIncE) { hit->as.treeView.scrollPosH += font->charWidth; } else if (sh == ScrollHitPageDecE) { hit->as.treeView.scrollPosH -= pageSize; } else if (sh == ScrollHitPageIncE) { hit->as.treeView.scrollPosH += pageSize; } hit->as.treeView.scrollPosH = clampInt(hit->as.treeView.scrollPosH, 0, maxScrollH); return; } } // Click in dead corner (both scrollbars present) — ignore if (needVSb && needHSb) { int32_t cornerX = hit->x + hit->w - TREE_BORDER - WGT_SB_W; int32_t cornerY = hit->y + hit->h - TREE_BORDER - WGT_SB_W; if (vx >= cornerX && vy >= cornerY) { return; } } // Tree item click — adjust for scroll offsets int32_t curY = hit->y + TREE_BORDER - hit->as.treeView.scrollPos; WidgetT *item = treeItemAtY(hit, vy, &curY, font); if (!item) { return; } // Update selection bool multi = hit->as.treeView.multiSelect; bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0; bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0; hit->as.treeView.selectedItem = item; if (multi) { if (ctrl) { // Ctrl+click: toggle item, update anchor item->as.treeItem.selected = !item->as.treeItem.selected; hit->as.treeView.anchorItem = item; } else if (shift && hit->as.treeView.anchorItem) { // Shift+click: range from anchor to clicked clearAllSelections(hit); selectRange(hit, hit->as.treeView.anchorItem, item); } else { // Plain click: select only this item, update anchor clearAllSelections(hit); item->as.treeItem.selected = true; hit->as.treeView.anchorItem = item; } } // Check if click is on expand/collapse icon bool hasChildren = false; bool clickedExpandIcon = false; for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) { if (gc->type == WidgetTreeItemE) { hasChildren = true; break; } } if (hasChildren) { // Calculate indent depth int32_t depth = 0; WidgetT *p = item->parent; while (p && p->type == WidgetTreeItemE) { depth++; p = p->parent; } int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT - hit->as.treeView.scrollPosH; if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) { clickedExpandIcon = true; item->as.treeItem.expanded = !item->as.treeItem.expanded; // Clamp scroll positions if collapsing reduced content size if (!item->as.treeItem.expanded) { int32_t newTotalH; int32_t newTotalW; int32_t newInnerH; int32_t newInnerW; bool newNeedVSb; bool newNeedHSb; treeCalcScrollbarNeeds(hit, font, &newTotalH, &newTotalW, &newInnerH, &newInnerW, &newNeedVSb, &newNeedHSb); int32_t newMaxScrlV = newTotalH - newInnerH; int32_t newMaxScrlH = newTotalW - newInnerW; if (newMaxScrlV < 0) { newMaxScrlV = 0; } if (newMaxScrlH < 0) { newMaxScrlH = 0; } hit->as.treeView.scrollPos = clampInt(hit->as.treeView.scrollPos, 0, newMaxScrlV); hit->as.treeView.scrollPosH = clampInt(hit->as.treeView.scrollPosH, 0, newMaxScrlH); } if (item->onChange) { item->onChange(item); } } else { if (item->onClick) { item->onClick(item); } } } else { if (item->onClick) { item->onClick(item); } } // Initiate drag-reorder if enabled (not from expand icon or modifier clicks) if (hit->as.treeView.reorderable && !clickedExpandIcon && !shift && !ctrl) { hit->as.treeView.dragItem = item; hit->as.treeView.dropTarget = NULL; sDragReorder = hit; } } // ============================================================ // widgetTreeViewPaint // ============================================================ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; 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); // Clamp scroll positions int32_t maxScrollV = totalH - innerH; int32_t maxScrollH = totalW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } w->as.treeView.scrollPos = clampInt(w->as.treeView.scrollPos, 0, maxScrollV); w->as.treeView.scrollPosH = clampInt(w->as.treeView.scrollPosH, 0, maxScrollH); // Sunken border BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Set clip rect to inner content area (excludes scrollbars) int32_t oldClipX = d->clipX; int32_t oldClipY = d->clipY; int32_t oldClipW = d->clipW; int32_t oldClipH = d->clipH; setClipRect(d, w->x + TREE_BORDER, w->y + TREE_BORDER, innerW, innerH); // Paint tree items offset by both scroll positions int32_t itemY = w->y + TREE_BORDER - w->as.treeView.scrollPos; int32_t baseX = w->x + TREE_BORDER - w->as.treeView.scrollPosH; paintTreeItems(w, d, ops, font, colors, baseX, &itemY, 0, w->y + TREE_BORDER, w->y + TREE_BORDER + innerH, w); // Draw drag-reorder insertion indicator (while still clipped) paintReorderIndicator(w, d, ops, font, colors, innerW); // Restore clip rect setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); // Draw scrollbars if (needVSb) { int32_t sbX = w->x + w->w - TREE_BORDER - WGT_SB_W; int32_t sbY = w->y + TREE_BORDER; widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, totalH, innerH, w->as.treeView.scrollPos); } if (needHSb) { int32_t sbX = w->x + TREE_BORDER; int32_t sbY = w->y + w->h - TREE_BORDER - WGT_SB_W; widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalW, innerW, w->as.treeView.scrollPosH); // Fill dead corner when both scrollbars present if (needVSb) { rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace); } } if (w->focused) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } }