// dvxWidget.c — Widget system for DV/X GUI #include "dvxWidget.h" #include "dvxApp.h" #include "dvxDraw.h" #include "dvxWm.h" #include "dvxVideo.h" #include #include #include // ============================================================ // Constants // ============================================================ #define DEFAULT_SPACING 4 #define DEFAULT_PADDING 4 #define SEPARATOR_THICKNESS 2 #define BUTTON_PAD_H 8 #define BUTTON_PAD_V 4 #define CHECKBOX_BOX_SIZE 12 #define CHECKBOX_GAP 4 #define FRAME_BORDER 2 #define FRAME_TITLE_GAP 4 #define TEXT_INPUT_PAD 3 // ============================================================ // Prototypes // ============================================================ static void addChild(WidgetT *parent, WidgetT *child); static WidgetT *allocWidget(WidgetT *parent, WidgetTypeE type); static void calcMinSizeBox(WidgetT *w, const BitmapFontT *font); static void calcMinSizeLeaf(WidgetT *w, const BitmapFontT *font); static void calcMinSizeTree(WidgetT *w, const BitmapFontT *font); static int32_t countVisibleChildren(const WidgetT *w); static void destroyChildren(WidgetT *w); static WidgetT *hitTest(WidgetT *w, int32_t x, int32_t y); static void layoutBox(WidgetT *w, const BitmapFontT *font); static void layoutChildren(WidgetT *w, const BitmapFontT *font); static void paintWidget(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); static void removeChild(WidgetT *parent, WidgetT *child); static void widgetManageScrollbars(WindowT *win, AppContextT *ctx); static void widgetOnKey(WindowT *win, int32_t key, int32_t mod); static void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); static void widgetOnPaint(WindowT *win, RectT *dirtyArea); static void widgetOnResize(WindowT *win, int32_t newW, int32_t newH); static void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value); // ============================================================ // addChild // ============================================================ static void addChild(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; } } // ============================================================ // allocWidget // ============================================================ static WidgetT *allocWidget(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; addChild(parent, w); } return w; } // ============================================================ // calcMinSizeBox // ============================================================ static void calcMinSizeBox(WidgetT *w, const BitmapFontT *font) { bool horiz = (w->type == WidgetHBoxE); // RadioGroupE and VBoxE are vertical int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth); int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); int32_t mainSize = 0; int32_t crossSize = 0; int32_t count = 0; if (pad == 0) { pad = DEFAULT_PADDING; } if (gap == 0) { gap = DEFAULT_SPACING; } // Frame adds title height and border int32_t frameExtraTop = 0; if (w->type == WidgetFrameE) { frameExtraTop = font->charHeight + FRAME_TITLE_GAP; pad = DEFAULT_PADDING; } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (!c->visible) { continue; } calcMinSizeTree(c, font); if (horiz) { mainSize += c->calcMinW; crossSize = DVX_MAX(crossSize, c->calcMinH); } else { mainSize += c->calcMinH; crossSize = DVX_MAX(crossSize, c->calcMinW); } count++; } // Add spacing between children if (count > 1) { mainSize += gap * (count - 1); } // Add padding mainSize += pad * 2; crossSize += pad * 2; if (horiz) { w->calcMinW = mainSize; w->calcMinH = crossSize + frameExtraTop; } else { w->calcMinW = crossSize; w->calcMinH = mainSize + frameExtraTop; } // Frame border if (w->type == WidgetFrameE) { w->calcMinW += FRAME_BORDER * 2; w->calcMinH += FRAME_BORDER * 2; } } // ============================================================ // calcMinSizeLeaf // ============================================================ static void calcMinSizeLeaf(WidgetT *w, const BitmapFontT *font) { switch (w->type) { case WidgetLabelE: w->calcMinW = (int32_t)strlen(w->as.label.text) * font->charWidth; w->calcMinH = font->charHeight; break; case WidgetButtonE: w->calcMinW = (int32_t)strlen(w->as.button.text) * font->charWidth + BUTTON_PAD_H * 2; w->calcMinH = font->charHeight + BUTTON_PAD_V * 2; break; case WidgetCheckboxE: w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + (int32_t)strlen(w->as.checkbox.text) * font->charWidth; w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); break; case WidgetRadioE: w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + (int32_t)strlen(w->as.radio.text) * font->charWidth; w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); break; case WidgetTextInputE: w->calcMinW = font->charWidth * 8 + TEXT_INPUT_PAD * 2; w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2; break; case WidgetSpacerE: w->calcMinW = 0; w->calcMinH = 0; break; case WidgetSeparatorE: if (w->as.separator.vertical) { w->calcMinW = SEPARATOR_THICKNESS; w->calcMinH = 0; } else { w->calcMinW = 0; w->calcMinH = SEPARATOR_THICKNESS; } break; default: w->calcMinW = 0; w->calcMinH = 0; break; } } // ============================================================ // calcMinSizeTree // ============================================================ static void calcMinSizeTree(WidgetT *w, const BitmapFontT *font) { if (w->type == WidgetVBoxE || w->type == WidgetHBoxE || w->type == WidgetFrameE || w->type == WidgetRadioGroupE) { calcMinSizeBox(w, font); } else { calcMinSizeLeaf(w, font); } // Apply size hints (override calculated minimum) if (w->minW) { int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth); if (hintW > w->calcMinW) { w->calcMinW = hintW; } } if (w->minH) { int32_t hintH = wgtResolveSize(w->minH, 0, font->charWidth); if (hintH > w->calcMinH) { w->calcMinH = hintH; } } } // ============================================================ // countVisibleChildren // ============================================================ static int32_t countVisibleChildren(const WidgetT *w) { int32_t count = 0; for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->visible) { count++; } } return count; } // ============================================================ // destroyChildren // ============================================================ static void destroyChildren(WidgetT *w) { WidgetT *child = w->firstChild; while (child) { WidgetT *next = child->nextSibling; destroyChildren(child); if (child->type == WidgetTextInputE) { free(child->as.textInput.buf); } else if (child->type == WidgetTextAreaE) { free(child->as.textArea.buf); } free(child); child = next; } w->firstChild = NULL; w->lastChild = NULL; } // ============================================================ // hitTest // ============================================================ static WidgetT *hitTest(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; } // Check children in reverse order (last child is on top) // Walk to last visible child, then check backwards // Since we use a singly-linked list, just check all and take the last match WidgetT *hit = NULL; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { WidgetT *childHit = hitTest(c, x, y); if (childHit) { hit = childHit; } } return hit ? hit : w; } // ============================================================ // layoutBox // ============================================================ static void layoutBox(WidgetT *w, const BitmapFontT *font) { bool horiz = (w->type == WidgetHBoxE); int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth); int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); if (pad == 0) { pad = DEFAULT_PADDING; } if (gap == 0) { gap = DEFAULT_SPACING; } // Frame adjustments int32_t frameExtraTop = 0; int32_t frameBorder = 0; if (w->type == WidgetFrameE) { frameExtraTop = font->charHeight + FRAME_TITLE_GAP; frameBorder = FRAME_BORDER; pad = DEFAULT_PADDING; } int32_t innerX = w->x + pad + frameBorder; int32_t innerY = w->y + pad + frameBorder + frameExtraTop; int32_t innerW = w->w - pad * 2 - frameBorder * 2; int32_t innerH = w->h - pad * 2 - frameBorder * 2 - frameExtraTop; if (innerW < 0) { innerW = 0; } if (innerH < 0) { innerH = 0; } int32_t count = countVisibleChildren(w); if (count == 0) { return; } int32_t totalGap = gap * (count - 1); int32_t availMain = horiz ? (innerW - totalGap) : (innerH - totalGap); int32_t availCross = horiz ? innerH : innerW; // First pass: sum minimum sizes and total weight int32_t totalMin = 0; int32_t totalWeight = 0; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (!c->visible) { continue; } int32_t cmin = horiz ? c->calcMinW : c->calcMinH; totalMin += cmin; totalWeight += c->weight; } int32_t extraSpace = availMain - totalMin; if (extraSpace < 0) { extraSpace = 0; } // Compute alignment offset for main axis int32_t alignOffset = 0; if (totalWeight == 0 && extraSpace > 0) { if (w->align == AlignCenterE) { alignOffset = extraSpace / 2; } else if (w->align == AlignEndE) { alignOffset = extraSpace; } } // Second pass: assign positions and sizes int32_t pos = (horiz ? innerX : innerY) + alignOffset; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (!c->visible) { continue; } int32_t cmin = horiz ? c->calcMinW : c->calcMinH; int32_t mainSize = cmin; // Distribute extra space by weight if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) { mainSize += (extraSpace * c->weight) / totalWeight; } // Apply max size constraint if (horiz && c->maxW) { int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth); if (mainSize > maxPx) { mainSize = maxPx; } } else if (!horiz && c->maxH) { int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth); if (mainSize > maxPx) { mainSize = maxPx; } } // Assign geometry if (horiz) { c->x = pos; c->y = innerY; c->w = mainSize; c->h = availCross; } else { c->x = innerX; c->y = pos; c->w = availCross; c->h = mainSize; } // Apply preferred/max on cross axis if (horiz && c->maxH) { int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth); if (c->h > maxPx) { c->h = maxPx; } } else if (!horiz && c->maxW) { int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth); if (c->w > maxPx) { c->w = maxPx; } } pos += mainSize + gap; // Recurse into child containers layoutChildren(c, font); } } // ============================================================ // layoutChildren // ============================================================ static void layoutChildren(WidgetT *w, const BitmapFontT *font) { if (w->type == WidgetVBoxE || w->type == WidgetHBoxE || w->type == WidgetFrameE || w->type == WidgetRadioGroupE) { layoutBox(w, font); } } // ============================================================ // paintWidget // ============================================================ static void paintWidget(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { if (!w->visible) { return; } switch (w->type) { case WidgetVBoxE: case WidgetHBoxE: // Containers are transparent — just paint children break; case WidgetFrameE: { // Draw beveled border BevelStyleT bevel; bevel.highlight = colors->windowHighlight; bevel.shadow = colors->windowShadow; bevel.face = 0; bevel.width = FRAME_BORDER; drawBevel(d, ops, w->x, w->y + font->charHeight / 2, w->w, w->h - font->charHeight / 2, &bevel); // Draw title over the top border if (w->as.frame.title && w->as.frame.title[0]) { int32_t titleW = (int32_t)strlen(w->as.frame.title) * font->charWidth; int32_t titleX = w->x + DEFAULT_PADDING + FRAME_BORDER; rectFill(d, ops, titleX - 2, w->y, titleW + 4, font->charHeight, colors->windowFace); drawText(d, ops, font, titleX, w->y, w->as.frame.title, colors->contentFg, colors->windowFace, true); } break; } case WidgetLabelE: drawText(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2, w->as.label.text, colors->contentFg, colors->contentBg, false); break; case WidgetButtonE: { BevelStyleT bevel; bevel.highlight = w->as.button.pressed ? colors->windowShadow : colors->windowHighlight; bevel.shadow = w->as.button.pressed ? colors->windowHighlight : colors->windowShadow; bevel.face = colors->buttonFace; bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); int32_t textW = (int32_t)strlen(w->as.button.text) * font->charWidth; int32_t textX = w->x + (w->w - textW) / 2; int32_t textY = w->y + (w->h - font->charHeight) / 2; if (w->as.button.pressed) { textX++; textY++; } drawText(d, ops, font, textX, textY, w->as.button.text, w->enabled ? colors->contentFg : colors->windowShadow, colors->buttonFace, true); break; } case WidgetCheckboxE: { int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; // Draw checkbox box BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = colors->contentBg; bevel.width = 1; drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel); // Draw check mark if checked if (w->as.checkbox.checked) { int32_t cx = w->x + 3; int32_t cy = boxY + 3; int32_t cs = CHECKBOX_BOX_SIZE - 6; for (int32_t i = 0; i < cs; i++) { drawHLine(d, ops, cx + i, cy + i, 1, colors->contentFg); drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, colors->contentFg); } } // Draw label drawText(d, ops, font, w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, w->y + (w->h - font->charHeight) / 2, w->as.checkbox.text, colors->contentFg, colors->contentBg, false); break; } case WidgetRadioE: { int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; // Draw radio box (same as checkbox for now, could use circle later) BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = colors->contentBg; bevel.width = 1; drawBevel(d, ops, w->x, boxY, CHECKBOX_BOX_SIZE, CHECKBOX_BOX_SIZE, &bevel); // Draw filled dot if selected if (w->parent && w->parent->type == WidgetRadioGroupE && w->parent->as.radioGroup.selectedIdx == w->as.radio.index) { rectFill(d, ops, w->x + 3, boxY + 3, CHECKBOX_BOX_SIZE - 6, CHECKBOX_BOX_SIZE - 6, colors->contentFg); } drawText(d, ops, font, w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP, w->y + (w->h - font->charHeight) / 2, w->as.radio.text, colors->contentFg, colors->contentBg, false); break; } case WidgetTextInputE: { // Sunken border BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = colors->contentBg; bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Draw text if (w->as.textInput.buf) { int32_t textX = w->x + TEXT_INPUT_PAD; int32_t textY = w->y + (w->h - font->charHeight) / 2; int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; int32_t off = w->as.textInput.scrollOff; int32_t len = w->as.textInput.len - off; if (len > maxChars) { len = maxChars; } for (int32_t i = 0; i < len; i++) { drawChar(d, ops, font, textX + i * font->charWidth, textY, w->as.textInput.buf[off + i], colors->contentFg, colors->contentBg, true); } // Draw cursor if (w->focused) { int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth; if (cursorX >= w->x + TEXT_INPUT_PAD && cursorX < w->x + w->w - TEXT_INPUT_PAD) { drawVLine(d, ops, cursorX, textY, font->charHeight, colors->contentFg); } } } break; } case WidgetSpacerE: // Invisible — draws nothing break; case WidgetSeparatorE: if (w->as.separator.vertical) { int32_t cx = w->x + w->w / 2; drawVLine(d, ops, cx, w->y, w->h, colors->windowShadow); drawVLine(d, ops, cx + 1, w->y, w->h, colors->windowHighlight); } else { int32_t cy = w->y + w->h / 2; drawHLine(d, ops, w->x, cy, w->w, colors->windowShadow); drawHLine(d, ops, w->x, cy + 1, w->w, colors->windowHighlight); } break; default: break; } // Paint children for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { paintWidget(c, d, ops, font, colors); } } // ============================================================ // removeChild // ============================================================ static void removeChild(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; } } // ============================================================ // widgetManageScrollbars // ============================================================ // // Checks whether the widget tree's minimum size exceeds the // window content area. Adds or removes WM scrollbars as needed, // then relayouts the widget tree at the virtual content size. static void widgetManageScrollbars(WindowT *win, AppContextT *ctx) { WidgetT *root = win->widgetRoot; if (!root) { return; } // Measure the tree without any layout pass calcMinSizeTree(root, &ctx->font); // Save old scroll positions before removing scrollbars int32_t oldVValue = win->vScroll ? win->vScroll->value : 0; int32_t oldHValue = win->hScroll ? win->hScroll->value : 0; bool hadVScroll = (win->vScroll != NULL); bool hadHScroll = (win->hScroll != NULL); // Remove existing scrollbars to measure full available area if (hadVScroll) { free(win->vScroll); win->vScroll = NULL; } if (hadHScroll) { free(win->hScroll); win->hScroll = NULL; } wmUpdateContentRect(win); int32_t availW = win->contentW; int32_t availH = win->contentH; int32_t minW = root->calcMinW; int32_t minH = root->calcMinH; bool needV = (minH > availH); bool needH = (minW > availW); // Adding one scrollbar reduces space, which may require the other if (needV && !needH) { needH = (minW > availW - SCROLLBAR_WIDTH); } if (needH && !needV) { needV = (minH > availH - SCROLLBAR_WIDTH); } bool changed = (needV != hadVScroll) || (needH != hadHScroll); if (needV) { int32_t pageV = needH ? availH - SCROLLBAR_WIDTH : availH; int32_t maxV = minH - pageV; if (maxV < 0) { maxV = 0; } wmAddVScrollbar(win, 0, maxV, pageV); win->vScroll->value = DVX_MIN(oldVValue, maxV); } if (needH) { int32_t pageH = needV ? availW - SCROLLBAR_WIDTH : availW; int32_t maxH = minW - pageH; if (maxH < 0) { maxH = 0; } wmAddHScrollbar(win, 0, maxH, pageH); win->hScroll->value = DVX_MIN(oldHValue, maxH); } if (changed) { // wmAddVScrollbar/wmAddHScrollbar already call wmUpdateContentRect wmReallocContentBuf(win, &ctx->display); } // Install scroll handler win->onScroll = widgetOnScroll; // Layout at the virtual content size (the larger of content area and min size) int32_t layoutW = DVX_MAX(win->contentW, minW); int32_t layoutH = DVX_MAX(win->contentH, minH); wgtLayout(root, layoutW, layoutH, &ctx->font); } // ============================================================ // widgetOnKey // ============================================================ static void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { (void)mod; WidgetT *root = win->widgetRoot; if (!root) { return; } // Find the focused widget // For now, simple: find a focused text input and send keys to it // TODO: proper focus chain / tab navigation WidgetT *focus = NULL; // Simple linear scan for focused widget WidgetT *stack[64]; int32_t top = 0; stack[top++] = root; while (top > 0) { WidgetT *w = stack[--top]; if (w->focused && w->type == WidgetTextInputE) { focus = w; break; } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (top < 64) { stack[top++] = c; } } } if (!focus || focus->type != WidgetTextInputE) { return; } // Handle key input for text input widget if (key >= 32 && key < 127) { // Printable character if (focus->as.textInput.len < focus->as.textInput.bufSize - 1) { int32_t pos = focus->as.textInput.cursorPos; memmove(focus->as.textInput.buf + pos + 1, focus->as.textInput.buf + pos, focus->as.textInput.len - pos + 1); focus->as.textInput.buf[pos] = (char)key; focus->as.textInput.len++; focus->as.textInput.cursorPos++; if (focus->onChange) { focus->onChange(focus); } } } else if (key == 8) { // Backspace if (focus->as.textInput.cursorPos > 0) { int32_t pos = focus->as.textInput.cursorPos; memmove(focus->as.textInput.buf + pos - 1, focus->as.textInput.buf + pos, focus->as.textInput.len - pos + 1); focus->as.textInput.len--; focus->as.textInput.cursorPos--; if (focus->onChange) { focus->onChange(focus); } } } else if (key == (0x4B | 0x100)) { // Left arrow if (focus->as.textInput.cursorPos > 0) { focus->as.textInput.cursorPos--; } } else if (key == (0x4D | 0x100)) { // Right arrow if (focus->as.textInput.cursorPos < focus->as.textInput.len) { focus->as.textInput.cursorPos++; } } else if (key == (0x47 | 0x100)) { // Home focus->as.textInput.cursorPos = 0; } else if (key == (0x4F | 0x100)) { // End focus->as.textInput.cursorPos = focus->as.textInput.len; } else if (key == (0x53 | 0x100)) { // Delete if (focus->as.textInput.cursorPos < focus->as.textInput.len) { int32_t pos = focus->as.textInput.cursorPos; memmove(focus->as.textInput.buf + pos, focus->as.textInput.buf + pos + 1, focus->as.textInput.len - pos); focus->as.textInput.len--; if (focus->onChange) { focus->onChange(focus); } } } // Adjust scroll offset to keep cursor visible AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t visibleChars = (focus->w - TEXT_INPUT_PAD * 2) / font->charWidth; if (focus->as.textInput.cursorPos < focus->as.textInput.scrollOff) { focus->as.textInput.scrollOff = focus->as.textInput.cursorPos; } if (focus->as.textInput.cursorPos >= focus->as.textInput.scrollOff + visibleChars) { focus->as.textInput.scrollOff = focus->as.textInput.cursorPos - visibleChars + 1; } // Repaint the window wgtInvalidate(focus); } // ============================================================ // widgetOnMouse // ============================================================ static void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; if (!root || !(buttons & 1)) { return; } // Adjust mouse coordinates for scroll offset int32_t scrollX = win->hScroll ? win->hScroll->value : 0; int32_t scrollY = win->vScroll ? win->vScroll->value : 0; int32_t vx = x + scrollX; int32_t vy = y + scrollY; WidgetT *hit = hitTest(root, vx, vy); if (!hit) { return; } // Clear focus from all text inputs, set focus on clicked text input WidgetT *stack[64]; int32_t top = 0; stack[top++] = root; while (top > 0) { WidgetT *w = stack[--top]; w->focused = false; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (top < 64) { stack[top++] = c; } } } if (hit->type == WidgetTextInputE) { hit->focused = true; // Place cursor at click position AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t relX = x - hit->x - TEXT_INPUT_PAD; int32_t charPos = relX / font->charWidth + hit->as.textInput.scrollOff; if (charPos < 0) { charPos = 0; } if (charPos > hit->as.textInput.len) { charPos = hit->as.textInput.len; } hit->as.textInput.cursorPos = charPos; } if (hit->type == WidgetButtonE && hit->enabled) { hit->as.button.pressed = true; wgtInvalidate(hit); // The button release will be handled by the next mouse event // For now, just fire onClick on press if (hit->onClick) { hit->onClick(hit); } hit->as.button.pressed = false; } if (hit->type == WidgetCheckboxE && hit->enabled) { hit->as.checkbox.checked = !hit->as.checkbox.checked; if (hit->onChange) { hit->onChange(hit); } } if (hit->type == WidgetRadioE && hit->enabled && hit->parent && hit->parent->type == WidgetRadioGroupE) { hit->parent->as.radioGroup.selectedIdx = hit->as.radio.index; if (hit->parent->onChange) { hit->parent->onChange(hit->parent); } } wgtInvalidate(root); } // ============================================================ // widgetOnPaint // ============================================================ static void widgetOnPaint(WindowT *win, RectT *dirtyArea) { (void)dirtyArea; WidgetT *root = win->widgetRoot; if (!root) { return; } // Get context from root's userData AppContextT *ctx = (AppContextT *)root->userData; if (!ctx) { return; } // Set up a display context pointing at the content buffer DisplayT cd = ctx->display; cd.backBuf = win->contentBuf; cd.width = win->contentW; cd.height = win->contentH; cd.pitch = win->contentPitch; cd.clipX = 0; cd.clipY = 0; cd.clipW = win->contentW; cd.clipH = win->contentH; // Clear background rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg); // Apply scroll offset — layout at virtual size, positioned at -scroll int32_t scrollX = win->hScroll ? win->hScroll->value : 0; int32_t scrollY = win->vScroll ? win->vScroll->value : 0; int32_t layoutW = DVX_MAX(win->contentW, root->calcMinW); int32_t layoutH = DVX_MAX(win->contentH, root->calcMinH); root->x = -scrollX; root->y = -scrollY; root->w = layoutW; root->h = layoutH; layoutChildren(root, &ctx->font); // Paint widget tree (clip rect limits drawing to visible area) wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); } // ============================================================ // widgetOnResize // ============================================================ static void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) { (void)newW; (void)newH; WidgetT *root = win->widgetRoot; if (!root) { return; } AppContextT *ctx = (AppContextT *)root->userData; if (!ctx) { return; } widgetManageScrollbars(win, ctx); } // ============================================================ // widgetOnScroll // ============================================================ static void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) { (void)orient; (void)value; // Repaint with new scroll position if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); } } // ============================================================ // wgtButton // ============================================================ WidgetT *wgtButton(WidgetT *parent, const char *text) { WidgetT *w = allocWidget(parent, WidgetButtonE); if (w) { w->as.button.text = text; w->as.button.pressed = false; } return w; } // ============================================================ // wgtCheckbox // ============================================================ WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { WidgetT *w = allocWidget(parent, WidgetCheckboxE); if (w) { w->as.checkbox.text = text; w->as.checkbox.checked = false; } return w; } // ============================================================ // wgtDestroy // ============================================================ void wgtDestroy(WidgetT *w) { if (!w) { return; } if (w->parent) { removeChild(w->parent, w); } destroyChildren(w); if (w->type == WidgetTextInputE) { free(w->as.textInput.buf); } else if (w->type == WidgetTextAreaE) { free(w->as.textArea.buf); } // If this is the root, clear the window's reference if (w->window && w->window->widgetRoot == w) { w->window->widgetRoot = NULL; } free(w); } // ============================================================ // wgtFind // ============================================================ WidgetT *wgtFind(WidgetT *root, const char *name) { if (!root || !name) { return NULL; } if (root->name[0] && strcmp(root->name, name) == 0) { return root; } for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { WidgetT *found = wgtFind(c, name); if (found) { return found; } } return NULL; } // ============================================================ // wgtFrame // ============================================================ WidgetT *wgtFrame(WidgetT *parent, const char *title) { WidgetT *w = allocWidget(parent, WidgetFrameE); if (w) { w->as.frame.title = title; } return w; } // ============================================================ // wgtGetText // ============================================================ const char *wgtGetText(const WidgetT *w) { if (!w) { return ""; } switch (w->type) { case WidgetLabelE: return w->as.label.text; case WidgetButtonE: return w->as.button.text; case WidgetCheckboxE: return w->as.checkbox.text; case WidgetRadioE: return w->as.radio.text; case WidgetTextInputE: return w->as.textInput.buf ? w->as.textInput.buf : ""; default: return ""; } } // ============================================================ // wgtHBox // ============================================================ WidgetT *wgtHBox(WidgetT *parent) { return allocWidget(parent, WidgetHBoxE); } // ============================================================ // wgtHSeparator // ============================================================ WidgetT *wgtHSeparator(WidgetT *parent) { WidgetT *w = allocWidget(parent, WidgetSeparatorE); if (w) { w->as.separator.vertical = false; } return w; } // ============================================================ // wgtInitWindow // ============================================================ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { WidgetT *root = allocWidget(NULL, WidgetVBoxE); if (!root) { return NULL; } root->window = win; root->userData = ctx; win->widgetRoot = root; win->onPaint = widgetOnPaint; win->onMouse = widgetOnMouse; win->onKey = widgetOnKey; win->onResize = widgetOnResize; // Layout and paint are deferred until the caller adds widgets // and calls wgtInvalidate(root) or until the first resize/paint event. return root; } // ============================================================ // wgtInvalidate // ============================================================ void wgtInvalidate(WidgetT *w) { if (!w || !w->window) { return; } // Find the root WidgetT *root = w; while (root->parent) { root = root->parent; } AppContextT *ctx = (AppContextT *)root->userData; if (!ctx) { return; } // Manage scrollbars (measures, adds/removes scrollbars, relayouts) widgetManageScrollbars(w->window, ctx); // Repaint RectT fullRect = {0, 0, w->window->contentW, w->window->contentH}; widgetOnPaint(w->window, &fullRect); // Dirty the window on screen dvxInvalidateWindow(ctx, w->window); } // ============================================================ // wgtLabel // ============================================================ WidgetT *wgtLabel(WidgetT *parent, const char *text) { WidgetT *w = allocWidget(parent, WidgetLabelE); if (w) { w->as.label.text = text; } return w; } // ============================================================ // wgtLayout // ============================================================ void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) { if (!root) { return; } // Measure pass calcMinSizeTree(root, font); // Layout pass root->x = 0; root->y = 0; root->w = availW; root->h = availH; layoutChildren(root, font); } // ============================================================ // wgtListBox // ============================================================ WidgetT *wgtListBox(WidgetT *parent) { WidgetT *w = allocWidget(parent, WidgetListBoxE); if (w) { w->as.listBox.selectedIdx = -1; } return w; } // ============================================================ // wgtListBoxGetSelected // ============================================================ int32_t wgtListBoxGetSelected(const WidgetT *w) { if (!w || w->type != WidgetListBoxE) { return -1; } return w->as.listBox.selectedIdx; } // ============================================================ // wgtListBoxSetItems // ============================================================ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { if (!w || w->type != WidgetListBoxE) { return; } w->as.listBox.items = items; w->as.listBox.itemCount = count; if (w->as.listBox.selectedIdx >= count) { w->as.listBox.selectedIdx = -1; } } // ============================================================ // wgtListBoxSetSelected // ============================================================ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) { if (!w || w->type != WidgetListBoxE) { return; } w->as.listBox.selectedIdx = idx; } // ============================================================ // wgtPaint // ============================================================ void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { if (!root) { return; } paintWidget(root, d, ops, font, colors); } // ============================================================ // wgtRadio // ============================================================ WidgetT *wgtRadio(WidgetT *parent, const char *text) { WidgetT *w = allocWidget(parent, WidgetRadioE); if (w) { w->as.radio.text = text; // Auto-assign index based on position in parent int32_t idx = 0; for (WidgetT *c = parent->firstChild; c != w; c = c->nextSibling) { if (c->type == WidgetRadioE) { idx++; } } w->as.radio.index = idx; } return w; } // ============================================================ // wgtRadioGroup // ============================================================ WidgetT *wgtRadioGroup(WidgetT *parent) { WidgetT *w = allocWidget(parent, WidgetRadioGroupE); if (w) { w->as.radioGroup.selectedIdx = 0; } return w; } // ============================================================ // wgtResolveSize // ============================================================ int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) { if (taggedSize == 0) { return 0; } uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK; int32_t value = taggedSize & WGT_SIZE_VAL_MASK; switch (sizeType) { case WGT_SIZE_PIXELS: return value; case WGT_SIZE_CHARS: return value * charWidth; case WGT_SIZE_PERCENT: return (parentSize * value) / 100; default: return value; } } // ============================================================ // wgtSetEnabled // ============================================================ void wgtSetEnabled(WidgetT *w, bool enabled) { if (w) { w->enabled = enabled; } } // ============================================================ // wgtSetText // ============================================================ void wgtSetText(WidgetT *w, const char *text) { if (!w) { return; } switch (w->type) { case WidgetLabelE: w->as.label.text = text; break; case WidgetButtonE: w->as.button.text = text; break; case WidgetCheckboxE: w->as.checkbox.text = text; break; case WidgetRadioE: w->as.radio.text = text; break; case WidgetTextInputE: if (w->as.textInput.buf) { strncpy(w->as.textInput.buf, text, w->as.textInput.bufSize - 1); w->as.textInput.buf[w->as.textInput.bufSize - 1] = '\0'; w->as.textInput.len = (int32_t)strlen(w->as.textInput.buf); w->as.textInput.cursorPos = w->as.textInput.len; w->as.textInput.scrollOff = 0; } break; default: break; } } // ============================================================ // wgtSetVisible // ============================================================ void wgtSetVisible(WidgetT *w, bool visible) { if (w) { w->visible = visible; } } // ============================================================ // wgtSpacer // ============================================================ WidgetT *wgtSpacer(WidgetT *parent) { WidgetT *w = allocWidget(parent, WidgetSpacerE); if (w) { w->weight = 100; // spacers stretch by default } return w; } // ============================================================ // wgtTextArea // ============================================================ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { WidgetT *w = allocWidget(parent, WidgetTextAreaE); if (w) { int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; w->as.textArea.buf = (char *)malloc(bufSize); w->as.textArea.bufSize = bufSize; if (w->as.textArea.buf) { w->as.textArea.buf[0] = '\0'; } } return w; } // ============================================================ // wgtTextInput // ============================================================ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { WidgetT *w = allocWidget(parent, WidgetTextInputE); if (w) { int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; w->as.textInput.buf = (char *)malloc(bufSize); w->as.textInput.bufSize = bufSize; if (w->as.textInput.buf) { w->as.textInput.buf[0] = '\0'; } w->weight = 100; // text inputs stretch by default } return w; } // ============================================================ // wgtVBox // ============================================================ WidgetT *wgtVBox(WidgetT *parent) { return allocWidget(parent, WidgetVBoxE); } // ============================================================ // wgtVSeparator // ============================================================ WidgetT *wgtVSeparator(WidgetT *parent) { WidgetT *w = allocWidget(parent, WidgetSeparatorE); if (w) { w->as.separator.vertical = true; } return w; }