1967 lines
64 KiB
C
1967 lines
64 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
#define DVX_WIDGET_IMPL
|
|
// widgetTreeView.c -- TreeView and TreeItem widgets
|
|
//
|
|
// A hierarchical list with expand/collapse nodes, scrolling, multi-select,
|
|
// and drag-reorder support.
|
|
//
|
|
// Architecture: TreeViewE is the container/viewport widget. TreeItemE
|
|
// children are the actual data nodes, forming a tree via the standard
|
|
// widget parent/child/sibling links. This reuses the existing widget
|
|
// tree structure as the data model -- no separate tree data structure
|
|
// needed. Each TreeItemE can have child TreeItemEs for nesting.
|
|
//
|
|
// Traversal: the tree is navigated in depth-first order using
|
|
// nextVisibleItem/prevVisibleItem. "Visible" means the item's parent
|
|
// chain is fully expanded -- collapsed subtrees are skipped. These
|
|
// traversal functions are O(depth) worst case, which is fine for the
|
|
// moderate tree depths typical in a DOS GUI.
|
|
//
|
|
// Selection model: single-select uses selectedItem pointer on the
|
|
// TreeViewE. Multi-select adds per-item 'selected' booleans and an
|
|
// anchorItem for Shift+click range selection. The cursor (selectedItem)
|
|
// and selection are separate concepts in multi-select mode -- the
|
|
// cursor is the "current" item for keyboard navigation, while the
|
|
// selected set is the highlighted items. Space toggles individual
|
|
// items, Shift+arrow extends range from anchor.
|
|
//
|
|
// Scrolling: dual-axis with automatic scrollbar appearance using the
|
|
// same two-pass mutual-dependency resolution as ScrollPane. Vertical
|
|
// scroll is in pixels (not items), allowing smooth scrolling. The
|
|
// treeCalcScrollbarNeeds function centralizes the dimension/scrollbar
|
|
// computation shared by layout, paint, and mouse handlers.
|
|
//
|
|
// Rendering: items are painted recursively with depth-based indent.
|
|
// The expand/collapse icon is a 9x9 box with +/- inside (Windows
|
|
// Explorer style). Only items within the visible clip region are
|
|
// actually drawn -- items outside clipTop/clipBottom are skipped
|
|
// (though their Y positions are still accumulated for correct
|
|
// positioning of subsequent items).
|
|
//
|
|
// Drag-reorder: when enabled, mouse-down on an item sets dragItem.
|
|
// Mouse-move (handled in widgetEvent.c) updates dropTarget/dropAfter
|
|
// to show an insertion line. Mouse-up (widgetReorderDrop) performs
|
|
// the actual node reparenting via widgetRemoveChild/widgetAddChild.
|
|
//
|
|
// Performance note: calcTreeItemsHeight and calcTreeItemsMaxWidth
|
|
// walk the full visible tree, but results are cached in the TreeView's
|
|
// cachedTotalHeight/cachedMaxWidth fields. The cache is invalidated
|
|
// (dimsValid = false) whenever the tree structure changes: item add/
|
|
// remove, expand/collapse, text change, or drag-reorder.
|
|
|
|
#include "dvxWgtP.h"
|
|
|
|
#define TREE_INDENT 16
|
|
#define TREE_EXPAND_SIZE 9
|
|
#define TREE_ICON_GAP 4
|
|
#define TREE_BORDER 2
|
|
#define TREE_MIN_ROWS 4
|
|
|
|
static int32_t sTreeViewTypeId = -1;
|
|
static int32_t sTreeItemTypeId = -1;
|
|
|
|
typedef struct {
|
|
int32_t scrollPos;
|
|
int32_t scrollPosH;
|
|
WidgetT *selectedItem;
|
|
WidgetT *anchorItem;
|
|
bool multiSelect;
|
|
bool reorderable;
|
|
WidgetT *dragItem;
|
|
WidgetT *dropTarget;
|
|
bool dropAfter;
|
|
bool dropInto;
|
|
int32_t cachedTotalHeight;
|
|
int32_t cachedMaxWidth;
|
|
bool dimsValid;
|
|
int32_t sbDragOrient;
|
|
int32_t sbDragOff;
|
|
} TreeViewDataT;
|
|
|
|
typedef struct {
|
|
const char *text;
|
|
bool expanded;
|
|
bool selected;
|
|
} TreeItemDataT;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void basAddChildItem(WidgetT *w, int32_t parentIdx, const char *text);
|
|
static void basAddItem(WidgetT *w, const char *text);
|
|
static void basClear(WidgetT *w);
|
|
static const char *basGetItemText(const WidgetT *w, int32_t idx);
|
|
static bool basIsExpanded(const WidgetT *w, int32_t idx);
|
|
static bool basIsItemSelected(const WidgetT *w, int32_t idx);
|
|
static int32_t basItemCount(const WidgetT *w);
|
|
static void basSetExpanded(WidgetT *w, int32_t idx, bool expanded);
|
|
static void basSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
|
static int32_t basTreeCountItems(WidgetT *parent);
|
|
static WidgetT *basTreeFindByIndex(WidgetT *parent, int32_t target, int32_t *cur);
|
|
static WidgetT *basTreeItemAt(WidgetT *tree, int32_t index);
|
|
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 invalidateTreeDims(WidgetT *w);
|
|
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 setSelectedItem(WidgetT *treeView, WidgetT *item);
|
|
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);
|
|
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
|
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
|
bool wgtTreeItemIsSelected(const WidgetT *w);
|
|
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
|
void wgtTreeItemSetSelected(WidgetT *w, bool selected);
|
|
WidgetT *wgtTreeView(WidgetT *parent);
|
|
WidgetT *wgtTreeViewGetSelected(const WidgetT *w);
|
|
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi);
|
|
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable);
|
|
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item);
|
|
static void widgetTreeItemDestroy(WidgetT *w);
|
|
const char *widgetTreeItemGetText(const WidgetT *w);
|
|
void widgetTreeItemSetText(WidgetT *w, const char *text);
|
|
void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
static void widgetTreeViewDestroy(WidgetT *w);
|
|
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
|
|
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView);
|
|
void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child);
|
|
static void widgetTreeViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
|
static void widgetTreeViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
|
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
|
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
static void widgetTreeViewReorderDrop(WidgetT *w);
|
|
static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
|
static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
|
|
|
|
|
|
// AddChildItem index%, text$ -- add child under item at depth-first index
|
|
static void basAddChildItem(WidgetT *w, int32_t parentIdx, const char *text) {
|
|
WidgetT *parent = basTreeItemAt(w, parentIdx);
|
|
|
|
if (parent) {
|
|
wgtTreeItem(parent, text);
|
|
}
|
|
}
|
|
|
|
|
|
// AddItem text$ -- add root-level item
|
|
static void basAddItem(WidgetT *w, const char *text) {
|
|
wgtTreeItem(w, text);
|
|
}
|
|
|
|
|
|
// Clear -- remove all items
|
|
static void basClear(WidgetT *w) {
|
|
// Remove all tree item children
|
|
WidgetT *c = w->firstChild;
|
|
|
|
while (c) {
|
|
WidgetT *next = c->nextSibling;
|
|
|
|
if (c->type == sTreeItemTypeId) {
|
|
wgtDestroy(c);
|
|
}
|
|
|
|
c = next;
|
|
}
|
|
}
|
|
|
|
|
|
// GetItemText$(index%) -- get item text at index
|
|
static const char *basGetItemText(const WidgetT *w, int32_t idx) {
|
|
WidgetT *item = basTreeItemAt((WidgetT *)w, idx);
|
|
|
|
if (item && item->data) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)item->data;
|
|
return ti->text ? ti->text : "";
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
// IsExpanded(index%) -- check if item at index is expanded
|
|
static bool basIsExpanded(const WidgetT *w, int32_t idx) {
|
|
WidgetT *item = basTreeItemAt((WidgetT *)w, idx);
|
|
return item ? wgtTreeItemIsExpanded(item) : false;
|
|
}
|
|
|
|
|
|
// IsItemSelected(index%) -- check if item at index is selected
|
|
static bool basIsItemSelected(const WidgetT *w, int32_t idx) {
|
|
WidgetT *item = basTreeItemAt((WidgetT *)w, idx);
|
|
return item ? wgtTreeItemIsSelected(item) : false;
|
|
}
|
|
|
|
|
|
// ItemCount -- return total number of items (depth-first)
|
|
static int32_t basItemCount(const WidgetT *w) {
|
|
return basTreeCountItems((WidgetT *)w);
|
|
}
|
|
|
|
|
|
// SetExpanded index%, expanded -- expand/collapse item at index
|
|
static void basSetExpanded(WidgetT *w, int32_t idx, bool expanded) {
|
|
WidgetT *item = basTreeItemAt(w, idx);
|
|
|
|
if (item) {
|
|
wgtTreeItemSetExpanded(item, expanded);
|
|
}
|
|
}
|
|
|
|
|
|
// SetItemSelected index%, selected -- select/deselect item at index
|
|
static void basSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
|
WidgetT *item = basTreeItemAt(w, idx);
|
|
|
|
if (item) {
|
|
wgtTreeItemSetSelected(item, selected);
|
|
}
|
|
}
|
|
|
|
|
|
static int32_t basTreeCountItems(WidgetT *parent) {
|
|
int32_t count = 0;
|
|
|
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTreeItemTypeId) {
|
|
continue;
|
|
}
|
|
|
|
count++;
|
|
count += basTreeCountItems(c);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
static WidgetT *basTreeFindByIndex(WidgetT *parent, int32_t target, int32_t *cur) {
|
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTreeItemTypeId) {
|
|
continue;
|
|
}
|
|
|
|
if (*cur == target) {
|
|
return c;
|
|
}
|
|
|
|
(*cur)++;
|
|
|
|
WidgetT *found = basTreeFindByIndex(c, target, cur);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static WidgetT *basTreeItemAt(WidgetT *tree, int32_t index) {
|
|
int32_t cur = 0;
|
|
return basTreeFindByIndex(tree, index, &cur);
|
|
}
|
|
|
|
|
|
// Recursively sums the pixel height of all visible (expanded) items.
|
|
// Each item is one charHeight row. Only descends into expanded nodes,
|
|
// so collapsed subtrees contribute zero height.
|
|
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 != sTreeItemTypeId || !c->visible) {
|
|
continue;
|
|
}
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
totalH += font->charHeight;
|
|
|
|
if (ti->expanded && c->firstChild) {
|
|
totalH += calcTreeItemsHeight(c, font);
|
|
}
|
|
}
|
|
|
|
return totalH;
|
|
}
|
|
|
|
|
|
// Finds the widest visible item accounting for depth-based indent.
|
|
// Used to determine if a horizontal scrollbar is needed. The text
|
|
// width uses strlen * charWidth (monospace font assumption) rather
|
|
// than a proportional measurement function.
|
|
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 != sTreeItemTypeId || !c->visible) {
|
|
continue;
|
|
}
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
int32_t indent = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP;
|
|
int32_t textW = (int32_t)strlen(ti->text) * font->charWidth;
|
|
int32_t itemW = indent + textW;
|
|
|
|
if (itemW > maxW) {
|
|
maxW = itemW;
|
|
}
|
|
|
|
if (ti->expanded && c->firstChild) {
|
|
int32_t childW = calcTreeItemsMaxWidth(c, font, depth + 1);
|
|
|
|
if (childW > maxW) {
|
|
maxW = childW;
|
|
}
|
|
}
|
|
}
|
|
|
|
return maxW;
|
|
}
|
|
|
|
|
|
static void clearAllSelections(WidgetT *parent) {
|
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTreeItemTypeId) {
|
|
continue;
|
|
}
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
ti->selected = false;
|
|
|
|
if (c->firstChild) {
|
|
clearAllSelections(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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 == sTreeItemTypeId && c->visible) {
|
|
return c;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// Walk up from any widget (typically a TreeItem) to find the ancestor
|
|
// TreeView and mark its cached dimensions invalid. This ensures the
|
|
// next treeCalcScrollbarNeeds call will recompute height and width.
|
|
static void invalidateTreeDims(WidgetT *w) {
|
|
for (WidgetT *p = w; p; p = p->parent) {
|
|
if (p->type == sTreeViewTypeId) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)p->data;
|
|
tv->dimsValid = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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 != sTreeItemTypeId || !c->visible) {
|
|
continue;
|
|
}
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
|
|
c->x = x;
|
|
c->y = *y;
|
|
c->w = width;
|
|
c->h = font->charHeight;
|
|
*y += font->charHeight;
|
|
|
|
if (ti->expanded && c->firstChild) {
|
|
layoutTreeItems(c, font, x, y, width, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Depth-first forward traversal: try children first (if expanded),
|
|
// then next sibling, then walk up to find an uncle. This gives the
|
|
// standard tree traversal order matching what the user sees on screen.
|
|
// The treeView parameter is the traversal boundary -- we stop walking
|
|
// up when we reach it.
|
|
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)item->data;
|
|
|
|
// If expanded with children, descend
|
|
if (ti->expanded) {
|
|
for (WidgetT *c = item->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == sTreeItemTypeId && c->visible) {
|
|
return c;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try next sibling
|
|
for (WidgetT *s = item->nextSibling; s; s = s->nextSibling) {
|
|
if (s->type == sTreeItemTypeId && 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 == sTreeItemTypeId && s->visible) {
|
|
return s;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// 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) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
|
|
if (!tv->reorderable || !tv->dropTarget || !tv->dragItem) {
|
|
return;
|
|
}
|
|
|
|
int32_t dropY = treeItemYPos(w, tv->dropTarget, font);
|
|
|
|
if (dropY < 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t baseY = w->y + TREE_BORDER - tv->scrollPos + dropY;
|
|
int32_t lineX = w->x + TREE_BORDER;
|
|
|
|
if (tv->dropInto) {
|
|
// Draw a rectangle around the target to indicate reparenting
|
|
drawHLine(d, ops, lineX, baseY, innerW, colors->contentFg);
|
|
drawHLine(d, ops, lineX, baseY + font->charHeight - 1, innerW, colors->contentFg);
|
|
drawVLine(d, ops, lineX, baseY, font->charHeight, colors->contentFg);
|
|
drawVLine(d, ops, lineX + innerW - 1, baseY, font->charHeight, colors->contentFg);
|
|
} else {
|
|
// Draw a 2px insertion line
|
|
int32_t lineY = baseY;
|
|
|
|
if (tv->dropAfter) {
|
|
lineY += font->charHeight;
|
|
}
|
|
|
|
drawHLine(d, ops, lineX, lineY, innerW, colors->contentFg);
|
|
drawHLine(d, ops, lineX, lineY + 1, innerW, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
|
|
// Recursive item painting with visibility culling. Items whose Y
|
|
// range falls entirely outside [clipTop, clipBottom) are skipped --
|
|
// their Y is still accumulated so subsequent items position correctly,
|
|
// but no draw calls are made. This is the key performance optimization
|
|
// for large trees: only visible items incur draw cost.
|
|
//
|
|
// Each visible item draws: optional selection highlight background,
|
|
// expand/collapse icon (if has children), and text label. The expand
|
|
// icon is a 9x9 bordered box with a horizontal line (minus) when
|
|
// expanded, or horizontal + vertical lines (plus) when collapsed.
|
|
// This matches the Windows 95/NT Explorer style.
|
|
//
|
|
// In multi-select mode, a focus rect is drawn around the cursor item
|
|
// (selectedItem) to distinguish it from selected-but-not-cursor items.
|
|
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) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)treeView->data;
|
|
bool multi = tv->multiSelect;
|
|
|
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTreeItemTypeId || !c->visible) {
|
|
continue;
|
|
}
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
|
|
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 (ti->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 = ti->selected;
|
|
} else {
|
|
isSelected = (c == tv->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 == sTreeItemTypeId) {
|
|
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 (!ti->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, ti->text, fg, bg, isSelected);
|
|
|
|
// Draw focus rectangle around cursor item in multi-select mode
|
|
if (multi && c == tv->selectedItem && treeView == sFocusedWidget) {
|
|
uint32_t focusFg = isSelected ? colors->menuHighlightFg : colors->contentFg;
|
|
drawFocusRect(d, ops, baseX, iy, d->clipW, font->charHeight, focusFg);
|
|
}
|
|
|
|
// Recurse into expanded children
|
|
if (ti->expanded && c->firstChild) {
|
|
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, treeView);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Depth-first backward traversal: find previous sibling, then descend
|
|
// to its last visible descendant (the deepest last-child chain). If
|
|
// no previous sibling, go to parent. This is the inverse of
|
|
// nextVisibleItem and produces the correct "up arrow" traversal order.
|
|
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 == sTreeItemTypeId && s->visible) {
|
|
prevSib = s;
|
|
}
|
|
}
|
|
|
|
if (prevSib) {
|
|
// Descend to last visible descendant of previous sibling
|
|
WidgetT *node = prevSib;
|
|
|
|
while (((TreeItemDataT *)node->data)->expanded) {
|
|
WidgetT *last = NULL;
|
|
|
|
for (WidgetT *c = node->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == sTreeItemTypeId && 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 == sTreeItemTypeId) {
|
|
return item->parent;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// Selects all visible items between 'from' and 'to' inclusive. The
|
|
// direction is auto-detected by walking forward from 'from' -- if
|
|
// 'to' is found, that's the forward direction; otherwise we swap.
|
|
// This handles both Shift+Down and Shift+Up range selection with
|
|
// the same code. The forward walk is O(N) in the worst case but
|
|
// range selection in trees is inherently O(N) anyway.
|
|
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to) {
|
|
if (!from || !to) {
|
|
return;
|
|
}
|
|
|
|
if (from == to) {
|
|
((TreeItemDataT *)from->data)->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)) {
|
|
((TreeItemDataT *)v->data)->selected = true;
|
|
|
|
if (v == end) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Set the selected item and fire the tree widget's onChange callback
|
|
// so the host knows the selection changed.
|
|
static void setSelectedItem(WidgetT *treeView, WidgetT *item) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)treeView->data;
|
|
|
|
if (tv->selectedItem == item) {
|
|
return;
|
|
}
|
|
|
|
tv->selectedItem = item;
|
|
|
|
if (treeView->onChange) {
|
|
treeView->onChange(treeView);
|
|
}
|
|
}
|
|
|
|
|
|
// 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) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
|
|
if (!tv->dimsValid) {
|
|
tv->cachedTotalHeight = calcTreeItemsHeight(w, font);
|
|
tv->cachedMaxWidth = calcTreeItemsMaxWidth(w, font, 0);
|
|
tv->dimsValid = true;
|
|
}
|
|
|
|
int32_t totalH = tv->cachedTotalHeight;
|
|
int32_t totalW = tv->cachedMaxWidth;
|
|
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;
|
|
}
|
|
|
|
|
|
// 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 != sTreeItemTypeId || !c->visible) {
|
|
continue;
|
|
}
|
|
|
|
if (targetY >= *curY && targetY < *curY + font->charHeight) {
|
|
return c;
|
|
}
|
|
|
|
*curY += font->charHeight;
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
|
|
if (ti->expanded && c->firstChild) {
|
|
WidgetT *found = treeItemAtY(c, targetY, curY, font);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
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 != sTreeItemTypeId || !c->visible) {
|
|
continue;
|
|
}
|
|
|
|
if (c == target) {
|
|
return 1;
|
|
}
|
|
|
|
*curY += font->charHeight;
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)c->data;
|
|
|
|
if (ti->expanded && c->firstChild) {
|
|
if (treeItemYPosHelper(c, target, curY, font)) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
WidgetT *wgtTreeItem(WidgetT *parent, const char *text) {
|
|
WidgetT *w = widgetAlloc(parent, sTreeItemTypeId);
|
|
|
|
if (w) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)calloc(1, sizeof(TreeItemDataT));
|
|
|
|
if (ti) {
|
|
w->data = ti;
|
|
ti->text = text ? strdup(text) : NULL;
|
|
ti->expanded = false;
|
|
}
|
|
|
|
invalidateTreeDims(w);
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
bool wgtTreeItemIsExpanded(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTreeItemTypeId, false);
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
return ti->expanded;
|
|
}
|
|
|
|
|
|
bool wgtTreeItemIsSelected(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTreeItemTypeId, false);
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
|
|
// ti->selected tracks multi-select state (Ctrl-click, shift-click
|
|
// range). In single-select mode, the current selection lives on
|
|
// the TreeView's tv->selectedItem pointer instead -- walk up to
|
|
// the owning TreeView and compare, so callers see the expected
|
|
// answer for both modes.
|
|
if (ti->selected) {
|
|
return true;
|
|
}
|
|
|
|
for (WidgetT *p = w->parent; p; p = p->parent) {
|
|
if (p->type == sTreeViewTypeId) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)p->data;
|
|
return tv && tv->selectedItem == w;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded) {
|
|
VALIDATE_WIDGET_VOID(w, sTreeItemTypeId);
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
ti->expanded = expanded;
|
|
invalidateTreeDims(w);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtTreeItemSetSelected(WidgetT *w, bool selected) {
|
|
VALIDATE_WIDGET_VOID(w, sTreeItemTypeId);
|
|
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
ti->selected = selected;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
WidgetT *wgtTreeView(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, sTreeViewTypeId);
|
|
|
|
if (w) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)calloc(1, sizeof(TreeViewDataT));
|
|
|
|
if (tv) {
|
|
w->data = tv;
|
|
}
|
|
|
|
w->weight = WGT_WEIGHT_FILL;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
WidgetT *wgtTreeViewGetSelected(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTreeViewTypeId, NULL);
|
|
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
return tv->selectedItem;
|
|
}
|
|
|
|
|
|
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi) {
|
|
VALIDATE_WIDGET_VOID(w, sTreeViewTypeId);
|
|
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
tv->multiSelect = multi;
|
|
}
|
|
|
|
|
|
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable) {
|
|
VALIDATE_WIDGET_VOID(w, sTreeViewTypeId);
|
|
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
tv->reorderable = reorderable;
|
|
}
|
|
|
|
|
|
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
|
|
VALIDATE_WIDGET_VOID(w, sTreeViewTypeId);
|
|
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
|
|
// Expand all ancestors so the item is visible in the tree.
|
|
// Invalidate cached dims so the scroll-into-view below sees the
|
|
// correct post-expansion totalH / item Y coordinate.
|
|
if (item) {
|
|
bool expandedAny = false;
|
|
|
|
for (WidgetT *p = item->parent; p && p != w; p = p->parent) {
|
|
if (p->type == sTreeItemTypeId) {
|
|
TreeItemDataT *pti = (TreeItemDataT *)p->data;
|
|
|
|
if (pti && !pti->expanded) {
|
|
pti->expanded = true;
|
|
expandedAny = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (expandedAny) {
|
|
tv->dimsValid = false;
|
|
}
|
|
}
|
|
|
|
setSelectedItem(w, item);
|
|
|
|
if (tv->multiSelect && item) {
|
|
clearAllSelections(w);
|
|
TreeItemDataT *ti = (TreeItemDataT *)item->data;
|
|
ti->selected = true;
|
|
tv->anchorItem = item;
|
|
}
|
|
|
|
// Scroll so the selected item is inside the visible band. Manual
|
|
// expand+click goes through widgetTreeViewOnMouse / OnKey which
|
|
// have their own scroll-into-view logic; programmatic callers
|
|
// (e.g. dvxhelp syncing the TOC from navigateToTopic) need this.
|
|
if (item) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
|
|
if (ctx) {
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t itemY = treeItemYPos(w, item, 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 < tv->scrollPos) {
|
|
tv->scrollPos = itemY;
|
|
} else if (itemY + font->charHeight > tv->scrollPos + innerH) {
|
|
tv->scrollPos = itemY + font->charHeight - innerH;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
static void widgetTreeItemDestroy(WidgetT *w) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
free((void *)ti->text);
|
|
free(w->data);
|
|
w->data = NULL;
|
|
}
|
|
|
|
|
|
const char *widgetTreeItemGetText(const WidgetT *w) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
return ti->text ? ti->text : "";
|
|
}
|
|
|
|
|
|
void widgetTreeItemSetText(WidgetT *w, const char *text) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
|
free((void *)ti->text);
|
|
ti->text = text ? strdup(text) : NULL;
|
|
invalidateTreeDims(w);
|
|
}
|
|
|
|
|
|
// Min size: wide enough for one indent level + expand icon + 6 chars
|
|
// of text + border + vertical scrollbar. Tall enough for TREE_MIN_ROWS
|
|
// (4) items. This ensures the tree is usable even when space is tight,
|
|
// while the weight=100 default allows it to grow to fill available space.
|
|
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;
|
|
}
|
|
|
|
|
|
static void widgetTreeViewDestroy(WidgetT *w) {
|
|
free(w->data);
|
|
w->data = NULL;
|
|
}
|
|
|
|
|
|
// Layout assigns (x, y, w, h) to all visible tree items at their
|
|
// scroll-adjusted screen coordinates. Item width extends to the
|
|
// full inner width (or total content width if wider), so selection
|
|
// highlight bars span the full visible width. Auto-selects the first
|
|
// item if nothing is selected yet, providing a reasonable default.
|
|
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
|
|
// Auto-select first item if nothing is selected. This runs at
|
|
// layout time (not user action), so set tv->selectedItem directly
|
|
// instead of going through setSelectedItem -- the latter fires
|
|
// onChange as if the user clicked, which triggers callers (e.g.
|
|
// dvxhelp's TOC sync) to re-navigate to the first tree item and
|
|
// clobber any programmatic selection the host is about to make.
|
|
if (!tv->selectedItem) {
|
|
WidgetT *first = firstVisibleItem(w);
|
|
tv->selectedItem = first;
|
|
|
|
if (tv->multiSelect && first) {
|
|
((TreeItemDataT *)first->data)->selected = true;
|
|
tv->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);
|
|
}
|
|
|
|
|
|
// Non-static wrapper around nextVisibleItem for use by
|
|
// widgetReorderUpdate() in widgetEvent.c.
|
|
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) {
|
|
return nextVisibleItem(item, treeView);
|
|
}
|
|
|
|
|
|
void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child) {
|
|
(void)child;
|
|
TreeViewDataT *tv = (TreeViewDataT *)parent->data;
|
|
tv->dimsValid = false;
|
|
}
|
|
|
|
|
|
static void widgetTreeViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
(void)root;
|
|
(void)x;
|
|
(void)y;
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
|
|
if (tv->dragItem) {
|
|
widgetTreeViewReorderDrop(w);
|
|
tv->dragItem = NULL;
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
static void widgetTreeViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
|
|
if (tv->dragItem) {
|
|
widgetTreeViewReorderUpdate(w, root, x, y);
|
|
} else {
|
|
widgetTreeViewScrollDragUpdate(w, tv->sbDragOrient, tv->sbDragOff, x, y);
|
|
}
|
|
}
|
|
|
|
|
|
// Keyboard navigation follows Windows Explorer conventions:
|
|
// - Up/Down: move cursor to prev/next visible item
|
|
// - Right: expand collapsed node, or move to first child if expanded
|
|
// - Left: collapse expanded node, or move to parent if collapsed
|
|
// - Enter: toggle expand/collapse for parents, onClick for leaves
|
|
// - Space (multi-select): toggle selection of cursor item
|
|
//
|
|
// After cursor movement, the view auto-scrolls to keep the cursor
|
|
// visible. Multi-select range extension (Shift+arrow) clears existing
|
|
// selections and selects the range from anchor to cursor.
|
|
//
|
|
// The Right/Left expand/collapse behavior provides efficient keyboard
|
|
// tree navigation: Right drills in, Left backs out, without needing
|
|
// separate expand/collapse keys.
|
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
VALIDATE_WIDGET_VOID(w, sTreeViewTypeId);
|
|
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
bool multi = tv->multiSelect;
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
WidgetT *sel = tv->selectedItem;
|
|
|
|
if (key == KEY_DOWN) {
|
|
// Down arrow -- next visible item
|
|
if (!sel) {
|
|
setSelectedItem(w, firstVisibleItem(w));
|
|
} else {
|
|
WidgetT *next = nextVisibleItem(sel, w);
|
|
|
|
if (next) {
|
|
setSelectedItem(w, next);
|
|
}
|
|
}
|
|
} else if (key == KEY_UP) {
|
|
// Up arrow -- previous visible item
|
|
if (!sel) {
|
|
setSelectedItem(w, firstVisibleItem(w));
|
|
} else {
|
|
WidgetT *prev = prevVisibleItem(sel, w);
|
|
|
|
if (prev) {
|
|
setSelectedItem(w, prev);
|
|
}
|
|
}
|
|
} else if (key == KEY_RIGHT) {
|
|
// Right arrow -- expand if collapsed, else move to first child
|
|
if (sel) {
|
|
TreeItemDataT *selTi = (TreeItemDataT *)sel->data;
|
|
bool hasChildren = false;
|
|
|
|
for (WidgetT *c = sel->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == sTreeItemTypeId) {
|
|
hasChildren = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasChildren) {
|
|
if (!selTi->expanded) {
|
|
selTi->expanded = true;
|
|
tv->dimsValid = false;
|
|
|
|
if (sel->onChange) {
|
|
sel->onChange(sel);
|
|
}
|
|
} else {
|
|
WidgetT *next = nextVisibleItem(sel, w);
|
|
|
|
if (next) {
|
|
setSelectedItem(w, next);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (key == KEY_LEFT) {
|
|
// Left arrow -- collapse if expanded, else move to parent
|
|
if (sel) {
|
|
TreeItemDataT *selTi = (TreeItemDataT *)sel->data;
|
|
|
|
if (selTi->expanded) {
|
|
selTi->expanded = false;
|
|
tv->dimsValid = false;
|
|
|
|
if (sel->onChange) {
|
|
sel->onChange(sel);
|
|
}
|
|
} else if (sel->parent && sel->parent != w && sel->parent->type == sTreeItemTypeId) {
|
|
setSelectedItem(w, sel->parent);
|
|
}
|
|
}
|
|
} else if (key == 0x0D) {
|
|
// Enter -- toggle expand/collapse for parents, onClick for leaves
|
|
if (sel) {
|
|
TreeItemDataT *selTi = (TreeItemDataT *)sel->data;
|
|
bool hasChildren = false;
|
|
|
|
for (WidgetT *c = sel->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == sTreeItemTypeId) {
|
|
hasChildren = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasChildren) {
|
|
selTi->expanded = !selTi->expanded;
|
|
tv->dimsValid = false;
|
|
|
|
if (sel->onChange) {
|
|
sel->onChange(sel);
|
|
}
|
|
} else {
|
|
if (sel->onDblClick) {
|
|
sel->onDblClick(sel);
|
|
} else if (sel->onClick) {
|
|
sel->onClick(sel);
|
|
}
|
|
}
|
|
}
|
|
} else if (key == ' ' && multi) {
|
|
// Space -- toggle selection of current item in multi-select
|
|
if (sel) {
|
|
TreeItemDataT *selTi = (TreeItemDataT *)sel->data;
|
|
selTi->selected = !selTi->selected;
|
|
tv->anchorItem = sel;
|
|
}
|
|
} else if (key >= 0x20 && key < 0x7F) {
|
|
// Type-ahead: search visible items for next match
|
|
char upper = (char)key;
|
|
|
|
if (upper >= 'a' && upper <= 'z') {
|
|
upper -= 32;
|
|
}
|
|
|
|
WidgetT *start = sel ? sel : firstVisibleItem(w);
|
|
|
|
if (start) {
|
|
WidgetT *cur = nextVisibleItem(start, w);
|
|
|
|
if (!cur) {
|
|
cur = firstVisibleItem(w);
|
|
}
|
|
|
|
while (cur && cur != start) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)cur->data;
|
|
|
|
if (ti->text && ti->text[0]) {
|
|
char first = ti->text[0];
|
|
|
|
if (first >= 'a' && first <= 'z') {
|
|
first -= 32;
|
|
}
|
|
|
|
if (first == upper) {
|
|
setSelectedItem(w, cur);
|
|
break;
|
|
}
|
|
}
|
|
|
|
cur = nextVisibleItem(cur, w);
|
|
|
|
if (!cur) {
|
|
cur = firstVisibleItem(w);
|
|
}
|
|
}
|
|
|
|
// Check the start item itself if we wrapped all the way around
|
|
if (cur == start && sel) {
|
|
TreeItemDataT *ti = (TreeItemDataT *)start->data;
|
|
|
|
if (ti->text && ti->text[0]) {
|
|
char first = ti->text[0];
|
|
|
|
if (first >= 'a' && first <= 'z') {
|
|
first -= 32;
|
|
}
|
|
|
|
if (first == upper && cur != sel) {
|
|
setSelectedItem(w, cur);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// Update multi-select state after Up/Down navigation
|
|
WidgetT *newSel = tv->selectedItem;
|
|
|
|
if (multi && newSel != sel && newSel) {
|
|
if (shift && tv->anchorItem) {
|
|
// Shift+arrow: range from anchor to new cursor
|
|
clearAllSelections(w);
|
|
selectRange(w, tv->anchorItem, newSel);
|
|
}
|
|
// Plain arrow: just move cursor, leave selections untouched
|
|
}
|
|
|
|
// Set anchor on first selection if not set
|
|
if (multi && !tv->anchorItem && newSel) {
|
|
tv->anchorItem = newSel;
|
|
}
|
|
|
|
// Scroll to keep selected item visible
|
|
sel = tv->selectedItem;
|
|
|
|
if (sel) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
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 < tv->scrollPos) {
|
|
tv->scrollPos = itemY;
|
|
} else if (itemY + font->charHeight > tv->scrollPos + innerH) {
|
|
tv->scrollPos = itemY + font->charHeight - innerH;
|
|
}
|
|
}
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// Mouse handling priority: V scrollbar > H scrollbar > dead corner >
|
|
// tree item. Item clicks are resolved by treeItemAtY which walks the
|
|
// visible tree converting Y coordinates to items. The item's depth
|
|
// is computed by walking up the parent chain to determine the expand
|
|
// icon's X position and check if the click landed on it.
|
|
//
|
|
// Multi-select mouse behavior:
|
|
// - Plain click: select only this item (clear others), set anchor
|
|
// - Ctrl+click: toggle item, update anchor
|
|
// - Shift+click: range-select from anchor to clicked item
|
|
//
|
|
// Collapsing a node re-checks scroll bounds because content height
|
|
// may have decreased, and the current scroll position may now be
|
|
// past the new maximum.
|
|
//
|
|
// Drag-reorder initiation: plain click (no modifier, not on expand
|
|
// icon) sets dragItem and sDragReorder. The actual reorder drag
|
|
// tracking happens in widgetEvent.c during mouse-move.
|
|
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)hit->data;
|
|
sFocusedWidget = hit;
|
|
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;
|
|
}
|
|
|
|
tv->scrollPos = clampInt(tv->scrollPos, 0, maxScrollV);
|
|
tv->scrollPosH = clampInt(tv->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, tv->scrollPos);
|
|
|
|
if (sh == ScrollHitArrowDecE) {
|
|
tv->scrollPos -= font->charHeight;
|
|
} else if (sh == ScrollHitArrowIncE) {
|
|
tv->scrollPos += font->charHeight;
|
|
} else if (sh == ScrollHitPageDecE) {
|
|
tv->scrollPos -= pageSize;
|
|
} else if (sh == ScrollHitPageIncE) {
|
|
tv->scrollPos += pageSize;
|
|
} else if (sh == ScrollHitThumbE) {
|
|
int32_t trackLen = innerH - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, totalH, innerH, tv->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
sDragWidget = hit;
|
|
tv->sbDragOrient = 0;
|
|
tv->sbDragOff = relY - WGT_SB_W - thumbPos;
|
|
}
|
|
|
|
tv->scrollPos = clampInt(tv->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, tv->scrollPosH);
|
|
|
|
if (sh == ScrollHitArrowDecE) {
|
|
tv->scrollPosH -= font->charWidth;
|
|
} else if (sh == ScrollHitArrowIncE) {
|
|
tv->scrollPosH += font->charWidth;
|
|
} else if (sh == ScrollHitPageDecE) {
|
|
tv->scrollPosH -= pageSize;
|
|
} else if (sh == ScrollHitPageIncE) {
|
|
tv->scrollPosH += pageSize;
|
|
} else if (sh == ScrollHitThumbE) {
|
|
int32_t trackLen = innerW - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, totalW, innerW, tv->scrollPosH, &thumbPos, &thumbSize);
|
|
|
|
sDragWidget = hit;
|
|
tv->sbDragOrient = 1;
|
|
tv->sbDragOff = relX - WGT_SB_W - thumbPos;
|
|
}
|
|
|
|
tv->scrollPosH = clampInt(tv->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 - tv->scrollPos;
|
|
|
|
WidgetT *item = treeItemAtY(hit, vy, &curY, font);
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
// Update selection
|
|
bool multi = tv->multiSelect;
|
|
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
|
|
|
|
setSelectedItem(hit, item);
|
|
TreeItemDataT *itemTi = (TreeItemDataT *)item->data;
|
|
|
|
if (multi) {
|
|
if (ctrl) {
|
|
// Ctrl+click: toggle item, update anchor
|
|
itemTi->selected = !itemTi->selected;
|
|
tv->anchorItem = item;
|
|
} else if (shift && tv->anchorItem) {
|
|
// Shift+click: range from anchor to clicked
|
|
clearAllSelections(hit);
|
|
selectRange(hit, tv->anchorItem, item);
|
|
} else {
|
|
// Plain click: select only this item, update anchor
|
|
clearAllSelections(hit);
|
|
itemTi->selected = true;
|
|
tv->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 == sTreeItemTypeId) {
|
|
hasChildren = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasChildren) {
|
|
// Calculate indent depth
|
|
int32_t depth = 0;
|
|
WidgetT *p = item->parent;
|
|
|
|
while (p && p->type == sTreeItemTypeId) {
|
|
depth++;
|
|
p = p->parent;
|
|
}
|
|
|
|
int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT - tv->scrollPosH;
|
|
|
|
if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) {
|
|
clickedExpandIcon = true;
|
|
itemTi->expanded = !itemTi->expanded;
|
|
tv->dimsValid = false;
|
|
|
|
// Clamp scroll positions if collapsing reduced content size
|
|
if (!itemTi->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;
|
|
}
|
|
|
|
tv->scrollPos = clampInt(tv->scrollPos, 0, newMaxScrlV);
|
|
tv->scrollPosH = clampInt(tv->scrollPosH, 0, newMaxScrlH);
|
|
}
|
|
|
|
if (item->onChange) {
|
|
item->onChange(item);
|
|
}
|
|
} else {
|
|
int32_t clicks = multiClickDetect(vx, vy);
|
|
|
|
if (clicks >= 2 && item->onDblClick) {
|
|
item->onDblClick(item);
|
|
} else if (item->onClick) {
|
|
item->onClick(item);
|
|
}
|
|
}
|
|
} else {
|
|
int32_t clicks = multiClickDetect(vx, vy);
|
|
|
|
if (clicks >= 2 && item->onDblClick) {
|
|
item->onDblClick(item);
|
|
} else if (item->onClick) {
|
|
item->onClick(item);
|
|
}
|
|
}
|
|
|
|
// Initiate drag-reorder if enabled (not from expand icon or modifier clicks)
|
|
if (tv->reorderable && !clickedExpandIcon && !shift && !ctrl) {
|
|
tv->dragItem = item;
|
|
tv->dropTarget = NULL;
|
|
sDragWidget = hit;
|
|
}
|
|
}
|
|
|
|
|
|
// Paint: sunken border, then clipped content area for items +
|
|
// reorder indicator, then scrollbars outside the clip rect. The clip
|
|
// rect excludes the scrollbar area so items don't paint over scrollbars.
|
|
// Both scroll axes are applied to baseX/itemY so the tree content
|
|
// shifts correctly in both dimensions.
|
|
void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
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;
|
|
}
|
|
|
|
tv->scrollPos = clampInt(tv->scrollPos, 0, maxScrollV);
|
|
tv->scrollPosH = clampInt(tv->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 - tv->scrollPos;
|
|
int32_t baseX = w->x + TREE_BORDER - tv->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, tv->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, tv->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 == sFocusedWidget) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
static void widgetTreeViewReorderDrop(WidgetT *w) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
WidgetT *dragItem = tv->dragItem;
|
|
WidgetT *dropTarget = tv->dropTarget;
|
|
bool dropAfter = tv->dropAfter;
|
|
|
|
tv->dragItem = NULL;
|
|
tv->dropTarget = NULL;
|
|
|
|
if (!dragItem || !dropTarget || dragItem == dropTarget) {
|
|
return;
|
|
}
|
|
|
|
// Remove dragged item from its current parent
|
|
WidgetT *oldParent = dragItem->parent;
|
|
bool dropInto = tv->dropInto;
|
|
tv->dropInto = false;
|
|
|
|
if (oldParent) {
|
|
widgetRemoveChild(oldParent, dragItem);
|
|
}
|
|
|
|
if (dropInto) {
|
|
// Reparent: make dragItem the last child of dropTarget
|
|
dragItem->nextSibling = NULL;
|
|
dragItem->parent = dropTarget;
|
|
|
|
if (dropTarget->lastChild) {
|
|
dropTarget->lastChild->nextSibling = dragItem;
|
|
} else {
|
|
dropTarget->firstChild = dragItem;
|
|
}
|
|
|
|
dropTarget->lastChild = dragItem;
|
|
} else {
|
|
// Insert at the drop position (sibling reorder)
|
|
WidgetT *newParent = dropTarget->parent;
|
|
|
|
if (!newParent) {
|
|
return;
|
|
}
|
|
|
|
dragItem->nextSibling = NULL;
|
|
dragItem->parent = newParent;
|
|
|
|
if (!dropAfter) {
|
|
// Insert before dropTarget
|
|
WidgetT *prev = NULL;
|
|
|
|
for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) {
|
|
if (c == dropTarget) {
|
|
dragItem->nextSibling = dropTarget;
|
|
|
|
if (prev) {
|
|
prev->nextSibling = dragItem;
|
|
} else {
|
|
newParent->firstChild = dragItem;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
prev = c;
|
|
}
|
|
} else {
|
|
// Insert after dropTarget
|
|
dragItem->nextSibling = dropTarget->nextSibling;
|
|
dropTarget->nextSibling = dragItem;
|
|
|
|
if (newParent->lastChild == dropTarget) {
|
|
newParent->lastChild = dragItem;
|
|
}
|
|
}
|
|
}
|
|
|
|
tv->dimsValid = false;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
|
|
static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
(void)root;
|
|
(void)x;
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
|
|
int32_t innerY = w->y + TREE_BORDER;
|
|
int32_t relY = y - innerY + tv->scrollPos;
|
|
|
|
// Find which item the mouse is over
|
|
int32_t curY = 0;
|
|
WidgetT *target = treeItemAtY(w, relY, &curY, font);
|
|
|
|
if (target) {
|
|
tv->dropTarget = target;
|
|
// Three-zone drop: top third = before, middle third = into, bottom third = after
|
|
int32_t itemY = treeItemYPos(w, target, font);
|
|
int32_t third = font->charHeight / 3;
|
|
int32_t offset = relY - itemY;
|
|
|
|
if (offset < third) {
|
|
tv->dropAfter = false;
|
|
tv->dropInto = false;
|
|
} else if (offset < third * 2) {
|
|
tv->dropAfter = false;
|
|
tv->dropInto = true;
|
|
} else {
|
|
tv->dropAfter = true;
|
|
tv->dropInto = false;
|
|
}
|
|
} else {
|
|
// Below all items -- drop after the last item
|
|
tv->dropTarget = w->lastChild;
|
|
tv->dropAfter = true;
|
|
}
|
|
|
|
// Auto-scroll when dragging near edges
|
|
int32_t innerH = w->h - TREE_BORDER * 2;
|
|
int32_t visRows = innerH / font->charHeight;
|
|
int32_t mouseRelY = y - innerY;
|
|
|
|
if (mouseRelY < font->charHeight && tv->scrollPos > 0) {
|
|
tv->scrollPos--;
|
|
} else if (mouseRelY >= (visRows - 1) * font->charHeight) {
|
|
tv->scrollPos++;
|
|
}
|
|
}
|
|
|
|
|
|
// Handle scrollbar thumb drag for vertical and horizontal scrollbars.
|
|
static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
|
|
TreeViewDataT *tv = (TreeViewDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
|
|
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 (orient == 0) {
|
|
// Vertical scrollbar drag
|
|
int32_t maxScroll = totalH - innerH;
|
|
|
|
if (maxScroll <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t trackLen = innerH - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, totalH, innerH, tv->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
int32_t sbY = w->y + TREE_BORDER;
|
|
int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;
|
|
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
|
|
tv->scrollPos = clampInt(newScroll, 0, maxScroll);
|
|
} else if (orient == 1) {
|
|
// Horizontal scrollbar drag
|
|
int32_t maxScroll = totalW - innerW;
|
|
|
|
if (maxScroll <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t trackLen = innerW - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, totalW, innerW, tv->scrollPosH, &thumbPos, &thumbSize);
|
|
|
|
int32_t sbX = w->x + TREE_BORDER;
|
|
int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff;
|
|
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
|
|
tv->scrollPosH = clampInt(newScroll, 0, maxScroll);
|
|
}
|
|
}
|
|
|
|
|
|
static const WidgetClassT sClassTreeView = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetTreeViewPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetTreeViewCalcMinSize,
|
|
[WGT_METHOD_LAYOUT] = (void *)widgetTreeViewLayout,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetTreeViewOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetTreeViewOnKey,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetTreeViewDestroy,
|
|
[WGT_METHOD_ON_CHILD_CHANGED] = (void *)widgetTreeViewOnChildChanged,
|
|
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetTreeViewOnDragUpdate,
|
|
[WGT_METHOD_ON_DRAG_END] = (void *)widgetTreeViewOnDragEnd,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassTreeItem = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = 0,
|
|
.handlers = {
|
|
[WGT_METHOD_DESTROY] = (void *)widgetTreeItemDestroy,
|
|
[WGT_METHOD_GET_TEXT] = (void *)widgetTreeItemGetText,
|
|
[WGT_METHOD_SET_TEXT] = (void *)widgetTreeItemSetText,
|
|
}
|
|
};
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent);
|
|
WidgetT *(*getSelected)(const WidgetT *w);
|
|
void (*setSelected)(WidgetT *w, WidgetT *item);
|
|
void (*setMultiSelect)(WidgetT *w, bool multi);
|
|
void (*setReorderable)(WidgetT *w, bool reorderable);
|
|
WidgetT *(*item)(WidgetT *parent, const char *text);
|
|
void (*itemSetExpanded)(WidgetT *w, bool expanded);
|
|
bool (*itemIsExpanded)(const WidgetT *w);
|
|
bool (*itemIsSelected)(const WidgetT *w);
|
|
void (*itemSetSelected)(WidgetT *w, bool selected);
|
|
} sApi = {
|
|
.create = wgtTreeView,
|
|
.getSelected = wgtTreeViewGetSelected,
|
|
.setSelected = wgtTreeViewSetSelected,
|
|
.setMultiSelect = wgtTreeViewSetMultiSelect,
|
|
.setReorderable = wgtTreeViewSetReorderable,
|
|
.item = wgtTreeItem,
|
|
.itemSetExpanded = wgtTreeItemSetExpanded,
|
|
.itemIsExpanded = wgtTreeItemIsExpanded,
|
|
.itemIsSelected = wgtTreeItemIsSelected,
|
|
.itemSetSelected = wgtTreeItemSetSelected
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "AddChildItem", WGT_SIG_INT_STR, (void *)basAddChildItem },
|
|
{ "AddItem", WGT_SIG_STR, (void *)basAddItem },
|
|
{ "Clear", WGT_SIG_VOID, (void *)basClear },
|
|
{ "GetItemText", WGT_SIG_RET_STR_INT, (void *)basGetItemText },
|
|
{ "IsExpanded", WGT_SIG_RET_BOOL_INT, (void *)basIsExpanded },
|
|
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)basIsItemSelected },
|
|
{ "ItemCount", WGT_SIG_RET_INT, (void *)basItemCount },
|
|
{ "SetExpanded", WGT_SIG_INT_BOOL, (void *)basSetExpanded },
|
|
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)basSetItemSelected },
|
|
{ "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtTreeViewSetMultiSelect },
|
|
{ "SetReorderable", WGT_SIG_BOOL, (void *)wgtTreeViewSetReorderable },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "TreeView",
|
|
.props = NULL,
|
|
.propCount = 0,
|
|
.methods = sMethods,
|
|
.methodCount = 11,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTreeViewTypeId = wgtRegisterClass(&sClassTreeView);
|
|
sTreeItemTypeId = wgtRegisterClass(&sClassTreeItem);
|
|
wgtRegisterApi("treeview", &sApi);
|
|
wgtRegisterIface("treeview", &sIface);
|
|
}
|