#define DVX_WIDGET_IMPL // widgetEvent.c -- Window event handlers for widget system // // This file routes window-level events (mouse, keyboard, paint, resize, // scroll) into the widget tree. It serves as the bridge between the // window manager (dvxWm) and the widget system. // // Event handling follows a priority system for mouse events: // 1. Active drag/interaction states (slider drag, button press tracking, // text selection, canvas drawing, column resize, drag-reorder, // splitter drag) are checked first and handled directly. // 2. Open popups (dropdown/combobox lists) intercept clicks next. // 3. Normal hit testing routes clicks to the target widget. // // This priority ordering ensures that ongoing interactions complete // correctly even if the mouse moves outside the originating widget. // For example, dragging a slider and moving the mouse above the slider // still adjusts the value, because sDragSlider captures the event // before hit testing runs. #include "dvxWidgetPlugin.h" // Widget whose popup was just closed by click-outside -- prevents // immediate re-open on the same click. Without this, clicking the // dropdown button to close its popup would immediately hit-test the // button again and re-open the popup in the same event. WidgetT *sClosedPopup = NULL; // ============================================================ // widgetManageScrollbars // ============================================================ // // Manages automatic scrollbar addition/removal for widget-based windows. // Called on every invalidation to ensure scrollbars match the current // widget tree's minimum size requirements. // // The algorithm: // 1. Measure the full widget tree to get its minimum size. // 2. Remove all existing scrollbars to measure the full available area. // 3. Compare min size vs available area to decide if scrollbars are needed. // 4. Account for scrollbar interaction: adding a vertical scrollbar // reduces horizontal space, which may require a horizontal scrollbar // (and vice versa). This mutual dependency is handled with a single // extra check rather than iterating to convergence. // 5. Preserve scroll positions across scrollbar recreation. // 6. Layout at the virtual content size (max of available and minimum). // // The virtual content size concept is key: if the widget tree needs // 800px but only 600px is available, the tree is laid out at 800px // and the window scrolls to show the visible portion. This means // widget positions can be negative (scrolled above the viewport) // or extend past the window edge (scrolled below). 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 // ============================================================ // // Keyboard event dispatch. Unlike mouse events which use hit testing, // keyboard events go directly to the focused widget (sFocusedWidget). // The cached pointer avoids an O(n) tree walk to find the focused // widget on every keypress. // // There is no keyboard event bubbling -- if the focused widget doesn't // handle a key, it's simply dropped. Accelerators (Alt+key) are // handled at a higher level in the app event loop, not here. void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { WidgetT *root = win->widgetRoot; if (!root) { return; } // Use cached focus pointer -- O(1) instead of O(n) tree walk WidgetT *focus = sFocusedWidget; if (!focus || !focus->focused || focus->window != win) { return; } // Don't dispatch keys to disabled widgets if (!focus->enabled) { return; } // Attribute allocations during event handling to the owning app AppContextT *ctx = (AppContextT *)root->userData; int32_t prevAppId = ctx->currentAppId; ctx->currentAppId = win->appId; wclsOnKey(focus, key, mod); ctx->currentAppId = prevAppId; } // ============================================================ // widgetOnMouse // ============================================================ // // Main mouse event handler. This is the most complex event handler // because it must manage multiple overlapping interaction states. // // The function is structured as a series of early-return checks: // each active interaction (drag, press, popup) is checked in priority // order. If the interaction handles the event, it returns immediately. // Only if no interaction is active does the event fall through to // normal hit testing. // // Coordinates (x, y) are in content-buffer space -- the window manager // has already subtracted the window chrome offset. Widget positions // are also in content-buffer space (set during layout), so no // coordinate transform is needed for hit testing. static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons); void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; sClosedPopup = NULL; if (!root) { return; } // Attribute allocations during event handling to the owning app AppContextT *ctx = (AppContextT *)root->userData; int32_t prevAppId = ctx->currentAppId; ctx->currentAppId = win->appId; widgetOnMouseInner(win, root, x, y, buttons); ctx->currentAppId = prevAppId; } static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) { // Close popups from other windows if (sOpenPopup && sOpenPopup->window != win) { wclsClosePopup(sOpenPopup); sOpenPopup = NULL; } // Handle drag release if (sDragWidget && !(buttons & MOUSE_LEFT)) { wclsOnDragEnd(sDragWidget, root, x, y); wgtInvalidatePaint(root); sDragWidget = NULL; return; } // Handle drag move if (sDragWidget && (buttons & MOUSE_LEFT)) { wclsOnDragUpdate(sDragWidget, root, x, y); // quickRepaint fast path for text drag (dirty rect instead of full repaint) if (wclsHas(sDragWidget, WGT_METHOD_QUICK_REPAINT)) { int32_t dirtyY = 0; int32_t dirtyH = 0; if (wclsQuickRepaint(sDragWidget, &dirtyY, &dirtyH) > 0) { AppContextT *ctx = (AppContextT *)root->userData; int32_t scrollY2 = win->vScroll ? win->vScroll->value : 0; int32_t rectX = win->x + win->contentX; int32_t rectY = win->y + win->contentY + dirtyY - scrollY2; int32_t rectW = win->contentW; win->contentDirty = true; dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); return; } } // Scroll containers need full relayout if (sDragWidget->wclass && (sDragWidget->wclass->flags & WCLASS_RELAYOUT_ON_SCROLL)) { wgtInvalidate(sDragWidget); } else { wgtInvalidatePaint(root); } return; } // Handle open popup clicks if (sOpenPopup && (buttons & MOUSE_LEFT)) { AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t popX; int32_t popY; int32_t popW; int32_t popH; if (!wclsHas(sOpenPopup, WGT_METHOD_GET_POPUP_RECT)) { sOpenPopup = NULL; return; } wclsGetPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH); if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) { // Click on popup item -- dispatch to widget's onMouse wclsOnMouse(sOpenPopup, root, x, y); sOpenPopup = NULL; wgtInvalidate(root); return; } // Click outside popup -- close it and remember which widget it was sClosedPopup = sOpenPopup; wclsClosePopup(sOpenPopup); sOpenPopup = NULL; wgtInvalidatePaint(root); // Fall through to normal click handling } if (!(buttons & MOUSE_LEFT)) { return; } // Widget positions are already in content-buffer space (widgetOnPaint // sets root to -scrollX/-scrollY), so use raw content-relative coords. int32_t vx = x; int32_t vy = y; WidgetT *hit = widgetHitTest(root, vx, vy); if (!hit) { return; } // Clear focus from the previously focused widget. This is done via // the cached sFocusedWidget pointer rather than walking the tree to // find the focused widget -- an O(1) operation vs O(n). WidgetT *prevFocus = sFocusedWidget; if (sFocusedWidget) { sFocusedWidget->focused = false; sFocusedWidget = NULL; } // Dispatch to the hit widget's mouse handler via vtable. The handler // is responsible for setting hit->focused=true if it wants focus. if (hit->enabled) { wclsOnMouse(hit, root, vx, vy); } // Universal click/double-click callbacks -- fire for ALL widget types // after the type-specific handler has run. Buttons and image buttons // are excluded from onClick because they use press-release semantics // (onClick fires on button-up, not button-down) and already handle it // in the release handler above. They still get onDblClick here. if (hit->enabled) { int32_t clicks = multiClickDetect(vx, vy); bool isBtn = (hit->wclass && (hit->wclass->flags & WCLASS_PRESS_RELEASE)); if (clicks >= 2 && hit->onDblClick) { hit->onDblClick(hit); } else if (!isBtn && hit->onClick) { hit->onClick(hit); } } // Update the cached focus pointer for O(1) access in widgetOnKey if (hit->focused) { sFocusedWidget = hit; } // Fire focus/blur callbacks on transitions if (prevFocus && prevFocus != sFocusedWidget && prevFocus->onBlur) { prevFocus->onBlur(prevFocus); } if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) { sFocusedWidget->onFocus(sFocusedWidget); } wgtInvalidate(root); } // ============================================================ // widgetOnPaint // ============================================================ // // Paints the entire widget tree into the window's content buffer. // Called whenever the window needs a redraw (invalidation, scroll, // resize). // // Sets up a temporary DisplayT context that points at the window's // content buffer instead of the screen backbuffer. This means all // draw operations (drawText, rectFill, drawBevel, etc.) write // directly into the per-window content buffer, which the compositor // later blits to the screen backbuffer. // // Scroll offset is applied by shifting the root widget's position // to negative coordinates (-scrollX, -scrollY). This elegantly makes // scrolling work without any special scroll handling in individual // widgets -- their positions are simply offset, and the clip rect // on the DisplayT limits drawing to the visible area. // // The conditional re-layout avoids redundant layout passes when only // the paint is needed (e.g. cursor blink, selection change). 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 and re-layout at virtual size 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); // Only re-layout if root position or size actually changed if (root->x != -scrollX || root->y != -scrollY || root->w != layoutW || root->h != layoutH) { root->x = -scrollX; root->y = -scrollY; root->w = layoutW; root->h = layoutH; widgetLayoutChildren(root, &ctx->font); } // Auto-focus first focusable widget if nothing has focus yet if (!sFocusedWidget) { WidgetT *first = widgetFindNextFocusable(root, NULL); if (first) { first->focused = true; sFocusedWidget = first; } } // 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 // ============================================================ // // Called when the window is resized. Triggers a full scrollbar // re-evaluation and relayout, since the available content area // changed and scrollbars may need to be added or removed. 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 // ============================================================ // // Called by the WM when a window scrollbar value changes (user dragged // the thumb, clicked the track, or used arrow buttons). Triggers a // full repaint so the widget tree is redrawn at the new scroll offset. // The actual scroll offset is read from win->vScroll/hScroll in the // paint handler, so the orient and value parameters aren't directly used. void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) { (void)orient; (void)value; // Repaint with new scroll position -- dvxInvalidateWindow calls onPaint if (win->widgetRoot) { AppContextT *ctx = wgtGetContext(win->widgetRoot); if (ctx) { dvxInvalidateWindow(ctx, win); } } }