// widgetEvent.c — Window event handlers for widget system #include "widgetInternal.h" // Widget whose popup was just closed by click-outside — prevents // immediate re-open on the same click. WidgetT *sClosedPopup = NULL; // ============================================================ // 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) { 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 && widgetIsFocusable(w->type)) { focus = w; break; } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->visible && top < 64) { stack[top++] = c; } } } if (!focus) { return; } // Dispatch to per-widget onKey handler via vtable if (focus->wclass && focus->wclass->onKey) { focus->wclass->onKey(focus, key, mod); } } // ============================================================ // widgetOnMouse // ============================================================ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; sClosedPopup = NULL; 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 text drag-select release if (sDragTextSelect && !(buttons & 1)) { sDragTextSelect = NULL; return; } // Handle text drag-select (mouse move while pressed) if (sDragTextSelect && (buttons & 1)) { 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; widgetTextDragUpdate(sDragTextSelect, root, vx, vy); wgtInvalidate(root); return; } // 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, root, 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 button press release if (sPressedButton && !(buttons & 1)) { if (sPressedButton->type == WidgetImageButtonE) { sPressedButton->as.imageButton.pressed = false; } else { sPressedButton->as.button.pressed = false; } // Fire onClick if released over the same button in the same window if (sPressedButton->window == win) { 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; if (vx >= sPressedButton->x && vx < sPressedButton->x + sPressedButton->w && vy >= sPressedButton->y && vy < sPressedButton->y + sPressedButton->h) { if (sPressedButton->onClick) { sPressedButton->onClick(sPressedButton); } } } wgtInvalidate(sPressedButton); sPressedButton = NULL; return; } // Handle button press tracking (mouse move while held) if (sPressedButton && (buttons & 1)) { bool over = false; if (sPressedButton->window == win) { 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; over = (vx >= sPressedButton->x && vx < sPressedButton->x + sPressedButton->w && vy >= sPressedButton->y && vy < sPressedButton->y + sPressedButton->h); } bool curPressed = (sPressedButton->type == WidgetImageButtonE) ? sPressedButton->as.imageButton.pressed : sPressedButton->as.button.pressed; if (curPressed != over) { if (sPressedButton->type == WidgetImageButtonE) { sPressedButton->as.imageButton.pressed = over; } else { sPressedButton->as.button.pressed = over; } wgtInvalidate(sPressedButton); } 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 and remember which widget it was sClosedPopup = sOpenPopup; 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 handler via vtable if (hit->enabled && hit->wclass && hit->wclass->onMouse) { hit->wclass->onMouse(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; // Dirty the window content area on screen so compositor redraws it if (win->widgetRoot) { AppContextT *ctx = (AppContextT *)win->widgetRoot->userData; if (ctx) { dvxInvalidateWindow(ctx, win); } } } }