// 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 "widgetInternal.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; } // Dispatch to per-widget onKey handler via vtable if (focus->wclass && focus->wclass->onKey) { focus->wclass->onKey(focus, key, mod); } } // ============================================================ // 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. 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 & MOUSE_LEFT)) { sDragTextSelect = NULL; return; } // Handle text drag-select (mouse move while pressed) if (sDragTextSelect && (buttons & MOUSE_LEFT)) { widgetTextDragUpdate(sDragTextSelect, root, x, y); if (sDragTextSelect->type == WidgetAnsiTermE) { // Fast path: repaint only dirty terminal rows into the // content buffer, then dirty just that screen stripe. int32_t dirtyY = 0; int32_t dirtyH = 0; if (wgtAnsiTermRepaint(sDragTextSelect, &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); } } else { wgtInvalidate(root); } return; } // Handle canvas drawing release if (sDrawingCanvas && !(buttons & MOUSE_LEFT)) { sDrawingCanvas->as.canvas.lastX = -1; sDrawingCanvas->as.canvas.lastY = -1; sDrawingCanvas = NULL; wgtInvalidatePaint(root); return; } // Handle canvas drawing (mouse move while pressed) if (sDrawingCanvas && (buttons & MOUSE_LEFT)) { widgetCanvasOnMouse(sDrawingCanvas, root, x, y); wgtInvalidatePaint(root); return; } // Handle slider drag release if (sDragSlider && !(buttons & MOUSE_LEFT)) { sDragSlider = NULL; wgtInvalidatePaint(root); return; } // Handle slider drag (mouse move while pressed) if (sDragSlider && (buttons & MOUSE_LEFT)) { 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); } wgtInvalidatePaint(root); } } return; } // Handle ListView column resize release if (sResizeListView && !(buttons & MOUSE_LEFT)) { sResizeListView = NULL; sResizeCol = -1; return; } // Handle ListView column resize drag if (sResizeListView && (buttons & MOUSE_LEFT)) { int32_t delta = x - sResizeStartX; int32_t newW = sResizeOrigW + delta; if (newW < LISTVIEW_MIN_COL_W) { newW = LISTVIEW_MIN_COL_W; } if (newW != sResizeListView->as.listView->resolvedColW[sResizeCol]) { sResizeListView->as.listView->resolvedColW[sResizeCol] = newW; // Recalculate totalColW int32_t total = 0; for (int32_t c = 0; c < sResizeListView->as.listView->colCount; c++) { total += sResizeListView->as.listView->resolvedColW[c]; } sResizeListView->as.listView->totalColW = total; wgtInvalidatePaint(root); } return; } // Handle drag-reorder release if (sDragReorder && !(buttons & MOUSE_LEFT)) { widgetReorderDrop(sDragReorder); sDragReorder = NULL; wgtInvalidatePaint(root); return; } // Handle drag-reorder move if (sDragReorder && (buttons & MOUSE_LEFT)) { widgetReorderUpdate(sDragReorder, root, x, y); wgtInvalidatePaint(root); return; } // Handle splitter drag release if (sDragSplitter && !(buttons & MOUSE_LEFT)) { sDragSplitter = NULL; return; } // Handle splitter drag if (sDragSplitter && (buttons & MOUSE_LEFT)) { int32_t pos; if (sDragSplitter->as.splitter.vertical) { pos = x - sDragSplitter->x - sDragSplitStart; } else { pos = y - sDragSplitter->y - sDragSplitStart; } widgetSplitterClampPos(sDragSplitter, &pos); if (pos != sDragSplitter->as.splitter.dividerPos) { sDragSplitter->as.splitter.dividerPos = pos; if (sDragSplitter->onChange) { sDragSplitter->onChange(sDragSplitter); } wgtInvalidate(sDragSplitter); } return; } // Handle button press release if (sPressedButton && !(buttons & MOUSE_LEFT)) { 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) { if (x >= sPressedButton->x && x < sPressedButton->x + sPressedButton->w && y >= sPressedButton->y && y < sPressedButton->y + sPressedButton->h) { if (sPressedButton->onClick) { sPressedButton->onClick(sPressedButton); } } } wgtInvalidatePaint(sPressedButton); sPressedButton = NULL; return; } // Handle button press tracking (mouse move while held) if (sPressedButton && (buttons & MOUSE_LEFT)) { bool over = false; if (sPressedButton->window == win) { over = (x >= sPressedButton->x && x < sPressedButton->x + sPressedButton->w && y >= sPressedButton->y && y < 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; } wgtInvalidatePaint(sPressedButton); } 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; 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; 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). 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 && hit->wclass && hit->wclass->onMouse) { hit->wclass->onMouse(hit, root, vx, vy); } // Update the cached focus pointer for O(1) access in widgetOnKey if (hit->focused) { sFocusedWidget = hit; } 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); } } } // ============================================================ // widgetReorderDrop — finalize drag-reorder on mouse release // ============================================================ // // Completes a drag-reorder operation for ListBox, ListView, or TreeView. // Moves the dragged item from its original position to the drop position // by shifting intermediate elements. This is an in-place array rotation // for ListBox/ListView (O(n) element moves) and a linked-list splice // for TreeView. // // For ListBox and ListView, the item array, selection bits, and sort // indices are all shifted together to maintain consistency. The selected // index is updated to follow the moved item. // // For TreeView, the operation is a tree node re-parenting: unlink the // dragged item from its old parent's child list, then insert it before // or after the drop target in the target's parent's child list. void widgetReorderDrop(WidgetT *w) { if (w->type == WidgetListBoxE) { int32_t drag = w->as.listBox.dragIdx; int32_t drop = w->as.listBox.dropIdx; w->as.listBox.dragIdx = -1; w->as.listBox.dropIdx = -1; if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) { return; } // Move item at dragIdx to before dropIdx const char *temp = w->as.listBox.items[drag]; uint8_t selBit = 0; if (w->as.listBox.multiSelect && w->as.listBox.selBits) { selBit = w->as.listBox.selBits[drag]; } if (drag < drop) { for (int32_t i = drag; i < drop - 1; i++) { w->as.listBox.items[i] = w->as.listBox.items[i + 1]; if (w->as.listBox.selBits) { w->as.listBox.selBits[i] = w->as.listBox.selBits[i + 1]; } } w->as.listBox.items[drop - 1] = temp; if (w->as.listBox.selBits) { w->as.listBox.selBits[drop - 1] = selBit; } w->as.listBox.selectedIdx = drop - 1; } else { for (int32_t i = drag; i > drop; i--) { w->as.listBox.items[i] = w->as.listBox.items[i - 1]; if (w->as.listBox.selBits) { w->as.listBox.selBits[i] = w->as.listBox.selBits[i - 1]; } } w->as.listBox.items[drop] = temp; if (w->as.listBox.selBits) { w->as.listBox.selBits[drop] = selBit; } w->as.listBox.selectedIdx = drop; } if (w->onChange) { w->onChange(w); } } else if (w->type == WidgetListViewE) { int32_t drag = w->as.listView->dragIdx; int32_t drop = w->as.listView->dropIdx; int32_t colCnt = w->as.listView->colCount; w->as.listView->dragIdx = -1; w->as.listView->dropIdx = -1; if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) { return; } // Move row at dragIdx to before dropIdx const char *temp[LISTVIEW_MAX_COLS]; for (int32_t c = 0; c < colCnt; c++) { temp[c] = w->as.listView->cellData[drag * colCnt + c]; } uint8_t selBit = 0; if (w->as.listView->multiSelect && w->as.listView->selBits) { selBit = w->as.listView->selBits[drag]; } int32_t sortVal = 0; if (w->as.listView->sortIndex) { sortVal = w->as.listView->sortIndex[drag]; } if (drag < drop) { for (int32_t i = drag; i < drop - 1; i++) { for (int32_t c = 0; c < colCnt; c++) { w->as.listView->cellData[i * colCnt + c] = w->as.listView->cellData[(i + 1) * colCnt + c]; } if (w->as.listView->selBits) { w->as.listView->selBits[i] = w->as.listView->selBits[i + 1]; } if (w->as.listView->sortIndex) { w->as.listView->sortIndex[i] = w->as.listView->sortIndex[i + 1]; } } int32_t dest = drop - 1; for (int32_t c = 0; c < colCnt; c++) { w->as.listView->cellData[dest * colCnt + c] = temp[c]; } if (w->as.listView->selBits) { w->as.listView->selBits[dest] = selBit; } if (w->as.listView->sortIndex) { w->as.listView->sortIndex[dest] = sortVal; } w->as.listView->selectedIdx = dest; } else { for (int32_t i = drag; i > drop; i--) { for (int32_t c = 0; c < colCnt; c++) { w->as.listView->cellData[i * colCnt + c] = w->as.listView->cellData[(i - 1) * colCnt + c]; } if (w->as.listView->selBits) { w->as.listView->selBits[i] = w->as.listView->selBits[i - 1]; } if (w->as.listView->sortIndex) { w->as.listView->sortIndex[i] = w->as.listView->sortIndex[i - 1]; } } for (int32_t c = 0; c < colCnt; c++) { w->as.listView->cellData[drop * colCnt + c] = temp[c]; } if (w->as.listView->selBits) { w->as.listView->selBits[drop] = selBit; } if (w->as.listView->sortIndex) { w->as.listView->sortIndex[drop] = sortVal; } w->as.listView->selectedIdx = drop; } if (w->onChange) { w->onChange(w); } } else if (w->type == WidgetTreeViewE) { WidgetT *drag = w->as.treeView.dragItem; WidgetT *target = w->as.treeView.dropTarget; bool after = w->as.treeView.dropAfter; w->as.treeView.dragItem = NULL; w->as.treeView.dropTarget = NULL; if (!drag || !target || drag == target) { return; } // Unlink drag from its current parent WidgetT *oldParent = drag->parent; if (oldParent->firstChild == drag) { oldParent->firstChild = drag->nextSibling; } else { for (WidgetT *c = oldParent->firstChild; c; c = c->nextSibling) { if (c->nextSibling == drag) { c->nextSibling = drag->nextSibling; break; } } } drag->nextSibling = NULL; // Insert drag before or after target (same parent level) WidgetT *newParent = target->parent; drag->parent = newParent; if (after) { drag->nextSibling = target->nextSibling; target->nextSibling = drag; } else { if (newParent->firstChild == target) { drag->nextSibling = target; newParent->firstChild = drag; } else { for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) { if (c->nextSibling == target) { c->nextSibling = drag; drag->nextSibling = target; break; } } } } if (w->onChange) { w->onChange(w); } } } // ============================================================ // widgetReorderUpdate — update drop position during drag // ============================================================ // // Tracks the mouse during a drag-reorder, updating the drop indicator // position and auto-scrolling when the mouse is near the widget's edges. // // The drop position is computed from the mouse Y relative to item // boundaries: if the mouse is in the top half of an item, the drop // indicator goes before that item; if in the bottom half, it goes // after. This gives intuitive "insert between items" behavior. // // Auto-scrolling happens when the mouse is within one row-height of // the top or bottom edge, allowing the user to drag items to positions // not currently visible. void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { (void)x; AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; if (w->type == WidgetListBoxE) { int32_t innerY = w->y + LISTBOX_BORDER; int32_t innerH = w->h - LISTBOX_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; int32_t maxScroll = w->as.listBox.itemCount - visibleRows; if (maxScroll < 0) { maxScroll = 0; } // Auto-scroll when dragging near edges if (y < innerY + font->charHeight && w->as.listBox.scrollPos > 0) { w->as.listBox.scrollPos--; } else if (y > innerY + innerH - font->charHeight && w->as.listBox.scrollPos < maxScroll) { w->as.listBox.scrollPos++; } int32_t relY = y - innerY; int32_t row = w->as.listBox.scrollPos + relY / font->charHeight; int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0; int32_t drop = row + halfRow; if (drop < 0) { drop = 0; } if (drop > w->as.listBox.itemCount) { drop = w->as.listBox.itemCount; } w->as.listBox.dropIdx = drop; } else if (w->type == WidgetListViewE) { int32_t headerH = font->charHeight + 4; int32_t innerY = w->y + LISTVIEW_BORDER + headerH; int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t visibleRows = innerH / font->charHeight; int32_t maxScroll = w->as.listView->rowCount - visibleRows; if (maxScroll < 0) { maxScroll = 0; } // Auto-scroll when dragging near edges if (y < innerY + font->charHeight && w->as.listView->scrollPos > 0) { w->as.listView->scrollPos--; } else if (y > innerY + innerH - font->charHeight && w->as.listView->scrollPos < maxScroll) { w->as.listView->scrollPos++; } int32_t relY = y - innerY; int32_t row = w->as.listView->scrollPos + relY / font->charHeight; int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0; int32_t drop = row + halfRow; if (drop < 0) { drop = 0; } if (drop > w->as.listView->rowCount) { drop = w->as.listView->rowCount; } w->as.listView->dropIdx = drop; } else if (w->type == WidgetTreeViewE) { int32_t innerY = w->y + TREE_BORDER; int32_t innerH = w->h - TREE_BORDER * 2; // Auto-scroll when dragging near edges (pixel-based scroll) if (y < innerY + font->charHeight && w->as.treeView.scrollPos > 0) { w->as.treeView.scrollPos -= font->charHeight; if (w->as.treeView.scrollPos < 0) { w->as.treeView.scrollPos = 0; } } else if (y > innerY + innerH - font->charHeight) { w->as.treeView.scrollPos += font->charHeight; // Paint will clamp to actual max } // Find which visible item the mouse is over int32_t curY = w->y + TREE_BORDER - w->as.treeView.scrollPos; // Walk visible items to find drop target WidgetT *first = NULL; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->type == WidgetTreeItemE && c->visible) { first = c; break; } } WidgetT *target = NULL; bool after = false; for (WidgetT *v = first; v; v = widgetTreeViewNextVisible(v, w)) { int32_t itemBot = curY + font->charHeight; int32_t mid = curY + font->charHeight / 2; if (y < mid) { target = v; after = false; break; } curY = itemBot; // Check if mouse is between this item and next WidgetT *next = widgetTreeViewNextVisible(v, w); if (!next || y < itemBot) { target = v; after = true; break; } } w->as.treeView.dropTarget = target; w->as.treeView.dropAfter = after; } }