DVX_GUI/core/widgetCore.c

1805 lines
53 KiB
C

// widgetCore.c -- Core widget infrastructure (alloc, tree ops, helpers)
//
// This file provides the foundation for the widget tree: allocation,
// parent-child linking, focus management, hit testing, and shared
// utility functions used across multiple widget types.
//
// Widgets form a tree using intrusive linked lists (firstChild/lastChild/
// nextSibling pointers inside each WidgetT). This is a singly-linked
// child list with a tail pointer for O(1) append. The tree is owned
// by its root, which is attached to a WindowT. Destroying the root
// recursively destroys all descendants.
//
// Memory allocation is plain malloc/free rather than an arena or pool.
// The widget count per window is typically small (tens to low hundreds),
// so the allocation overhead is negligible on target hardware. An arena
// approach was considered but rejected because widgets can be individually
// created and destroyed at runtime (dialog dynamics, tree item insertion),
// which doesn't map cleanly to an arena pattern.
#include "widgetInternal.h"
#include <ctype.h>
#include <time.h>
// ============================================================
// Global state for drag and popup tracking
// ============================================================
//
// These module-level pointers track ongoing UI interactions that span
// multiple mouse events (drags, popups, button presses). They are global
// rather than per-window because the DOS GUI is single-threaded and only
// one interaction can be active at a time.
//
// Each pointer is set when an interaction begins (e.g. mouse-down on a
// slider) and cleared when it ends (mouse-up). The event dispatcher in
// widgetEvent.c checks these before normal hit testing -- active drags
// take priority over everything else.
//
// All of these must be NULLed when the pointed-to widget is destroyed,
// otherwise dangling pointers would cause crashes. widgetDestroyChildren()
// and wgtDestroy() handle this cleanup.
clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint
bool sDebugLayout = false;
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
WidgetT *sPressedButton = NULL; // button being held down (tracks mouse in/out)
WidgetT *sDragSlider = NULL; // slider being dragged
WidgetT *sDrawingCanvas = NULL; // canvas receiving paint strokes
WidgetT *sDragTextSelect = NULL; // text widget in drag-select mode
int32_t sDragOffset = 0; // pixel offset from drag start to thumb center
WidgetT *sResizeListView = NULL; // ListView undergoing column resize
int32_t sResizeCol = -1; // which column is being resized
int32_t sResizeStartX = 0; // mouse X at resize start
int32_t sResizeOrigW = 0; // column width at resize start
bool sResizeDragging = false; // true once mouse moves during column resize
WidgetT *sDragSplitter = NULL; // splitter being dragged
int32_t sDragSplitStart = 0; // mouse offset from splitter edge at drag start
WidgetT *sDragReorder = NULL; // list/tree widget in drag-reorder mode
WidgetT *sDragScrollbar = NULL; // widget whose scrollbar thumb is being dragged
int32_t sDragScrollbarOff = 0; // mouse offset within thumb at drag start
int32_t sDragScrollbarOrient = 0; // 0=vertical, 1=horizontal
bool (*sListViewColBorderHitFn)(const WidgetT *w, int32_t vx, int32_t vy) = NULL;
void (*sSplitterClampPosFn)(WidgetT *w, int32_t *pos) = NULL;
WidgetT *(*sTreeViewNextVisibleFn)(WidgetT *item, WidgetT *treeView) = NULL;
// ============================================================
// widgetAddChild
// ============================================================
//
// Appends a child to the end of the parent's child list. O(1)
// thanks to the lastChild tail pointer. The child list is singly-
// linked (nextSibling), which saves 4 bytes per widget vs doubly-
// linked and is sufficient because child removal is infrequent.
void widgetAddChild(WidgetT *parent, WidgetT *child) {
child->parent = parent;
child->nextSibling = NULL;
if (parent->lastChild) {
parent->lastChild->nextSibling = child;
parent->lastChild = child;
} else {
parent->firstChild = child;
parent->lastChild = child;
}
}
// ============================================================
// widgetAlloc
// ============================================================
//
// Allocates and zero-initializes a new widget, links it to its
// class vtable via widgetClassTable[], and optionally adds it as
// a child of the given parent.
//
// The memset to 0 is intentional -- it establishes sane defaults
// for all fields: NULL pointers, zero coordinates, no focus,
// no accel key, etc. Only visible and enabled default to true.
//
// The window pointer is inherited from the parent so that any
// widget in the tree can find its owning window without walking
// to the root.
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) {
return NULL;
}
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
if (!w) {
return NULL;
}
memset(w, 0, sizeof(*w));
w->type = type;
w->wclass = widgetClassTable[type];
w->visible = true;
w->enabled = true;
if (parent) {
w->window = parent->window;
widgetAddChild(parent, w);
}
return w;
}
// ============================================================
// widgetClearFocus
// ============================================================
void widgetClearFocus(WidgetT *root) {
if (!root) {
return;
}
root->focused = false;
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
widgetClearFocus(c);
}
}
// ============================================================
// widgetCountVisibleChildren
// ============================================================
int32_t widgetCountVisibleChildren(const WidgetT *w) {
int32_t count = 0;
for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
count++;
}
}
return count;
}
// ============================================================
// widgetDestroyChildren
// ============================================================
//
// Recursively destroys all descendants of a widget. Processes
// children depth-first (destroy grandchildren before the child
// itself) so that per-widget destroy callbacks see a consistent
// tree state.
//
// Critically, this function clears all global state pointers that
// reference destroyed widgets. Without this, any pending drag or
// focus state would become a dangling pointer. Each global is
// checked individually rather than cleared unconditionally to
// avoid disrupting unrelated ongoing interactions.
void widgetDestroyChildren(WidgetT *w) {
WidgetT *child = w->firstChild;
while (child) {
WidgetT *next = child->nextSibling;
widgetDestroyChildren(child);
if (child->wclass && child->wclass->destroy) {
child->wclass->destroy(child);
}
// Clear static references if they point to destroyed widgets
if (sFocusedWidget == child) {
sFocusedWidget = NULL;
}
if (sOpenPopup == child) {
sOpenPopup = NULL;
}
if (sPressedButton == child) {
sPressedButton = NULL;
}
if (sDragSlider == child) {
sDragSlider = NULL;
}
if (sDrawingCanvas == child) {
sDrawingCanvas = NULL;
}
if (sResizeListView == child) {
sResizeListView = NULL;
sResizeCol = -1;
sResizeDragging = false;
}
if (sDragScrollbar == child) {
sDragScrollbar = NULL;
}
free(child);
child = next;
}
w->firstChild = NULL;
w->lastChild = NULL;
}
// ============================================================
// widgetDropdownPopupRect
// ============================================================
//
// Calculates the screen rectangle for a dropdown/combobox popup list.
// Shared between Dropdown and ComboBox since they have identical
// popup positioning logic.
//
// The popup tries to open below the widget first. If there isn't
// enough room (popup would extend past the content area bottom),
// it flips to open above instead. This ensures the popup is always
// visible, even for dropdowns near the bottom of a window.
//
// Popup height is capped at DROPDOWN_MAX_VISIBLE items to prevent
// huge popups from dominating the screen.
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
int32_t itemCount = 0;
if (w->type == WidgetDropdownE) {
itemCount = w->as.dropdown.itemCount;
} else if (w->type == WidgetComboBoxE) {
itemCount = w->as.comboBox.itemCount;
}
int32_t visibleItems = itemCount;
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
visibleItems = DROPDOWN_MAX_VISIBLE;
}
if (visibleItems < 1) {
visibleItems = 1;
}
*popX = w->x;
*popW = w->w;
*popH = visibleItems * font->charHeight + 4; // 2px border each side
// Try below first, then above if no room
if (w->y + w->h + *popH <= contentH) {
*popY = w->y + w->h;
} else {
*popY = w->y - *popH;
if (*popY < 0) {
*popY = 0;
}
}
}
// ============================================================
// widgetDrawDropdownArrow
// ============================================================
//
// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels
// wide across 4 rows) centered at the given position. Used by both
// Dropdown and ComboBox for the drop button arrow glyph.
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) {
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color);
}
}
// ============================================================
// widgetFindByAccel
// ============================================================
//
// Finds a widget with the given Alt+key accelerator. Recurses the
// tree depth-first, respecting visibility and enabled state.
//
// Special case for TabPage widgets: even if the tab page itself is
// not visible (inactive tab), its accelKey is still checked. This
// allows Alt+key to switch to a different tab. However, children
// of invisible tab pages are NOT searched -- their accelerators
// should not be active when the tab is hidden.
WidgetT *widgetFindByAccel(WidgetT *root, char key) {
if (!root || !root->enabled) {
return NULL;
}
// Invisible tab pages: match the page itself (for tab switching)
// but don't recurse into children (their accels shouldn't be active)
if (!root->visible) {
if (root->type == WidgetTabPageE && root->accelKey == key) {
return root;
}
return NULL;
}
if (root->accelKey == key) {
return root;
}
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
WidgetT *found = widgetFindByAccel(c, key);
if (found) {
return found;
}
}
return NULL;
}
// ============================================================
// widgetFindNextFocusable
// ============================================================
//
// Implements Tab-order navigation: finds the next focusable widget
// after 'after' in depth-first tree order. The two-pass approach
// (search from 'after' to end, then wrap to start) ensures circular
// tabbing -- Tab on the last focusable widget wraps to the first.
//
// The pastAfter flag tracks whether we've passed the 'after' widget
// during traversal. Once past it, the next focusable widget is the
// answer. This avoids collecting all focusable widgets into an array
// just to find the next one -- the common case returns quickly.
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
if (!w->visible || !w->enabled) {
return NULL;
}
if (after == NULL) {
*pastAfter = true;
}
if (w == after) {
*pastAfter = true;
} else if (*pastAfter && widgetIsFocusable(w->type)) {
return w;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
WidgetT *found = findNextFocusableImpl(c, after, pastAfter);
if (found) {
return found;
}
}
return NULL;
}
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
bool pastAfter = false;
WidgetT *found = findNextFocusableImpl(root, after, &pastAfter);
if (found) {
return found;
}
// Wrap around -- search from the beginning
pastAfter = true;
return findNextFocusableImpl(root, NULL, &pastAfter);
}
// ============================================================
// widgetFindPrevFocusable
// ============================================================
//
// Shift+Tab navigation: finds the previous focusable widget.
// Unlike findNextFocusable which can short-circuit during traversal,
// finding the PREVIOUS widget requires knowing the full order.
// So this collects all focusable widgets into an array, finds the
// target's index, and returns index-1 (with wraparound).
//
// The explicit stack-based DFS (rather than recursion) is used here
// because we need to push children in reverse order to get the same
// left-to-right depth-first ordering as the recursive version.
// Fixed-size arrays (128 widgets, 64 stack depth) are adequate for
// any reasonable dialog layout and avoid dynamic allocation.
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
WidgetT *list[128];
int32_t count = 0;
// Collect all focusable widgets via depth-first traversal
WidgetT *stack[64];
int32_t top = 0;
stack[top++] = root;
while (top > 0) {
WidgetT *w = stack[--top];
if (!w->visible || !w->enabled) {
continue;
}
if (widgetIsFocusable(w->type) && count < 128) {
list[count++] = w;
}
// Push children in reverse order so first child is processed first
WidgetT *children[64];
int32_t childCount = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (childCount < 64) {
children[childCount++] = c;
}
}
for (int32_t i = childCount - 1; i >= 0; i--) {
if (top < 64) {
stack[top++] = children[i];
}
}
}
if (count == 0) {
return NULL;
}
// Find 'before' in the list
int32_t idx = -1;
for (int32_t i = 0; i < count; i++) {
if (list[i] == before) {
idx = i;
break;
}
}
if (idx <= 0) {
return list[count - 1]; // Wrap to last
}
return list[idx - 1];
}
// ============================================================
// widgetFrameBorderWidth
// ============================================================
int32_t widgetFrameBorderWidth(const WidgetT *w) {
if (w->type != WidgetFrameE) {
return 0;
}
if (w->as.frame.style == FrameFlatE) {
return FRAME_FLAT_BORDER;
}
return FRAME_BEVEL_BORDER;
}
// ============================================================
// widgetHitTest
// ============================================================
//
// Recursive hit testing: finds the deepest (most specific) widget
// under the given coordinates. Returns the widget itself if no
// child is hit, or NULL if the point is outside this widget.
//
// Children are iterated front-to-back (first to last in the linked
// list), but the LAST match wins. This gives later siblings higher
// Z-order, which matches the painting order (later children paint
// on top of earlier ones). This is important for overlapping widgets,
// though in practice the layout engine rarely produces overlap.
//
// Widgets with WCLASS_NO_HIT_RECURSE stop the recursion -- the parent
// widget handles all mouse events for its children. This is used by
// TreeView, ScrollPane, ListView, and Splitter, which need to manage
// their own internal regions (scrollbars, column headers, tree
// expand buttons) that don't correspond to child widgets.
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
if (!w->visible) {
return NULL;
}
if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) {
return NULL;
}
// Widgets with WCLASS_NO_HIT_RECURSE manage their own children
if (w->wclass && (w->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
return w;
}
// Check children -- take the last match (topmost in Z-order)
WidgetT *hit = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
WidgetT *childHit = widgetHitTest(c, x, y);
if (childHit) {
hit = childHit;
}
}
return hit ? hit : w;
}
// ============================================================
// widgetIsFocusable
// ============================================================
bool widgetIsFocusable(WidgetTypeE type) {
if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) {
return false;
}
return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
}
// ============================================================
// widgetIsBoxContainer
// ============================================================
//
// Returns true for widget types that use the generic box layout.
bool widgetIsBoxContainer(WidgetTypeE type) {
if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) {
return false;
}
return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0;
}
// ============================================================
// widgetIsHorizContainer
// ============================================================
//
// Returns true for container types that lay out children horizontally.
bool widgetIsHorizContainer(WidgetTypeE type) {
if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) {
return false;
}
return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0;
}
// ============================================================
// widgetMaxItemLen
// ============================================================
//
// Scans an array of string items and returns the maximum strlen.
// Shared by ListBox, Dropdown, and ComboBox to cache the widest
// item length for calcMinSize without duplicating the loop.
int32_t widgetMaxItemLen(const char **items, int32_t count) {
int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) {
int32_t slen = (int32_t)strlen(items[i]);
if (slen > maxLen) {
maxLen = slen;
}
}
return maxLen;
}
// ============================================================
// widgetNavigateIndex
// ============================================================
//
// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic
// so each widget doesn't have to reimplement index clamping.
//
// Key values use the 0x100 flag to mark extended scan codes (arrow
// keys, Home, End, etc.) -- this is the DVX convention for passing
// scan codes through the same int32_t channel as ASCII values.
//
// Returns -1 for unrecognized keys so callers can check whether the
// key was consumed.
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
if (key == (0x50 | 0x100)) {
// Down arrow
if (current < count - 1) {
return current + 1;
}
return current < 0 ? 0 : current;
}
if (key == (0x48 | 0x100)) {
// Up arrow
if (current > 0) {
return current - 1;
}
return current < 0 ? 0 : current;
}
if (key == (0x47 | 0x100)) {
// Home
return 0;
}
if (key == (0x4F | 0x100)) {
// End
return count - 1;
}
if (key == (0x51 | 0x100)) {
// Page Down
int32_t n = current + pageSize;
return n >= count ? count - 1 : n;
}
if (key == (0x49 | 0x100)) {
// Page Up
int32_t n = current - pageSize;
return n < 0 ? 0 : n;
}
return -1;
}
// ============================================================
// widgetPaintPopupList
// ============================================================
//
// Shared popup list painting for Dropdown and ComboBox.
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
// Draw popup border
BevelStyleT bevel;
bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow;
bevel.face = colors->contentBg;
bevel.width = 2;
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
// Draw items
int32_t visibleItems = popH / font->charHeight;
int32_t textX = popX + TEXT_INPUT_PAD;
int32_t textY = popY + 2;
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
int32_t idx = scrollPos + i;
int32_t iy = textY + i * font->charHeight;
uint32_t ifg = colors->contentFg;
uint32_t ibg = colors->contentBg;
if (idx == hoverIdx) {
ifg = colors->menuHighlightFg;
ibg = colors->menuHighlightBg;
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
}
}
// ============================================================
// widgetScrollbarThumb
// ============================================================
//
// Calculates thumb position and size for a scrollbar track.
// Used by both the WM-level scrollbars and widget-internal scrollbars
// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior.
//
// The thumb size is proportional to visibleSize/totalSize -- a larger
// visible area means a larger thumb, giving visual feedback about how
// much content is scrollable. SB_MIN_THUMB prevents the thumb from
// becoming too small to grab with a mouse.
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
*thumbSize = (trackLen * visibleSize) / totalSize;
if (*thumbSize < SB_MIN_THUMB) {
*thumbSize = SB_MIN_THUMB;
}
if (*thumbSize > trackLen) {
*thumbSize = trackLen;
}
int32_t maxScroll = totalSize - visibleSize;
if (maxScroll > 0) {
*thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll;
} else {
*thumbPos = 0;
}
}
// ============================================================
// widgetRemoveChild
// ============================================================
//
// Unlinks a child from its parent's child list. O(n) in the number
// of children because the singly-linked list requires walking to
// find the predecessor. This is acceptable because child removal
// is infrequent (widget destruction, tree item reordering).
void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
WidgetT *prev = NULL;
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c == child) {
if (prev) {
prev->nextSibling = c->nextSibling;
} else {
parent->firstChild = c->nextSibling;
}
if (parent->lastChild == child) {
parent->lastChild = prev;
}
child->nextSibling = NULL;
child->parent = NULL;
return;
}
prev = c;
}
}
// ============================================================
// Shared text editing infrastructure
// ============================================================
//
// The following functions provide clipboard, multi-click detection,
// word boundary logic, cross-widget selection clearing, and the
// single-line text editing engine. They are shared across multiple
// widget DXEs (TextInput, TextArea, ComboBox, Spinner, AnsiTerm)
// and live here in the core library so all DXEs can link to them.
#define CLIPBOARD_MAX 4096
// Shared clipboard -- process-wide, not per-widget.
static char sClipboard[CLIPBOARD_MAX];
static int32_t sClipboardLen = 0;
// Multi-click state
static clock_t sLastClickTime = 0;
static int32_t sLastClickX = -1;
static int32_t sLastClickY = -1;
static int32_t sClickCount = 0;
// Track the widget that last had an active selection so we can
// clear it in O(1) instead of walking every widget in every window.
static WidgetT *sLastSelectedWidget = NULL;
// TextArea line helpers (static copies for widgetTextDragUpdate)
static int32_t textAreaCountLines(const char *buf, int32_t len);
static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col);
static int32_t textAreaGetLineCount(WidgetT *w);
static int32_t textAreaGetMaxLineLen(WidgetT *w);
static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row);
static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row);
static int32_t textAreaMaxLineLen(const char *buf, int32_t len);
// Shared undo/selection helpers
static bool clearSelectionOnWidget(WidgetT *w);
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
static int32_t wordBoundaryLeft(const char *buf, int32_t pos);
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
static bool clearSelectionOnWidget(WidgetT *w) {
if (w->type == WidgetTextInputE) {
if (w->as.textInput.selStart != w->as.textInput.selEnd) {
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
return true;
}
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
} else if (w->type == WidgetTextAreaE) {
if (w->as.textArea.selAnchor != w->as.textArea.selCursor) {
w->as.textArea.selAnchor = -1;
w->as.textArea.selCursor = -1;
return true;
}
w->as.textArea.selAnchor = -1;
w->as.textArea.selCursor = -1;
} else if (w->type == WidgetComboBoxE) {
if (w->as.comboBox.selStart != w->as.comboBox.selEnd) {
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
return true;
}
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
} else if (w->type == WidgetAnsiTermE) {
if (w->as.ansiTerm->selStartLine >= 0 &&
(w->as.ansiTerm->selStartLine != w->as.ansiTerm->selEndLine ||
w->as.ansiTerm->selStartCol != w->as.ansiTerm->selEndCol)) {
w->as.ansiTerm->dirtyRows = 0xFFFFFFFF;
w->as.ansiTerm->selStartLine = -1;
w->as.ansiTerm->selStartCol = -1;
w->as.ansiTerm->selEndLine = -1;
w->as.ansiTerm->selEndCol = -1;
w->as.ansiTerm->selecting = false;
return true;
}
w->as.ansiTerm->selStartLine = -1;
w->as.ansiTerm->selStartCol = -1;
w->as.ansiTerm->selEndLine = -1;
w->as.ansiTerm->selEndCol = -1;
w->as.ansiTerm->selecting = false;
}
return false;
}
// Clears selection on the previously-selected widget (if different
// from the newly-focused one). Validates that the previous widget's
// window is still in the window stack before touching it -- the
// window may have been closed since sLastSelectedWidget was set.
// If the previous widget was in a different window, that window
// gets a full repaint to clear the stale selection highlight.
void clearOtherSelections(WidgetT *except) {
if (!except || !except->window || !except->window->widgetRoot) {
return;
}
WidgetT *prev = sLastSelectedWidget;
sLastSelectedWidget = except;
if (!prev || prev == except) {
return;
}
// Verify the widget is still alive (its window still in the stack)
WindowT *prevWin = prev->window;
if (!prevWin) {
return;
}
AppContextT *ctx = wgtGetContext(except);
if (!ctx) {
return;
}
bool found = false;
for (int32_t i = 0; i < ctx->stack.count; i++) {
if (ctx->stack.windows[i] == prevWin) {
found = true;
break;
}
}
if (!found) {
return;
}
if (clearSelectionOnWidget(prev) && prevWin != except->window) {
dvxInvalidateWindow(ctx, prevWin);
}
}
void clipboardCopy(const char *text, int32_t len) {
if (!text || len <= 0) {
return;
}
if (len > CLIPBOARD_MAX - 1) {
len = CLIPBOARD_MAX - 1;
}
memcpy(sClipboard, text, len);
sClipboard[len] = '\0';
sClipboardLen = len;
}
const char *clipboardGet(int32_t *outLen) {
if (outLen) {
*outLen = sClipboardLen;
}
return sClipboard;
}
bool isWordChar(char c) {
return isalnum((unsigned char)c) || c == '_';
}
int32_t multiClickDetect(int32_t vx, int32_t vy) {
clock_t now = clock();
if ((now - sLastClickTime) < sDblClickTicks &&
abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) {
sClickCount++;
} else {
sClickCount = 1;
}
sLastClickTime = now;
sLastClickX = vx;
sLastClickY = vy;
return sClickCount;
}
// ============================================================
// TextArea line helpers (static copies for widgetTextDragUpdate)
// ============================================================
static int32_t textAreaCountLines(const char *buf, int32_t len) {
int32_t lines = 1;
for (int32_t i = 0; i < len; i++) {
if (buf[i] == '\n') {
lines++;
}
}
return lines;
}
static int32_t textAreaGetLineCount(WidgetT *w) {
if (w->as.textArea.cachedLines < 0) {
w->as.textArea.cachedLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len);
}
return w->as.textArea.cachedLines;
}
static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) {
(void)len;
int32_t off = 0;
for (int32_t r = 0; r < row; r++) {
while (off < len && buf[off] != '\n') {
off++;
}
if (off < len) {
off++;
}
}
return off;
}
static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) {
int32_t start = textAreaLineStart(buf, len, row);
int32_t end = start;
while (end < len && buf[end] != '\n') {
end++;
}
return end - start;
}
static int32_t textAreaMaxLineLen(const char *buf, int32_t len) {
int32_t maxLen = 0;
int32_t curLen = 0;
for (int32_t i = 0; i < len; i++) {
if (buf[i] == '\n') {
if (curLen > maxLen) {
maxLen = curLen;
}
curLen = 0;
} else {
curLen++;
}
}
if (curLen > maxLen) {
maxLen = curLen;
}
return maxLen;
}
static int32_t textAreaGetMaxLineLen(WidgetT *w) {
if (w->as.textArea.cachedMaxLL < 0) {
w->as.textArea.cachedMaxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len);
}
return w->as.textArea.cachedMaxLL;
}
static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col) {
int32_t start = textAreaLineStart(buf, len, row);
int32_t lineL = textAreaLineLen(buf, len, row);
int32_t clampC = col < lineL ? col : lineL;
return start + clampC;
}
// ============================================================
// Shared undo/selection helpers
// ============================================================
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize) {
if (!undoBuf) {
return;
}
int32_t copyLen = len < bufSize ? len : bufSize - 1;
memcpy(undoBuf, buf, copyLen);
undoBuf[copyLen] = '\0';
*pUndoLen = copyLen;
*pUndoCursor = cursor;
}
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) {
int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd;
int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart;
if (lo < 0) {
lo = 0;
}
if (hi > *pLen) {
hi = *pLen;
}
if (lo >= hi) {
*pSelStart = -1;
*pSelEnd = -1;
return;
}
memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo);
*pCursor = lo;
*pSelStart = -1;
*pSelEnd = -1;
}
// ============================================================
// Word boundary helpers
// ============================================================
static int32_t wordBoundaryLeft(const char *buf, int32_t pos) {
if (pos <= 0) {
return 0;
}
// Skip non-word characters
while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') {
pos--;
}
// Skip word characters
while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) {
pos--;
}
return pos;
}
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
if (pos >= len) {
return len;
}
// Skip word characters
while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) {
pos++;
}
// Skip non-word characters
while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') {
pos++;
}
return pos;
}
// ============================================================
// widgetTextDragUpdate -- update selection during mouse drag
// ============================================================
//
// Called by the event loop on mouse-move while sDragTextSelect is set.
// Extends the selection from the anchor to the current mouse position.
// Handles auto-scroll: when the mouse is past the widget edges, the
// scroll offset is nudged by one unit per event, creating a smooth
// scroll-while-dragging effect.
void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
if (w->type == WidgetTextInputE) {
int32_t leftEdge = w->x + TEXT_INPUT_PAD;
int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth;
widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.textInput.len, &w->as.textInput.cursorPos, &w->as.textInput.scrollOff, &w->as.textInput.selEnd);
} else if (w->type == WidgetTextAreaE) {
int32_t innerX = w->x + 2 + 2; // TEXTAREA_BORDER + TEXTAREA_PAD
int32_t innerY = w->y + 2; // TEXTAREA_BORDER
int32_t innerW = w->w - 2 * 2 - 2 * 2 - 14; // borders, pads, scrollbar
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = textAreaGetMaxLineLen(w);
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - 2 * 2 - (needHSb ? 14 : 0);
int32_t visRows = innerH / font->charHeight;
int32_t totalLines = textAreaGetLineCount(w);
if (visRows < 1) {
visRows = 1;
}
if (visCols < 1) {
visCols = 1;
}
// Auto-scroll vertically
if (vy < innerY && w->as.textArea.scrollRow > 0) {
w->as.textArea.scrollRow--;
} else if (vy >= innerY + visRows * font->charHeight && w->as.textArea.scrollRow + visRows < totalLines) {
w->as.textArea.scrollRow++;
}
// Auto-scroll horizontally
int32_t rightEdge = innerX + visCols * font->charWidth;
if (vx < innerX && w->as.textArea.scrollCol > 0) {
w->as.textArea.scrollCol--;
} else if (vx >= rightEdge && w->as.textArea.scrollCol < maxLL - visCols) {
w->as.textArea.scrollCol++;
}
int32_t relX = vx - innerX;
int32_t relY = vy - innerY;
int32_t clickRow = w->as.textArea.scrollRow + relY / font->charHeight;
int32_t clickCol = w->as.textArea.scrollCol + relX / font->charWidth;
if (clickRow < 0) {
clickRow = 0;
}
if (clickRow >= totalLines) {
clickRow = totalLines - 1;
}
if (clickCol < 0) {
clickCol = 0;
}
int32_t lineL = textAreaLineLen(w->as.textArea.buf, w->as.textArea.len, clickRow);
if (clickCol > lineL) {
clickCol = lineL;
}
w->as.textArea.cursorRow = clickRow;
w->as.textArea.cursorCol = clickCol;
w->as.textArea.desiredCol = clickCol;
w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol);
} else if (w->type == WidgetComboBoxE) {
int32_t leftEdge = w->x + TEXT_INPUT_PAD;
int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth;
widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.comboBox.len, &w->as.comboBox.cursorPos, &w->as.comboBox.scrollOff, &w->as.comboBox.selEnd);
} else if (w->type == WidgetAnsiTermE) {
int32_t baseX = w->x + 2; // ANSI_BORDER
int32_t baseY = w->y + 2;
int32_t cols = w->as.ansiTerm->cols;
int32_t rows = w->as.ansiTerm->rows;
int32_t clickRow = (vy - baseY) / font->charHeight;
int32_t clickCol = (vx - baseX) / font->charWidth;
if (clickRow < 0) {
clickRow = 0;
}
if (clickRow >= rows) {
clickRow = rows - 1;
}
if (clickCol < 0) {
clickCol = 0;
}
if (clickCol >= cols) {
clickCol = cols;
}
w->as.ansiTerm->selEndLine = w->as.ansiTerm->scrollPos + clickRow;
w->as.ansiTerm->selEndCol = clickCol;
w->as.ansiTerm->dirtyRows = 0xFFFFFFFF;
}
}
// ============================================================
// widgetTextEditDragUpdateLine
// ============================================================
//
// Called during mouse drag to extend the selection for single-line
// text widgets. Auto-scrolls when the mouse moves past the visible
// text edges.
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd) {
int32_t rightEdge = leftEdge + maxChars * font->charWidth;
if (vx < leftEdge && *pScrollOff > 0) {
(*pScrollOff)--;
} else if (vx >= rightEdge && *pScrollOff + maxChars < len) {
(*pScrollOff)++;
}
int32_t relX = vx - leftEdge;
int32_t charPos = relX / font->charWidth + *pScrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > len) {
charPos = len;
}
*pCursorPos = charPos;
*pSelEnd = charPos;
}
// ============================================================
// widgetTextEditMouseClick
// ============================================================
//
// Computes cursor position from pixel coordinates, handles multi-click
// (double = word select, triple = select all), and optionally starts
// drag-select.
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect) {
int32_t relX = vx - textLeftX;
int32_t charPos = relX / font->charWidth + scrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > len) {
charPos = len;
}
int32_t clicks = multiClickDetect(vx, vy);
if (clicks >= 3) {
*pSelStart = 0;
*pSelEnd = len;
*pCursorPos = len;
sDragTextSelect = NULL;
return;
}
if (clicks == 2) {
if (wordSelect && buf) {
int32_t ws = wordStart(buf, charPos);
int32_t we = wordEnd(buf, len, charPos);
*pSelStart = ws;
*pSelEnd = we;
*pCursorPos = we;
} else {
*pSelStart = 0;
*pSelEnd = len;
*pCursorPos = len;
}
sDragTextSelect = NULL;
return;
}
// Single click: place cursor
*pCursorPos = charPos;
*pSelStart = charPos;
*pSelEnd = charPos;
sDragTextSelect = dragSelect ? w : NULL;
}
// ============================================================
// widgetTextEditOnKey -- shared single-line text editing logic
// ============================================================
//
// This is the core single-line text editing engine, parameterized by
// pointer to allow reuse across TextInput, Spinner, and ComboBox.
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) {
bool shift = (mod & KEY_MOD_SHIFT) != 0;
bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1;
int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1;
// Clamp selection to buffer bounds
if (hasSel) {
if (selLo < 0) {
selLo = 0;
}
if (selHi > *pLen) {
selHi = *pLen;
}
if (selLo >= selHi) {
hasSel = false;
selLo = -1;
selHi = -1;
}
}
// Ctrl+A -- select all
if (key == 1 && pSelStart && pSelEnd) {
*pSelStart = 0;
*pSelEnd = *pLen;
*pCursor = *pLen;
goto adjustScroll;
}
// Ctrl+C -- copy
if (key == 3) {
if (hasSel) {
clipboardCopy(buf + selLo, selHi - selLo);
}
return;
}
// Ctrl+V -- paste
if (key == 22) {
if (sClipboardLen > 0) {
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
if (hasSel) {
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
}
int32_t canFit = bufSize - 1 - *pLen;
// For single-line, skip newlines in clipboard
int32_t paste = 0;
for (int32_t i = 0; i < sClipboardLen && paste < canFit; i++) {
if (sClipboard[i] != '\n' && sClipboard[i] != '\r') {
paste++;
}
}
if (paste > 0) {
int32_t pos = *pCursor;
memmove(buf + pos + paste, buf + pos, *pLen - pos + 1);
int32_t j = 0;
for (int32_t i = 0; i < sClipboardLen && j < paste; i++) {
if (sClipboard[i] != '\n' && sClipboard[i] != '\r') {
buf[pos + j] = sClipboard[i];
j++;
}
}
*pLen += paste;
*pCursor += paste;
}
if (w->onChange) {
w->onChange(w);
}
}
goto adjustScroll;
}
// Ctrl+X -- cut
if (key == 24) {
if (hasSel) {
clipboardCopy(buf + selLo, selHi - selLo);
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
if (w->onChange) {
w->onChange(w);
}
}
goto adjustScroll;
}
// Ctrl+Z -- undo
if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
// Swap current and undo
char tmpBuf[CLIPBOARD_MAX];
int32_t tmpLen = *pLen;
int32_t tmpCursor = *pCursor;
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;
memcpy(tmpBuf, buf, copyLen);
tmpBuf[copyLen] = '\0';
int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1;
memcpy(buf, undoBuf, restLen);
buf[restLen] = '\0';
*pLen = restLen;
int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
// Save old as new undo
int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
memcpy(undoBuf, tmpBuf, saveLen);
undoBuf[saveLen] = '\0';
*pUndoLen = saveLen;
*pUndoCursor = tmpCursor;
*pCursor = restoreCursor;
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
if (w->onChange) {
w->onChange(w);
}
goto adjustScroll;
}
if (key >= 32 && key < 127) {
// Printable character
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
if (hasSel) {
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
}
if (*pLen < bufSize - 1) {
int32_t pos = *pCursor;
memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
buf[pos] = (char)key;
(*pLen)++;
(*pCursor)++;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == 8) {
// Backspace
if (hasSel) {
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
if (w->onChange) {
w->onChange(w);
}
} else if (*pCursor > 0) {
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
int32_t pos = *pCursor;
memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
(*pLen)--;
(*pCursor)--;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == (0x4B | 0x100)) {
// Left arrow
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
if (*pCursor > 0) {
(*pCursor)--;
}
*pSelEnd = *pCursor;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
if (*pCursor > 0) {
(*pCursor)--;
}
}
} else if (key == (0x4D | 0x100)) {
// Right arrow
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
if (*pCursor < *pLen) {
(*pCursor)++;
}
*pSelEnd = *pCursor;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
if (*pCursor < *pLen) {
(*pCursor)++;
}
}
} else if (key == (0x73 | 0x100)) {
// Ctrl+Left -- word left
int32_t newPos = wordBoundaryLeft(buf, *pCursor);
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
*pCursor = newPos;
*pSelEnd = newPos;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
*pCursor = newPos;
}
} else if (key == (0x74 | 0x100)) {
// Ctrl+Right -- word right
int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor);
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
*pCursor = newPos;
*pSelEnd = newPos;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
*pCursor = newPos;
}
} else if (key == (0x47 | 0x100)) {
// Home
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
*pCursor = 0;
*pSelEnd = 0;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
*pCursor = 0;
}
} else if (key == (0x4F | 0x100)) {
// End
if (shift && pSelStart && pSelEnd) {
if (*pSelStart < 0) {
*pSelStart = *pCursor;
*pSelEnd = *pCursor;
}
*pCursor = *pLen;
*pSelEnd = *pLen;
} else {
if (pSelStart) {
*pSelStart = -1;
}
if (pSelEnd) {
*pSelEnd = -1;
}
*pCursor = *pLen;
}
} else if (key == (0x53 | 0x100)) {
// Delete
if (hasSel) {
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
if (w->onChange) {
w->onChange(w);
}
} else if (*pCursor < *pLen) {
if (undoBuf) {
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
}
int32_t pos = *pCursor;
memmove(buf + pos, buf + pos + 1, *pLen - pos);
(*pLen)--;
if (w->onChange) {
w->onChange(w);
}
}
} else {
return;
}
adjustScroll:
// Adjust scroll offset to keep cursor visible
{
AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font;
int32_t fieldW = w->w;
if (w->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;
}
}
wgtInvalidatePaint(w);
}
// ============================================================
// widgetTextEditPaintLine
// ============================================================
//
// Renders a single line of text with optional selection highlighting
// and a blinking cursor.
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX) {
// Normalize selection to low/high
int32_t selLo = -1;
int32_t selHi = -1;
if (selStart >= 0 && selEnd >= 0 && selStart != selEnd) {
selLo = selStart < selEnd ? selStart : selEnd;
selHi = selStart < selEnd ? selEnd : selStart;
}
// Map selection to visible range
int32_t visSelLo = selLo - scrollOff;
int32_t visSelHi = selHi - scrollOff;
if (visSelLo < 0) { visSelLo = 0; }
if (visSelHi > visLen) { visSelHi = visLen; }
if (selLo >= 0 && visSelLo < visSelHi) {
if (visSelLo > 0) {
drawTextN(d, ops, font, textX, textY, buf, visSelLo, fg, bg, true);
}
drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, buf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
if (visSelHi < visLen) {
drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, buf + visSelHi, visLen - visSelHi, fg, bg, true);
}
} else if (visLen > 0) {
drawTextN(d, ops, font, textX, textY, buf, visLen, fg, bg, true);
}
// Blinking cursor
if (showCursor && sCursorBlinkOn) {
int32_t cursorX = textX + (cursorPos - scrollOff) * font->charWidth;
if (cursorX >= cursorMinX && cursorX < cursorMaxX) {
drawVLine(d, ops, cursorX, textY, font->charHeight, fg);
}
}
}
int32_t wordEnd(const char *buf, int32_t len, int32_t pos) {
while (pos < len && isWordChar(buf[pos])) {
pos++;
}
return pos;
}
int32_t wordStart(const char *buf, int32_t pos) {
while (pos > 0 && isWordChar(buf[pos - 1])) {
pos--;
}
return pos;
}