// widgetEvent.c — Window event handlers for widget system #include "widgetInternal.h" // ============================================================ // 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. void widgetManageScrollbars(WindowT *win, AppContextT *ctx) { WidgetT *root = win->widgetRoot; if (!root) { return; } // Measure the tree without any layout pass widgetCalcMinSizeTree(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 // ============================================================ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { (void)mod; WidgetT *root = win->widgetRoot; if (!root) { return; } // Find the focused widget WidgetT *focus = NULL; WidgetT *stack[64]; int32_t top = 0; stack[top++] = root; while (top > 0) { WidgetT *w = stack[--top]; if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE)) { focus = w; break; } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (top < 64) { stack[top++] = c; } } } if (!focus) { return; } // Handle text input for TextInput and ComboBox char *buf = NULL; int32_t bufSize = 0; int32_t *pLen = NULL; int32_t *pCursor = NULL; int32_t *pScrollOff = NULL; if (focus->type == WidgetTextInputE) { buf = focus->as.textInput.buf; bufSize = focus->as.textInput.bufSize; pLen = &focus->as.textInput.len; pCursor = &focus->as.textInput.cursorPos; pScrollOff = &focus->as.textInput.scrollOff; } else if (focus->type == WidgetComboBoxE) { buf = focus->as.comboBox.buf; bufSize = focus->as.comboBox.bufSize; pLen = &focus->as.comboBox.len; pCursor = &focus->as.comboBox.cursorPos; pScrollOff = &focus->as.comboBox.scrollOff; } if (!buf) { return; } if (key >= 32 && key < 127) { // Printable character if (*pLen < bufSize - 1) { int32_t pos = *pCursor; memmove(buf + pos + 1, buf + pos, *pLen - pos + 1); buf[pos] = (char)key; (*pLen)++; (*pCursor)++; if (focus->onChange) { focus->onChange(focus); } } } else if (key == 8) { // Backspace if (*pCursor > 0) { int32_t pos = *pCursor; memmove(buf + pos - 1, buf + pos, *pLen - pos + 1); (*pLen)--; (*pCursor)--; if (focus->onChange) { focus->onChange(focus); } } } else if (key == (0x4B | 0x100)) { // Left arrow if (*pCursor > 0) { (*pCursor)--; } } else if (key == (0x4D | 0x100)) { // Right arrow if (*pCursor < *pLen) { (*pCursor)++; } } else if (key == (0x47 | 0x100)) { // Home *pCursor = 0; } else if (key == (0x4F | 0x100)) { // End *pCursor = *pLen; } else if (key == (0x53 | 0x100)) { // Delete if (*pCursor < *pLen) { int32_t pos = *pCursor; memmove(buf + pos, buf + pos + 1, *pLen - pos); (*pLen)--; 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 fieldW = focus->w; if (focus->type == WidgetComboBoxE) { fieldW -= DROPDOWN_BTN_WIDTH; } int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth; if (*pCursor < *pScrollOff) { *pScrollOff = *pCursor; } if (*pCursor >= *pScrollOff + visibleChars) { *pScrollOff = *pCursor - visibleChars + 1; } // Repaint the window wgtInvalidate(focus); } // ============================================================ // widgetOnMouse // ============================================================ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; if (!root) { return; } // Close popups from other windows if (sOpenPopup && sOpenPopup->window != win) { if (sOpenPopup->type == WidgetDropdownE) { sOpenPopup->as.dropdown.open = false; } else if (sOpenPopup->type == WidgetComboBoxE) { sOpenPopup->as.comboBox.open = false; } sOpenPopup = NULL; } // Handle canvas drawing release if (sDrawingCanvas && !(buttons & 1)) { sDrawingCanvas->as.canvas.lastX = -1; sDrawingCanvas->as.canvas.lastY = -1; sDrawingCanvas = NULL; wgtInvalidate(root); return; } // Handle canvas drawing (mouse move while pressed) if (sDrawingCanvas && (buttons & 1)) { widgetCanvasOnMouse(sDrawingCanvas, x, y); wgtInvalidate(root); return; } // Handle slider drag release if (sDragSlider && !(buttons & 1)) { sDragSlider = NULL; wgtInvalidate(root); return; } // Handle slider drag (mouse move while pressed) if (sDragSlider && (buttons & 1)) { int32_t range = sDragSlider->as.slider.maxValue - sDragSlider->as.slider.minValue; if (range > 0) { int32_t newVal; if (sDragSlider->as.slider.vertical) { int32_t thumbRange = sDragSlider->h - SLIDER_THUMB_W; int32_t relY = y - sDragSlider->y - sDragOffset; newVal = sDragSlider->as.slider.minValue + (relY * range) / thumbRange; } else { int32_t thumbRange = sDragSlider->w - SLIDER_THUMB_W; int32_t relX = x - sDragSlider->x - sDragOffset; newVal = sDragSlider->as.slider.minValue + (relX * range) / thumbRange; } if (newVal < sDragSlider->as.slider.minValue) { newVal = sDragSlider->as.slider.minValue; } if (newVal > sDragSlider->as.slider.maxValue) { newVal = sDragSlider->as.slider.maxValue; } if (newVal != sDragSlider->as.slider.value) { sDragSlider->as.slider.value = newVal; if (sDragSlider->onChange) { sDragSlider->onChange(sDragSlider); } wgtInvalidate(root); } } return; } // Handle open popup clicks if (sOpenPopup && (buttons & 1)) { AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t popX; int32_t popY; int32_t popW; int32_t popH; widgetDropdownPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH); if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) { // Click on popup item int32_t itemIdx = (y - popY - 2) / font->charHeight; int32_t scrollP = 0; if (sOpenPopup->type == WidgetDropdownE) { scrollP = sOpenPopup->as.dropdown.scrollPos; } else { scrollP = sOpenPopup->as.comboBox.listScrollPos; } itemIdx += scrollP; if (sOpenPopup->type == WidgetDropdownE) { if (itemIdx >= 0 && itemIdx < sOpenPopup->as.dropdown.itemCount) { sOpenPopup->as.dropdown.selectedIdx = itemIdx; sOpenPopup->as.dropdown.open = false; if (sOpenPopup->onChange) { sOpenPopup->onChange(sOpenPopup); } } } else if (sOpenPopup->type == WidgetComboBoxE) { if (itemIdx >= 0 && itemIdx < sOpenPopup->as.comboBox.itemCount) { sOpenPopup->as.comboBox.selectedIdx = itemIdx; sOpenPopup->as.comboBox.open = false; // Copy selected item text to buffer const char *itemText = sOpenPopup->as.comboBox.items[itemIdx]; strncpy(sOpenPopup->as.comboBox.buf, itemText, sOpenPopup->as.comboBox.bufSize - 1); sOpenPopup->as.comboBox.buf[sOpenPopup->as.comboBox.bufSize - 1] = '\0'; sOpenPopup->as.comboBox.len = (int32_t)strlen(sOpenPopup->as.comboBox.buf); sOpenPopup->as.comboBox.cursorPos = sOpenPopup->as.comboBox.len; sOpenPopup->as.comboBox.scrollOff = 0; if (sOpenPopup->onChange) { sOpenPopup->onChange(sOpenPopup); } } } sOpenPopup = NULL; wgtInvalidate(root); return; } // Click outside popup — close it if (sOpenPopup->type == WidgetDropdownE) { sOpenPopup->as.dropdown.open = false; } else if (sOpenPopup->type == WidgetComboBoxE) { sOpenPopup->as.comboBox.open = false; } sOpenPopup = NULL; wgtInvalidate(root); // Fall through to normal click handling } if (!(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 = widgetHitTest(root, vx, vy); if (!hit) { return; } // Clear focus from all widgets, set focus on clicked widget WidgetT *fstack[64]; int32_t ftop = 0; fstack[ftop++] = root; while (ftop > 0) { WidgetT *w = fstack[--ftop]; w->focused = false; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (ftop < 64) { fstack[ftop++] = c; } } } // Dispatch to per-widget mouse handlers if (hit->type == WidgetTextInputE) { widgetTextInputOnMouse(hit, root, vx); } if (hit->type == WidgetButtonE && hit->enabled) { widgetButtonOnMouse(hit); } if (hit->type == WidgetCheckboxE && hit->enabled) { widgetCheckboxOnMouse(hit); } if (hit->type == WidgetRadioE && hit->enabled) { widgetRadioOnMouse(hit); } if (hit->type == WidgetImageE && hit->enabled) { widgetImageOnMouse(hit); } if (hit->type == WidgetCanvasE && hit->enabled) { widgetCanvasOnMouse(hit, vx, vy); } if (hit->type == WidgetDropdownE && hit->enabled) { widgetDropdownOnMouse(hit); } if (hit->type == WidgetComboBoxE && hit->enabled) { widgetComboBoxOnMouse(hit, root, vx); } if (hit->type == WidgetSliderE && hit->enabled) { widgetSliderOnMouse(hit, vx, vy); } if (hit->type == WidgetTabControlE && hit->enabled) { widgetTabControlOnMouse(hit, root, vx, vy); } if (hit->type == WidgetTreeViewE && hit->enabled) { widgetTreeViewOnMouse(hit, root, vx, vy); } wgtInvalidate(root); } // ============================================================ // widgetOnPaint // ============================================================ 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; widgetLayoutChildren(root, &ctx->font); // Paint widget tree (clip rect limits drawing to visible area) wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); // Paint overlay popups (dropdown/combobox) widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); } // ============================================================ // widgetOnResize // ============================================================ 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 // ============================================================ 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); win->contentDirty = true; } }