1051 lines
35 KiB
C
1051 lines
35 KiB
C
// 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;
|
|
}
|
|
}
|