DVX_GUI/dvx/widgets/widgetCore.c

567 lines
14 KiB
C

// widgetCore.c — Core widget infrastructure (alloc, tree ops, helpers)
#include "widgetInternal.h"
// ============================================================
// Global state for drag and popup tracking
// ============================================================
bool sDebugLayout = false;
WidgetT *sFocusedWidget = NULL;
WidgetT *sOpenPopup = NULL;
WidgetT *sPressedButton = NULL;
WidgetT *sDragSlider = NULL;
WidgetT *sDrawingCanvas = NULL;
WidgetT *sDragTextSelect = NULL;
int32_t sDragOffset = 0;
WidgetT *sResizeListView = NULL;
int32_t sResizeCol = -1;
int32_t sResizeStartX = 0;
int32_t sResizeOrigW = 0;
WidgetT *sDragSplitter = NULL;
int32_t sDragSplitStart = 0;
WidgetT *sDragReorder = NULL;
// ============================================================
// widgetAddChild
// ============================================================
void widgetAddChild(WidgetT *parent, WidgetT *child) {
child->parent = parent;
child->nextSibling = NULL;
if (parent->lastChild) {
parent->lastChild->nextSibling = child;
parent->lastChild = child;
} else {
parent->firstChild = child;
parent->lastChild = child;
}
}
// ============================================================
// widgetAlloc
// ============================================================
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
if (!w) {
return NULL;
}
memset(w, 0, sizeof(*w));
w->type = type;
w->wclass = widgetClassTable[type];
w->visible = true;
w->enabled = true;
if (parent) {
w->window = parent->window;
widgetAddChild(parent, w);
}
return w;
}
// ============================================================
// widgetClearFocus
// ============================================================
void widgetClearFocus(WidgetT *root) {
if (!root) {
return;
}
root->focused = false;
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
widgetClearFocus(c);
}
}
// ============================================================
// widgetCountVisibleChildren
// ============================================================
int32_t widgetCountVisibleChildren(const WidgetT *w) {
int32_t count = 0;
for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
count++;
}
}
return count;
}
// ============================================================
// widgetDestroyChildren
// ============================================================
void widgetDestroyChildren(WidgetT *w) {
WidgetT *child = w->firstChild;
while (child) {
WidgetT *next = child->nextSibling;
widgetDestroyChildren(child);
if (child->wclass && child->wclass->destroy) {
child->wclass->destroy(child);
}
// Clear static references if they point to destroyed widgets
if (sFocusedWidget == child) {
sFocusedWidget = NULL;
}
if (sOpenPopup == child) {
sOpenPopup = NULL;
}
if (sPressedButton == child) {
sPressedButton = NULL;
}
if (sDragSlider == child) {
sDragSlider = NULL;
}
if (sDrawingCanvas == child) {
sDrawingCanvas = NULL;
}
if (sResizeListView == child) {
sResizeListView = NULL;
sResizeCol = -1;
}
free(child);
child = next;
}
w->firstChild = NULL;
w->lastChild = NULL;
}
// ============================================================
// widgetDropdownPopupRect
// ============================================================
//
// Calculate the rectangle for a dropdown/combobox popup list.
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
int32_t itemCount = 0;
if (w->type == WidgetDropdownE) {
itemCount = w->as.dropdown.itemCount;
} else if (w->type == WidgetComboBoxE) {
itemCount = w->as.comboBox.itemCount;
}
int32_t visibleItems = itemCount;
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
visibleItems = DROPDOWN_MAX_VISIBLE;
}
if (visibleItems < 1) {
visibleItems = 1;
}
*popX = w->x;
*popW = w->w;
*popH = visibleItems * font->charHeight + 4; // 2px border each side
// Try below first, then above if no room
if (w->y + w->h + *popH <= contentH) {
*popY = w->y + w->h;
} else {
*popY = w->y - *popH;
if (*popY < 0) {
*popY = 0;
}
}
}
// ============================================================
// widgetFindByAccel
// ============================================================
WidgetT *widgetFindByAccel(WidgetT *root, char key) {
if (!root || !root->enabled) {
return NULL;
}
// Invisible tab pages: match the page itself (for tab switching)
// but don't recurse into children (their accels shouldn't be active)
if (!root->visible) {
if (root->type == WidgetTabPageE && root->accelKey == key) {
return root;
}
return NULL;
}
if (root->accelKey == key) {
return root;
}
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
WidgetT *found = widgetFindByAccel(c, key);
if (found) {
return found;
}
}
return NULL;
}
// ============================================================
// widgetFindNextFocusable
// ============================================================
//
// Depth-first walk of the widget tree. Returns the first focusable
// widget found after 'after'. If 'after' is NULL, returns the first
// focusable widget. Wraps around to the beginning if needed.
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
if (!w->visible || !w->enabled) {
return NULL;
}
if (after == NULL) {
*pastAfter = true;
}
if (w == after) {
*pastAfter = true;
} else if (*pastAfter && widgetIsFocusable(w->type)) {
return w;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
WidgetT *found = findNextFocusableImpl(c, after, pastAfter);
if (found) {
return found;
}
}
return NULL;
}
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
bool pastAfter = false;
WidgetT *found = findNextFocusableImpl(root, after, &pastAfter);
if (found) {
return found;
}
// Wrap around — search from the beginning
pastAfter = true;
return findNextFocusableImpl(root, NULL, &pastAfter);
}
// ============================================================
// widgetFindPrevFocusable
// ============================================================
//
// Depth-first walk collecting all visible+enabled focusable widgets,
// then returns the one before 'before'. Wraps around if needed.
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
WidgetT *list[128];
int32_t count = 0;
// Collect all focusable widgets via depth-first traversal
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
while (top > 0) {
WidgetT *w = stack[--top];
if (!w->visible || !w->enabled) {
continue;
}
if (widgetIsFocusable(w->type) && count < 128) {
list[count++] = w;
}
// Push children in reverse order so first child is processed first
WidgetT *children[64];
int32_t childCount = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (childCount < 64) {
children[childCount++] = c;
}
}
for (int32_t i = childCount - 1; i >= 0; i--) {
if (top < 64) {
stack[top++] = children[i];
}
}
}
if (count == 0) {
return NULL;
}
// Find 'before' in the list
int32_t idx = -1;
for (int32_t i = 0; i < count; i++) {
if (list[i] == before) {
idx = i;
break;
}
}
if (idx <= 0) {
return list[count - 1]; // Wrap to last
}
return list[idx - 1];
}
// ============================================================
// widgetFrameBorderWidth
// ============================================================
int32_t widgetFrameBorderWidth(const WidgetT *w) {
if (w->type != WidgetFrameE) {
return 0;
}
if (w->as.frame.style == FrameFlatE) {
return FRAME_FLAT_BORDER;
}
return FRAME_BEVEL_BORDER;
}
// ============================================================
// widgetHitTest
// ============================================================
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
if (!w->visible) {
return NULL;
}
if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) {
return NULL;
}
// Widgets with WCLASS_NO_HIT_RECURSE manage their own children
if (w->wclass && (w->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
return w;
}
// Check children — take the last match (topmost in Z-order)
WidgetT *hit = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
WidgetT *childHit = widgetHitTest(c, x, y);
if (childHit) {
hit = childHit;
}
}
return hit ? hit : w;
}
// ============================================================
// widgetIsFocusable
// ============================================================
bool widgetIsFocusable(WidgetTypeE type) {
return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
}
// ============================================================
// widgetIsBoxContainer
// ============================================================
//
// Returns true for widget types that use the generic box layout.
bool widgetIsBoxContainer(WidgetTypeE type) {
return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0;
}
// ============================================================
// widgetIsHorizContainer
// ============================================================
//
// Returns true for container types that lay out children horizontally.
bool widgetIsHorizContainer(WidgetTypeE type) {
return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0;
}
// ============================================================
// widgetNavigateIndex
// ============================================================
//
// Shared Up/Down/Home/End/PgUp/PgDn handler for list-like widgets.
// Returns the new index, or -1 if the key is not a navigation key.
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
if (key == (0x50 | 0x100)) {
// Down arrow
if (current < count - 1) {
return current + 1;
}
return current < 0 ? 0 : current;
}
if (key == (0x48 | 0x100)) {
// Up arrow
if (current > 0) {
return current - 1;
}
return current < 0 ? 0 : current;
}
if (key == (0x47 | 0x100)) {
// Home
return 0;
}
if (key == (0x4F | 0x100)) {
// End
return count - 1;
}
if (key == (0x51 | 0x100)) {
// Page Down
int32_t n = current + pageSize;
return n >= count ? count - 1 : n;
}
if (key == (0x49 | 0x100)) {
// Page Up
int32_t n = current - pageSize;
return n < 0 ? 0 : n;
}
return -1;
}
// ============================================================
// widgetPaintPopupList
// ============================================================
//
// Shared popup list painting for Dropdown and ComboBox.
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
// Draw popup border
BevelStyleT bevel;
bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow;
bevel.face = colors->contentBg;
bevel.width = 2;
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
// Draw items
int32_t visibleItems = popH / font->charHeight;
int32_t textX = popX + TEXT_INPUT_PAD;
int32_t textY = popY + 2;
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
int32_t idx = scrollPos + i;
int32_t iy = textY + i * font->charHeight;
uint32_t ifg = colors->contentFg;
uint32_t ibg = colors->contentBg;
if (idx == hoverIdx) {
ifg = colors->menuHighlightFg;
ibg = colors->menuHighlightBg;
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
}
}
// ============================================================
// widgetScrollbarThumb
// ============================================================
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
*thumbSize = (trackLen * visibleSize) / totalSize;
if (*thumbSize < SB_MIN_THUMB) {
*thumbSize = SB_MIN_THUMB;
}
if (*thumbSize > trackLen) {
*thumbSize = trackLen;
}
int32_t maxScroll = totalSize - visibleSize;
if (maxScroll > 0) {
*thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll;
} else {
*thumbPos = 0;
}
}
// ============================================================
// widgetRemoveChild
// ============================================================
void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
WidgetT *prev = NULL;
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c == child) {
if (prev) {
prev->nextSibling = c->nextSibling;
} else {
parent->firstChild = c->nextSibling;
}
if (parent->lastChild == child) {
parent->lastChild = prev;
}
child->nextSibling = NULL;
child->parent = NULL;
return;
}
prev = c;
}
}