478 lines
12 KiB
C
478 lines
12 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 *sOpenPopup = NULL;
|
|
WidgetT *sPressedButton = NULL;
|
|
WidgetT *sDragSlider = NULL;
|
|
WidgetT *sDrawingCanvas = NULL;
|
|
int32_t sDragOffset = 0;
|
|
|
|
|
|
// ============================================================
|
|
// 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->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->type == WidgetTextInputE) {
|
|
free(child->as.textInput.buf);
|
|
} else if (child->type == WidgetTextAreaE) {
|
|
free(child->as.textArea.buf);
|
|
} else if (child->type == WidgetComboBoxE) {
|
|
free(child->as.comboBox.buf);
|
|
} else if (child->type == WidgetImageE) {
|
|
free(child->as.image.data);
|
|
} else if (child->type == WidgetCanvasE) {
|
|
free(child->as.canvas.data);
|
|
} else if (child->type == WidgetImageButtonE) {
|
|
free(child->as.imageButton.data);
|
|
} else if (child->type == WidgetAnsiTermE) {
|
|
free(child->as.ansiTerm.cells);
|
|
free(child->as.ansiTerm.scrollback);
|
|
}
|
|
|
|
// Clear popup/drag references if they point to destroyed widgets
|
|
if (sOpenPopup == child) {
|
|
sOpenPopup = NULL;
|
|
}
|
|
|
|
if (sPressedButton == child) {
|
|
sPressedButton = NULL;
|
|
}
|
|
|
|
if (sDragSlider == child) {
|
|
sDragSlider = NULL;
|
|
}
|
|
|
|
if (sDrawingCanvas == child) {
|
|
sDrawingCanvas = NULL;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// TreeView manages its own children — don't recurse
|
|
if (w->type == WidgetTreeViewE) {
|
|
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 type == WidgetTextInputE || type == WidgetComboBoxE ||
|
|
type == WidgetDropdownE || type == WidgetCheckboxE ||
|
|
type == WidgetRadioE || type == WidgetButtonE ||
|
|
type == WidgetImageButtonE || type == WidgetSliderE ||
|
|
type == WidgetListBoxE || type == WidgetTreeViewE ||
|
|
type == WidgetAnsiTermE || type == WidgetTabControlE;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetIsBoxContainer
|
|
// ============================================================
|
|
//
|
|
// Returns true for widget types that use the generic box layout.
|
|
|
|
bool widgetIsBoxContainer(WidgetTypeE type) {
|
|
return type == WidgetVBoxE || type == WidgetHBoxE || type == WidgetFrameE ||
|
|
type == WidgetRadioGroupE || type == WidgetTabPageE ||
|
|
type == WidgetStatusBarE || type == WidgetToolbarE;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetIsHorizContainer
|
|
// ============================================================
|
|
//
|
|
// Returns true for container types that lay out children horizontally.
|
|
|
|
bool widgetIsHorizContainer(WidgetTypeE type) {
|
|
return type == WidgetHBoxE || type == WidgetStatusBarE || type == WidgetToolbarE;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
}
|