570 lines
16 KiB
C
570 lines
16 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 && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE)) {
|
|
focus = w;
|
|
break;
|
|
}
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (top < 64) {
|
|
stack[top++] = c;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!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;
|
|
|
|
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 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
|
|
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) {
|
|
widgetButtonOnMouse(hit);
|
|
}
|
|
|
|
if (hit->type == WidgetCheckboxE && hit->enabled) {
|
|
widgetCheckboxOnMouse(hit);
|
|
}
|
|
|
|
if (hit->type == WidgetRadioE && hit->enabled) {
|
|
widgetRadioOnMouse(hit);
|
|
}
|
|
|
|
if (hit->type == WidgetImageE && hit->enabled) {
|
|
widgetImageOnMouse(hit);
|
|
}
|
|
|
|
if (hit->type == WidgetCanvasE && hit->enabled) {
|
|
widgetCanvasOnMouse(hit, vx, vy);
|
|
}
|
|
|
|
if (hit->type == WidgetDropdownE && hit->enabled) {
|
|
widgetDropdownOnMouse(hit);
|
|
}
|
|
|
|
if (hit->type == WidgetComboBoxE && hit->enabled) {
|
|
widgetComboBoxOnMouse(hit, root, vx);
|
|
}
|
|
|
|
if (hit->type == WidgetSliderE && hit->enabled) {
|
|
widgetSliderOnMouse(hit, vx, vy);
|
|
}
|
|
|
|
if (hit->type == WidgetTabControlE && hit->enabled) {
|
|
widgetTabControlOnMouse(hit, root, vx, vy);
|
|
}
|
|
|
|
if (hit->type == WidgetTreeViewE && hit->enabled) {
|
|
widgetTreeViewOnMouse(hit, root, vx, vy);
|
|
}
|
|
|
|
wgtInvalidate(root);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnPaint
|
|
// ============================================================
|
|
|
|
void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
|
(void)dirtyArea;
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
// Get context from root's userData
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
// Set up a display context pointing at the content buffer
|
|
DisplayT cd = ctx->display;
|
|
cd.backBuf = win->contentBuf;
|
|
cd.width = win->contentW;
|
|
cd.height = win->contentH;
|
|
cd.pitch = win->contentPitch;
|
|
cd.clipX = 0;
|
|
cd.clipY = 0;
|
|
cd.clipW = win->contentW;
|
|
cd.clipH = win->contentH;
|
|
|
|
// Clear background
|
|
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg);
|
|
|
|
// Apply scroll offset — layout at virtual size, positioned at -scroll
|
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
|
int32_t layoutW = DVX_MAX(win->contentW, root->calcMinW);
|
|
int32_t layoutH = DVX_MAX(win->contentH, root->calcMinH);
|
|
|
|
root->x = -scrollX;
|
|
root->y = -scrollY;
|
|
root->w = layoutW;
|
|
root->h = layoutH;
|
|
widgetLayoutChildren(root, &ctx->font);
|
|
|
|
// Paint widget tree (clip rect limits drawing to visible area)
|
|
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
|
|
|
|
// Paint overlay popups (dropdown/combobox)
|
|
widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnResize
|
|
// ============================================================
|
|
|
|
void widgetOnResize(WindowT *win, int32_t newW, int32_t newH) {
|
|
(void)newW;
|
|
(void)newH;
|
|
WidgetT *root = win->widgetRoot;
|
|
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
widgetManageScrollbars(win, ctx);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetOnScroll
|
|
// ============================================================
|
|
|
|
void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
|
(void)orient;
|
|
(void)value;
|
|
|
|
// Repaint with new scroll position
|
|
if (win->onPaint) {
|
|
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
|
win->onPaint(win, &fullRect);
|
|
}
|
|
}
|