DVX_GUI/dvx/widgets/widgetEvent.c
2026-03-12 21:16:52 -05:00

1033 lines
32 KiB
C

// 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 && 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;
}
// Handle ANSI terminal key input
if (focus->type == WidgetAnsiTermE) {
widgetAnsiTermOnKey(focus, key);
wgtInvalidate(focus);
return;
}
// Handle tree view keyboard navigation
if (focus->type == WidgetTreeViewE) {
widgetTreeViewOnKey(focus, key);
return;
}
// Handle list box keyboard navigation
if (focus->type == WidgetListBoxE) {
widgetListBoxOnKey(focus, key);
return;
}
// Handle button keyboard activation
if (focus->type == WidgetButtonE) {
if (key == ' ' || key == 0x0D) {
focus->as.button.pressed = true;
sKeyPressedBtn = focus;
wgtInvalidate(focus);
}
return;
}
// Handle image button keyboard activation
if (focus->type == WidgetImageButtonE) {
if (key == ' ' || key == 0x0D) {
focus->as.imageButton.pressed = true;
sKeyPressedBtn = focus;
wgtInvalidate(focus);
}
return;
}
// Handle checkbox keyboard toggle
if (focus->type == WidgetCheckboxE) {
if (key == ' ' || key == 0x0D) {
focus->as.checkbox.checked = !focus->as.checkbox.checked;
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
}
return;
}
// Handle radio button keyboard navigation
if (focus->type == WidgetRadioE) {
if (key == ' ' || key == 0x0D) {
// Select this radio
if (focus->parent && focus->parent->type == WidgetRadioGroupE) {
focus->parent->as.radioGroup.selectedIdx = focus->as.radio.index;
if (focus->parent->onChange) {
focus->parent->onChange(focus->parent);
}
}
wgtInvalidate(focus);
} else if (key == (0x50 | 0x100) || key == (0x4D | 0x100)) {
// Down or Right — next radio in group
if (focus->parent && focus->parent->type == WidgetRadioGroupE) {
WidgetT *next = NULL;
for (WidgetT *s = focus->nextSibling; s; s = s->nextSibling) {
if (s->type == WidgetRadioE && s->visible && s->enabled) {
next = s;
break;
}
}
if (next) {
focus->focused = false;
next->focused = true;
next->parent->as.radioGroup.selectedIdx = next->as.radio.index;
if (next->parent->onChange) {
next->parent->onChange(next->parent);
}
wgtInvalidate(next);
}
}
} else if (key == (0x48 | 0x100) || key == (0x4B | 0x100)) {
// Up or Left — previous radio in group
if (focus->parent && focus->parent->type == WidgetRadioGroupE) {
WidgetT *prev = NULL;
for (WidgetT *s = focus->parent->firstChild; s && s != focus; s = s->nextSibling) {
if (s->type == WidgetRadioE && s->visible && s->enabled) {
prev = s;
}
}
if (prev) {
focus->focused = false;
prev->focused = true;
prev->parent->as.radioGroup.selectedIdx = prev->as.radio.index;
if (prev->parent->onChange) {
prev->parent->onChange(prev->parent);
}
wgtInvalidate(prev);
}
}
}
return;
}
// Handle slider keyboard adjustment
if (focus->type == WidgetSliderE) {
int32_t step = 1;
int32_t range = focus->as.slider.maxValue - focus->as.slider.minValue;
if (range > 100) {
step = range / 100;
}
if (focus->as.slider.vertical) {
if (key == (0x48 | 0x100)) {
// Up — decrease value
focus->as.slider.value -= step;
} else if (key == (0x50 | 0x100)) {
// Down — increase value
focus->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
// Home — minimum
focus->as.slider.value = focus->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
// End — maximum
focus->as.slider.value = focus->as.slider.maxValue;
} else {
return;
}
} else {
if (key == (0x4B | 0x100)) {
// Left — decrease value
focus->as.slider.value -= step;
} else if (key == (0x4D | 0x100)) {
// Right — increase value
focus->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
// Home — minimum
focus->as.slider.value = focus->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
// End — maximum
focus->as.slider.value = focus->as.slider.maxValue;
} else {
return;
}
}
// Clamp
focus->as.slider.value = clampInt(focus->as.slider.value, focus->as.slider.minValue, focus->as.slider.maxValue);
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
return;
}
// Handle tab control keyboard navigation
if (focus->type == WidgetTabControlE) {
int32_t tabCount = 0;
for (WidgetT *c = focus->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTabPageE) {
tabCount++;
}
}
if (tabCount > 1) {
int32_t active = focus->as.tabControl.activeTab;
if (key == (0x4D | 0x100)) {
// Right — next tab (wrap)
active = (active + 1) % tabCount;
} else if (key == (0x4B | 0x100)) {
// Left — previous tab (wrap)
active = (active - 1 + tabCount) % tabCount;
} else if (key == (0x47 | 0x100)) {
// Home — first tab
active = 0;
} else if (key == (0x4F | 0x100)) {
// End — last tab
active = tabCount - 1;
} else {
return;
}
if (active != focus->as.tabControl.activeTab) {
if (sOpenPopup) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
focus->as.tabControl.activeTab = active;
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
}
}
return;
}
// Handle dropdown keyboard navigation
if (focus->type == WidgetDropdownE) {
if (focus->as.dropdown.open) {
// Popup is open — navigate items
if (key == (0x48 | 0x100)) {
// Up arrow
if (focus->as.dropdown.hoverIdx > 0) {
focus->as.dropdown.hoverIdx--;
if (focus->as.dropdown.hoverIdx < focus->as.dropdown.scrollPos) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx;
}
}
} else if (key == (0x50 | 0x100)) {
// Down arrow
if (focus->as.dropdown.hoverIdx < focus->as.dropdown.itemCount - 1) {
focus->as.dropdown.hoverIdx++;
if (focus->as.dropdown.hoverIdx >= focus->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
} else if (key == 0x0D || key == ' ') {
// Enter or Space — select item and close
focus->as.dropdown.selectedIdx = focus->as.dropdown.hoverIdx;
focus->as.dropdown.open = false;
sOpenPopup = NULL;
if (focus->onChange) {
focus->onChange(focus);
}
}
} else {
// Popup is closed
if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) {
// Down arrow, Space, or Enter — open popup
focus->as.dropdown.open = true;
focus->as.dropdown.hoverIdx = focus->as.dropdown.selectedIdx;
sOpenPopup = focus;
// Ensure scroll position shows the selected item
if (focus->as.dropdown.hoverIdx >= focus->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (focus->as.dropdown.hoverIdx < focus->as.dropdown.scrollPos) {
focus->as.dropdown.scrollPos = focus->as.dropdown.hoverIdx;
}
} else if (key == (0x48 | 0x100)) {
// Up arrow — select previous item without opening
if (focus->as.dropdown.selectedIdx > 0) {
focus->as.dropdown.selectedIdx--;
if (focus->onChange) {
focus->onChange(focus);
}
}
}
}
wgtInvalidate(focus);
return;
}
// Handle combobox popup keyboard navigation
if (focus->type == WidgetComboBoxE && focus->as.comboBox.open) {
if (key == (0x48 | 0x100)) {
// Up arrow
if (focus->as.comboBox.hoverIdx > 0) {
focus->as.comboBox.hoverIdx--;
if (focus->as.comboBox.hoverIdx < focus->as.comboBox.listScrollPos) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx;
}
}
wgtInvalidate(focus);
return;
}
if (key == (0x50 | 0x100)) {
// Down arrow
if (focus->as.comboBox.hoverIdx < focus->as.comboBox.itemCount - 1) {
focus->as.comboBox.hoverIdx++;
if (focus->as.comboBox.hoverIdx >= focus->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
wgtInvalidate(focus);
return;
}
if (key == 0x0D) {
// Enter — select item, copy text, close
int32_t idx = focus->as.comboBox.hoverIdx;
if (idx >= 0 && idx < focus->as.comboBox.itemCount) {
focus->as.comboBox.selectedIdx = idx;
const char *itemText = focus->as.comboBox.items[idx];
strncpy(focus->as.comboBox.buf, itemText, focus->as.comboBox.bufSize - 1);
focus->as.comboBox.buf[focus->as.comboBox.bufSize - 1] = '\0';
focus->as.comboBox.len = (int32_t)strlen(focus->as.comboBox.buf);
focus->as.comboBox.cursorPos = focus->as.comboBox.len;
focus->as.comboBox.scrollOff = 0;
}
focus->as.comboBox.open = false;
sOpenPopup = NULL;
if (focus->onChange) {
focus->onChange(focus);
}
wgtInvalidate(focus);
return;
}
}
// Down arrow on closed combobox opens the popup
if (focus->type == WidgetComboBoxE && !focus->as.comboBox.open && key == (0x50 | 0x100)) {
focus->as.comboBox.open = true;
focus->as.comboBox.hoverIdx = focus->as.comboBox.selectedIdx;
sOpenPopup = focus;
if (focus->as.comboBox.hoverIdx >= focus->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (focus->as.comboBox.hoverIdx < focus->as.comboBox.listScrollPos) {
focus->as.comboBox.listScrollPos = focus->as.comboBox.hoverIdx;
}
wgtInvalidate(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;
WidgetT *closedPopup = 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 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 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
closedPopup = 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 handlers
if (hit->type == WidgetTextInputE) {
widgetTextInputOnMouse(hit, root, vx);
}
if (hit->type == WidgetButtonE && hit->enabled) {
hit->focused = true;
widgetButtonOnMouse(hit);
}
if (hit->type == WidgetCheckboxE && hit->enabled) {
hit->focused = true;
widgetCheckboxOnMouse(hit);
}
if (hit->type == WidgetRadioE && hit->enabled) {
hit->focused = true;
widgetRadioOnMouse(hit);
}
if (hit->type == WidgetImageE && hit->enabled) {
widgetImageOnMouse(hit);
}
if (hit->type == WidgetImageButtonE && hit->enabled) {
hit->focused = true;
widgetImageButtonOnMouse(hit);
}
if (hit->type == WidgetCanvasE && hit->enabled) {
widgetCanvasOnMouse(hit, vx, vy);
}
if (hit->type == WidgetListBoxE && hit->enabled) {
widgetListBoxOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetDropdownE && hit->enabled) {
if (hit != closedPopup) {
widgetDropdownOnMouse(hit);
}
}
if (hit->type == WidgetComboBoxE && hit->enabled) {
if (hit != closedPopup || vx < hit->x + hit->w - DROPDOWN_BTN_WIDTH) {
widgetComboBoxOnMouse(hit, root, vx);
}
}
if (hit->type == WidgetSliderE && hit->enabled) {
hit->focused = true;
widgetSliderOnMouse(hit, vx, vy);
}
if (hit->type == WidgetTabControlE && hit->enabled) {
hit->focused = true;
widgetTabControlOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetTreeViewE && hit->enabled) {
hit->focused = true;
widgetTreeViewOnMouse(hit, root, vx, vy);
}
if (hit->type == WidgetAnsiTermE && hit->enabled) {
AppContextT *actx = (AppContextT *)root->userData;
widgetAnsiTermOnMouse(hit, vx, vy, &actx->font);
}
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);
}
}
}
}