// 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; } }