DVX_GUI/core/widgetEvent.c

613 lines
19 KiB
C

#define DVX_WIDGET_IMPL
// widgetEvent.c -- Window event handlers for widget system
//
// This file routes window-level events (mouse, keyboard, paint, resize,
// scroll) into the widget tree. It serves as the bridge between the
// window manager (dvxWm) and the widget system.
//
// Event handling follows a priority system for mouse events:
// 1. Active drag/interaction states (slider drag, button press tracking,
// text selection, canvas drawing, column resize, drag-reorder,
// splitter drag) are checked first and handled directly.
// 2. Open popups (dropdown/combobox lists) intercept clicks next.
// 3. Normal hit testing routes clicks to the target widget.
//
// This priority ordering ensures that ongoing interactions complete
// correctly even if the mouse moves outside the originating widget.
// For example, dragging a slider and moving the mouse above the slider
// still adjusts the value, because sDragSlider captures the event
// before hit testing runs.
#include "dvxWgtP.h"
#include "platform/dvxPlat.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;
// Mouse state for tracking button transitions and movement
static int32_t sPrevMouseButtons = 0;
static int32_t sPrevMouseX = -1;
static int32_t sPrevMouseY = -1;
// ============================================================
// 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->window != win) {
return;
}
// Don't dispatch keys to disabled widgets
if (!focus->enabled) {
return;
}
// Attribute allocations during event handling to the owning app
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
wclsOnKey(focus, key, mod);
// Fire user callbacks after the widget's internal handler
if (key >= 32 && key < 127 && focus->onKeyPress) {
focus->onKeyPress(focus, key);
}
if (focus->onKeyDown) {
focus->onKeyDown(focus, key, mod);
}
ctx->currentAppId = prevAppId;
}
// ============================================================
// widgetOnKeyUp
// ============================================================
void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) {
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
WidgetT *focus = sFocusedWidget;
if (!focus || focus->window != win) {
return;
}
if (!focus->enabled) {
return;
}
if (focus->onKeyUp) {
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
focus->onKeyUp(focus, scancode, mod);
ctx->currentAppId = prevAppId;
}
}
// ============================================================
// widgetOnMouse
// ============================================================
//
// Main mouse event handler. This is the most complex event handler
// because it must manage multiple overlapping interaction states.
//
// The function is structured as a series of early-return checks:
// each active interaction (drag, press, popup) is checked in priority
// order. If the interaction handles the event, it returns immediately.
// Only if no interaction is active does the event fall through to
// normal hit testing.
//
// Coordinates (x, y) are in content-buffer space -- the window manager
// has already subtracted the window chrome offset. Widget positions
// are also in content-buffer space (set during layout), so no
// coordinate transform is needed for hit testing.
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons);
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
WidgetT *root = win->widgetRoot;
sClosedPopup = NULL;
if (!root) {
return;
}
// Attribute allocations during event handling to the owning app
AppContextT *ctx = (AppContextT *)root->userData;
int32_t prevAppId = ctx->currentAppId;
ctx->currentAppId = win->appId;
widgetOnMouseInner(win, root, x, y, buttons);
ctx->currentAppId = prevAppId;
}
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) {
// Close popups from other windows
if (sOpenPopup && sOpenPopup->window != win) {
wclsClosePopup(sOpenPopup);
sOpenPopup = NULL;
}
// Handle drag release
if (sDragWidget && !(buttons & MOUSE_LEFT)) {
wclsOnDragEnd(sDragWidget, root, x, y);
wgtInvalidatePaint(sDragWidget);
sDragWidget = NULL;
return;
}
// Handle drag move
if (sDragWidget && (buttons & MOUSE_LEFT)) {
wclsOnDragUpdate(sDragWidget, root, x, y);
// quickRepaint fast path for text drag (dirty rect instead of full repaint)
if (wclsHas(sDragWidget, WGT_METHOD_QUICK_REPAINT)) {
int32_t dirtyY = 0;
int32_t dirtyH = 0;
if (wclsQuickRepaint(sDragWidget, &dirtyY, &dirtyH) > 0) {
AppContextT *ctx = (AppContextT *)root->userData;
int32_t scrollY2 = win->vScroll ? win->vScroll->value : 0;
int32_t rectX = win->x + win->contentX;
int32_t rectY = win->y + win->contentY + dirtyY - scrollY2;
int32_t rectW = win->contentW;
win->contentDirty = true;
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
return;
}
}
// Scroll containers need full relayout
if (sDragWidget->wclass && (sDragWidget->wclass->flags & WCLASS_RELAYOUT_ON_SCROLL)) {
wgtInvalidate(sDragWidget);
} else {
wgtInvalidatePaint(sDragWidget);
}
return;
}
// Handle open popup clicks
if (sOpenPopup && (buttons & MOUSE_LEFT)) {
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t popX;
int32_t popY;
int32_t popW;
int32_t popH;
if (!wclsHas(sOpenPopup, WGT_METHOD_GET_POPUP_RECT)) {
sOpenPopup = NULL;
return;
}
wclsGetPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH);
if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) {
// Click on popup item -- dispatch to widget's onMouse
wclsOnMouse(sOpenPopup, root, x, y);
sOpenPopup = NULL;
wgtInvalidate(root);
return;
}
// Click outside popup -- close it and remember which widget it was
sClosedPopup = sOpenPopup;
wclsClosePopup(sOpenPopup);
sOpenPopup = NULL;
wgtInvalidatePaint(root);
// Fall through to normal click handling
}
if (!(buttons & MOUSE_LEFT)) {
sPrevMouseButtons = 0;
return;
}
// Widget positions are already in content-buffer space (widgetOnPaint
// sets root to -scrollX/-scrollY), so use raw content-relative coords.
int32_t vx = x;
int32_t vy = y;
WidgetT *hit = widgetHitTest(root, vx, vy);
if (!hit) {
return;
}
// Clear focus from the previously focused widget. This is done via
// the cached sFocusedWidget pointer rather than walking the tree to
// find the focused widget -- an O(1) operation vs O(n).
WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) {
wgtInvalidatePaint(sFocusedWidget);
sFocusedWidget = NULL;
}
// Dispatch to the hit widget's mouse handler via vtable. The handler
// is responsible for setting sFocusedWidget if it wants focus.
if (hit->enabled) {
wclsOnMouse(hit, root, vx, vy);
}
// Universal click/double-click callbacks -- fire for ALL widget types
// after the type-specific handler has run. Buttons and image buttons
// are excluded from onClick because they use press-release semantics
// (onClick fires on button-up, not button-down) and already handle it
// in the release handler above. They still get onDblClick here.
if (hit->enabled) {
int32_t clicks = multiClickDetect(vx, vy);
bool isBtn = (hit->wclass && (hit->wclass->flags & WCLASS_PRESS_RELEASE));
if (clicks >= 2 && hit->onDblClick) {
hit->onDblClick(hit);
} else if (!isBtn && hit->onClick) {
hit->onClick(hit);
}
}
// Fire mouse event callbacks (content-relative coordinates)
if (hit->enabled) {
int32_t relX = vx - hit->x - hit->contentOffX;
int32_t relY = vy - hit->y - hit->contentOffY;
// MouseDown: button just pressed
if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) {
if (hit->onMouseDown) {
hit->onMouseDown(hit, 1, relX, relY);
}
}
if ((buttons & MOUSE_RIGHT) && !(sPrevMouseButtons & MOUSE_RIGHT)) {
if (hit->onMouseDown) {
hit->onMouseDown(hit, 2, relX, relY);
}
}
if ((buttons & MOUSE_MIDDLE) && !(sPrevMouseButtons & MOUSE_MIDDLE)) {
if (hit->onMouseDown) {
hit->onMouseDown(hit, 3, relX, relY);
}
}
// MouseUp: button just released
if (!(buttons & MOUSE_LEFT) && (sPrevMouseButtons & MOUSE_LEFT)) {
if (hit->onMouseUp) {
hit->onMouseUp(hit, 1, relX, relY);
}
}
if (!(buttons & MOUSE_RIGHT) && (sPrevMouseButtons & MOUSE_RIGHT)) {
if (hit->onMouseUp) {
hit->onMouseUp(hit, 2, relX, relY);
}
}
if (!(buttons & MOUSE_MIDDLE) && (sPrevMouseButtons & MOUSE_MIDDLE)) {
if (hit->onMouseUp) {
hit->onMouseUp(hit, 3, relX, relY);
}
}
// MouseMove: position changed
if (vx != sPrevMouseX || vy != sPrevMouseY) {
if (hit->onMouseMove) {
hit->onMouseMove(hit, buttons, relX, relY);
}
}
}
sPrevMouseButtons = buttons;
sPrevMouseX = vx;
sPrevMouseY = vy;
// sFocusedWidget is now set directly by the widget's mouse handler
// Fire focus/blur callbacks on transitions
if (prevFocus && prevFocus != sFocusedWidget && prevFocus->onBlur) {
prevFocus->onBlur(prevFocus);
}
if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) {
sFocusedWidget->onFocus(sFocusedWidget);
}
wgtInvalidate(root);
}
// ============================================================
// widgetOnPaint
// ============================================================
//
// Paints the entire widget tree into the window's content buffer.
// Called whenever the window needs a redraw (invalidation, scroll,
// resize).
//
// Sets up a temporary DisplayT context that points at the window's
// content buffer instead of the screen backbuffer. This means all
// draw operations (drawText, rectFill, drawBevel, etc.) write
// directly into the per-window content buffer, which the compositor
// later blits to the screen backbuffer.
//
// Scroll offset is applied by shifting the root widget's position
// to negative coordinates (-scrollX, -scrollY). This elegantly makes
// scrolling work without any special scroll handling in individual
// widgets -- their positions are simply offset, and the clip rect
// on the DisplayT limits drawing to the visible area.
//
// The conditional re-layout avoids redundant layout passes when only
// the paint is needed (e.g. cursor blink, selection change).
void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
(void)dirtyArea;
WidgetT *root = win->widgetRoot;
if (!root) {
return;
}
// Get context from root's userData
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
// Set up a display context pointing at the content buffer
DisplayT cd = ctx->display;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
bool full = win->fullRepaint;
win->fullRepaint = false;
if (full) {
// Full repaint: clear background, relayout, paint everything
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);
root->x = -scrollX;
root->y = -scrollY;
root->w = layoutW;
root->h = layoutH;
if (full) {
widgetLayoutChildren(root, &ctx->font);
}
// Auto-focus first focusable widget if nothing has focus yet
if (!sFocusedWidget) {
WidgetT *first = widgetFindNextFocusable(root, NULL);
if (first) {
sFocusedWidget = first;
}
}
// Paint widget tree (full = all widgets, partial = only dirty ones)
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, full);
// 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);
}
}
}