File Open/Save dialogs, vertical and horizontal splitter panes, tooltips, disabled widgets, context menus, list/tree item drag reordering, multiselect treeview, keyboard accelerators, clipboard API. Whew.

This commit is contained in:
Scott Duensing 2026-03-16 17:46:37 -05:00
parent 705fa1e99a
commit 7129035bed
30 changed files with 2240 additions and 201 deletions

View file

@ -34,6 +34,7 @@ WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetRadio.c \ widgets/widgetRadio.c \
widgets/widgetScrollPane.c \ widgets/widgetScrollPane.c \
widgets/widgetSeparator.c \ widgets/widgetSeparator.c \
widgets/widgetSplitter.c \
widgets/widgetSlider.c \ widgets/widgetSlider.c \
widgets/widgetSpacer.c \ widgets/widgetSpacer.c \
widgets/widgetSpinner.c \ widgets/widgetSpinner.c \
@ -103,6 +104,7 @@ $(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS) $(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetScrollPane.o: widgets/widgetScrollPane.c $(WIDGET_DEPS) $(WOBJDIR)/widgetScrollPane.o: widgets/widgetScrollPane.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSplitter.o: widgets/widgetSplitter.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS)

View file

@ -17,12 +17,15 @@
#define KB_MOVE_STEP 8 #define KB_MOVE_STEP 8
#define MENU_CHECK_WIDTH 14 #define MENU_CHECK_WIDTH 14
#define SUBMENU_ARROW_WIDTH 12 #define SUBMENU_ARROW_WIDTH 12
#define TOOLTIP_DELAY_MS 500
#define TOOLTIP_PAD 3
// ============================================================ // ============================================================
// Prototypes // Prototypes
// ============================================================ // ============================================================
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph); static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers);
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx); static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
static void closeAllPopups(AppContextT *ctx); static void closeAllPopups(AppContextT *ctx);
static void closePopupLevel(AppContextT *ctx); static void closePopupLevel(AppContextT *ctx);
@ -38,6 +41,7 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons); static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void initColorScheme(AppContextT *ctx); static void initColorScheme(AppContextT *ctx);
static void initMouse(AppContextT *ctx); static void initMouse(AppContextT *ctx);
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx); static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
static void openSubMenu(AppContextT *ctx); static void openSubMenu(AppContextT *ctx);
static void openSysMenu(AppContextT *ctx, WindowT *win); static void openSysMenu(AppContextT *ctx, WindowT *win);
@ -47,6 +51,7 @@ static void pollKeyboard(AppContextT *ctx);
static void pollMouse(AppContextT *ctx); static void pollMouse(AppContextT *ctx);
static void refreshMinimizedIcons(AppContextT *ctx); static void refreshMinimizedIcons(AppContextT *ctx);
static void updateCursorShape(AppContextT *ctx); static void updateCursorShape(AppContextT *ctx);
static void updateTooltip(AppContextT *ctx);
// Button pressed via keyboard — shared with widgetEvent.c for Space/Enter // Button pressed via keyboard — shared with widgetEvent.c for Space/Enter
WidgetT *sKeyPressedBtn = NULL; WidgetT *sKeyPressedBtn = NULL;
@ -99,6 +104,54 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw
} }
// ============================================================
// checkAccelTable — test key against window's accelerator table
// ============================================================
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers) {
if (!win->accelTable || !win->onMenu) {
return false;
}
// Normalize Ctrl+letter: BIOS returns ASCII 0x01-0x1A for Ctrl+A..Z
// Map back to uppercase letter for matching
int32_t matchKey = key;
if ((modifiers & ACCEL_CTRL) && matchKey >= 0x01 && matchKey <= 0x1A) {
matchKey = matchKey + 'A' - 1;
}
// Uppercase for case-insensitive letter matching
if (matchKey >= 'a' && matchKey <= 'z') {
matchKey = matchKey - 32;
}
int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT);
AccelTableT *table = win->accelTable;
for (int32_t i = 0; i < table->count; i++) {
AccelEntryT *e = &table->entries[i];
// Uppercase the entry key too
int32_t entryKey = e->key;
if (entryKey >= 'a' && entryKey <= 'z') {
entryKey = entryKey - 32;
}
int32_t entryMods = e->modifiers & (ACCEL_CTRL | ACCEL_ALT);
if (entryKey == matchKey && entryMods == requiredMods) {
win->onMenu(win, e->cmdId);
return true;
}
}
(void)ctx;
return false;
}
// ============================================================ // ============================================================
// clickMenuCheckRadio — toggle check or select radio on click // clickMenuCheckRadio — toggle check or select radio on click
// ============================================================ // ============================================================
@ -305,7 +358,22 @@ static void compositeAndFlush(AppContextT *ctx) {
} }
} }
// 5. Draw cursor // 5. Draw tooltip
if (ctx->tooltipText) {
RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH };
RectT ttIsect;
if (rectIntersect(dr, &ttRect, &ttIsect)) {
rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg);
drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->colors.contentFg);
drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY + ctx->tooltipH - 1, ctx->tooltipW, ctx->colors.contentFg);
drawVLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg);
drawVLine(d, ops, ctx->tooltipX + ctx->tooltipW - 1, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg);
drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true);
}
}
// 6. Draw cursor
drawCursorAt(ctx, ctx->mouseX, ctx->mouseY); drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
// 6. Flush this dirty rect to LFB // 6. Flush this dirty rect to LFB
@ -692,7 +760,13 @@ static void dispatchEvents(AppContextT *ctx) {
} }
if (!inParent) { if (!inParent) {
// Check if mouse is on the menu bar (for switching top-level menus) if (ctx->popup.isContextMenu) {
// Context menu: any click outside closes it
if ((buttons & 1) && !(prevBtn & 1)) {
closeAllPopups(ctx);
}
} else {
// Menu bar popup: check if mouse is on the menu bar for switching
WindowT *win = findWindowById(ctx, ctx->popup.windowId); WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar) { if (win && win->menuBar) {
@ -716,7 +790,6 @@ static void dispatchEvents(AppContextT *ctx) {
} }
} }
} else if ((buttons & 1) && !(prevBtn & 1)) { } else if ((buttons & 1) && !(prevBtn & 1)) {
// Click outside all popups and menu bar — close everything
closeAllPopups(ctx); closeAllPopups(ctx);
} }
} else if ((buttons & 1) && !(prevBtn & 1)) { } else if ((buttons & 1) && !(prevBtn & 1)) {
@ -724,15 +797,61 @@ static void dispatchEvents(AppContextT *ctx) {
} }
} }
} }
}
return; return;
} }
// Handle button press // Handle left button press
if ((buttons & 1) && !(prevBtn & 1)) { if ((buttons & 1) && !(prevBtn & 1)) {
handleMouseButton(ctx, mx, my, buttons); handleMouseButton(ctx, mx, my, buttons);
} }
// Handle right button press — context menus
if ((buttons & 2) && !(prevBtn & 2)) {
int32_t hitPart;
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
if (hitIdx >= 0 && hitPart == 0) {
WindowT *win = ctx->stack.windows[hitIdx];
// Raise and focus if not already
if (hitIdx != ctx->stack.focusedIdx) {
wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
hitIdx = ctx->stack.count - 1;
wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
win = ctx->stack.windows[hitIdx];
}
// Check widget context menu first
MenuT *ctxMenu = NULL;
if (win->widgetRoot) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY);
// Walk up the tree to find a context menu
while (hit && !hit->contextMenu) {
hit = hit->parent;
}
if (hit) {
ctxMenu = hit->contextMenu;
}
}
// Fall back to window context menu
if (!ctxMenu) {
ctxMenu = win->contextMenu;
}
if (ctxMenu) {
openContextMenu(ctx, win, ctxMenu, mx, my);
}
}
}
// Handle button release on content — send to focused window // Handle button release on content — send to focused window
if (!(buttons & 1) && (prevBtn & 1)) { if (!(buttons & 1) && (prevBtn & 1)) {
if (ctx->stack.focusedIdx >= 0) { if (ctx->stack.focusedIdx >= 0) {
@ -886,6 +1005,50 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t
} }
// ============================================================
// dvxAddAccel
// ============================================================
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
if (!table || table->count >= MAX_ACCEL_ENTRIES) {
return;
}
AccelEntryT *e = &table->entries[table->count++];
e->key = key;
e->modifiers = modifiers;
e->cmdId = cmdId;
}
// ============================================================
// dvxClipboardCopy
// ============================================================
void dvxClipboardCopy(const char *text, int32_t len) {
clipboardCopy(text, len);
}
// ============================================================
// dvxClipboardGet
// ============================================================
const char *dvxClipboardGet(int32_t *outLen) {
return clipboardGet(outLen);
}
// ============================================================
// dvxCreateAccelTable
// ============================================================
AccelTableT *dvxCreateAccelTable(void) {
AccelTableT *table = (AccelTableT *)calloc(1, sizeof(AccelTableT));
return table;
}
// ============================================================ // ============================================================
// dvxDestroyWindow // dvxDestroyWindow
// ============================================================ // ============================================================
@ -939,6 +1102,15 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) {
} }
// ============================================================
// dvxFreeAccelTable
// ============================================================
void dvxFreeAccelTable(AccelTableT *table) {
free(table);
}
// ============================================================ // ============================================================
// dvxGetBlitOps // dvxGetBlitOps
// ============================================================ // ============================================================
@ -1101,6 +1273,7 @@ bool dvxUpdate(AppContextT *ctx) {
pollMouse(ctx); pollMouse(ctx);
pollKeyboard(ctx); pollKeyboard(ctx);
dispatchEvents(ctx); dispatchEvents(ctx);
updateTooltip(ctx);
pollAnsiTermWidgets(ctx); pollAnsiTermWidgets(ctx);
// Periodically refresh one minimized window thumbnail (staggered) // Periodically refresh one minimized window thumbnail (staggered)
@ -1466,6 +1639,45 @@ static void initMouse(AppContextT *ctx) {
} }
// ============================================================
// openContextMenu — open a context menu at a screen position
// ============================================================
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
if (!menu || menu->itemCount <= 0) {
return;
}
closeAllPopups(ctx);
closeSysMenu(ctx);
ctx->popup.active = true;
ctx->popup.isContextMenu = true;
ctx->popup.windowId = win->id;
ctx->popup.menuIdx = -1;
ctx->popup.menu = menu;
ctx->popup.hoverItem = -1;
ctx->popup.depth = 0;
calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);
// Position at mouse, clamped to screen
ctx->popup.popupX = screenX;
ctx->popup.popupY = screenY;
if (ctx->popup.popupX + ctx->popup.popupW > ctx->display.width) {
ctx->popup.popupX = ctx->display.width - ctx->popup.popupW;
}
if (ctx->popup.popupY + ctx->popup.popupH > ctx->display.height) {
ctx->popup.popupY = ctx->display.height - ctx->popup.popupH;
}
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
}
// ============================================================ // ============================================================
// openPopupAtMenu — open top-level popup for a menu bar menu // openPopupAtMenu — open top-level popup for a menu bar menu
// ============================================================ // ============================================================
@ -1481,6 +1693,7 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
MenuT *menu = &win->menuBar->menus[menuIdx]; MenuT *menu = &win->menuBar->menus[menuIdx];
ctx->popup.active = true; ctx->popup.active = true;
ctx->popup.isContextMenu = false;
ctx->popup.windowId = win->id; ctx->popup.windowId = win->id;
ctx->popup.menuIdx = menuIdx; ctx->popup.menuIdx = menuIdx;
ctx->popup.menu = menu; ctx->popup.menu = menu;
@ -2215,6 +2428,16 @@ static void pollKeyboard(AppContextT *ctx) {
continue; continue;
} }
// Check accelerator table on focused window
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
int32_t key = ascii ? ascii : (scancode | 0x100);
if (checkAccelTable(ctx, win, key, shiftFlags)) {
continue;
}
}
// Tab / Shift-Tab — cycle focus between widgets // Tab / Shift-Tab — cycle focus between widgets
// Tab: scancode=0x0F, ascii=0x09 // Tab: scancode=0x0F, ascii=0x09
// Shift-Tab: scancode=0x0F, ascii=0x00 // Shift-Tab: scancode=0x0F, ascii=0x00
@ -2438,6 +2661,10 @@ static void updateCursorShape(AppContextT *ctx) {
else if (sResizeListView) { else if (sResizeListView) {
newCursor = CURSOR_RESIZE_H; newCursor = CURSOR_RESIZE_H;
} }
// Active splitter drag
else if (sDragSplitter) {
newCursor = sDragSplitter->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
}
// Not in an active drag/resize — check what we're hovering // Not in an active drag/resize — check what we're hovering
else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) { else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) {
int32_t hitPart; int32_t hitPart;
@ -2479,6 +2706,36 @@ static void updateCursorShape(AppContextT *ctx) {
if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) { if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) {
newCursor = CURSOR_RESIZE_H; newCursor = CURSOR_RESIZE_H;
} }
// Walk into splitters (NO_HIT_RECURSE stops widgetHitTest at the outermost one)
while (hit && hit->type == WidgetSplitterE) {
int32_t pos = hit->as.splitter.dividerPos;
bool onBar;
if (hit->as.splitter.vertical) {
int32_t barX = hit->x + pos;
onBar = (vx >= barX && vx < barX + SPLITTER_BAR_W);
} else {
int32_t barY = hit->y + pos;
onBar = (vy >= barY && vy < barY + SPLITTER_BAR_W);
}
if (onBar) {
newCursor = hit->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
break;
}
// Not on this splitter's bar — check children for nested splitters
WidgetT *inner = NULL;
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
inner = c;
}
}
hit = inner;
}
} }
} }
} }
@ -2489,3 +2746,123 @@ static void updateCursorShape(AppContextT *ctx) {
ctx->cursorId = newCursor; ctx->cursorId = newCursor;
} }
} }
// ============================================================
// updateTooltip — show/hide tooltip based on hover state
// ============================================================
static void updateTooltip(AppContextT *ctx) {
clock_t now = clock();
clock_t threshold = (clock_t)TOOLTIP_DELAY_MS * CLOCKS_PER_SEC / 1000;
int32_t mx = ctx->mouseX;
int32_t my = ctx->mouseY;
// Mouse moved or button pressed — hide tooltip and reset timer
if (mx != ctx->prevMouseX || my != ctx->prevMouseY || ctx->mouseButtons) {
if (ctx->tooltipText) {
// Dirty old tooltip area
dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH);
ctx->tooltipText = NULL;
}
ctx->tooltipHoverStart = now;
return;
}
// Already showing a tooltip
if (ctx->tooltipText) {
return;
}
// Not enough time has passed
if ((now - ctx->tooltipHoverStart) < threshold) {
return;
}
// Don't show tooltips while popups/menus are active
if (ctx->popup.active || ctx->sysMenu.active) {
return;
}
// Find the widget under the cursor
int32_t hitPart;
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
if (hitIdx < 0 || hitPart != 0) {
return;
}
WindowT *win = ctx->stack.windows[hitIdx];
if (!win->widgetRoot) {
return;
}
int32_t cx = mx - win->x - win->contentX;
int32_t cy = my - win->y - win->contentY;
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t vx = cx + scrollX;
int32_t vy = cy + scrollY;
WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy);
// Walk into NO_HIT_RECURSE containers to find deepest child
while (hit && hit->wclass && (hit->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
WidgetT *inner = NULL;
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
inner = c;
}
}
if (!inner) {
break;
}
// If the inner child has a tooltip, use it; otherwise check if it's another container
if (inner->tooltip) {
hit = inner;
break;
}
if (inner->wclass && (inner->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
hit = inner;
} else {
WidgetT *deep = widgetHitTest(inner, vx, vy);
hit = deep ? deep : inner;
break;
}
}
if (!hit || !hit->tooltip) {
return;
}
// Show the tooltip
ctx->tooltipText = hit->tooltip;
int32_t tw = textWidth(&ctx->font, hit->tooltip) + TOOLTIP_PAD * 2;
int32_t th = ctx->font.charHeight + TOOLTIP_PAD * 2;
// Position below and right of cursor
ctx->tooltipX = mx + 12;
ctx->tooltipY = my + 16;
// Keep on screen
if (ctx->tooltipX + tw > ctx->display.width) {
ctx->tooltipX = ctx->display.width - tw;
}
if (ctx->tooltipY + th > ctx->display.height) {
ctx->tooltipY = my - th - 4;
}
ctx->tooltipW = tw;
ctx->tooltipH = th;
// Dirty the tooltip area
dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, tw, th);
}

View file

@ -45,6 +45,13 @@ typedef struct AppContextT {
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
void *idleCtx; void *idleCtx;
WindowT *modalWindow; // if non-NULL, only this window receives input WindowT *modalWindow; // if non-NULL, only this window receives input
// Tooltip state
clock_t tooltipHoverStart; // when mouse stopped moving
const char *tooltipText; // text to show (NULL = hidden)
int32_t tooltipX; // screen position
int32_t tooltipY;
int32_t tooltipW; // size (pre-computed)
int32_t tooltipH;
} AppContextT; } AppContextT;
// Initialize the application (VESA mode, input, etc.) // Initialize the application (VESA mode, input, etc.)
@ -102,4 +109,19 @@ const BlitOpsT *dvxGetBlitOps(const AppContextT *ctx);
// Load an icon for a window from an image file // Load an icon for a window from an image file
int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path); int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path);
// Create an accelerator table (caller must free with dvxFreeAccelTable)
AccelTableT *dvxCreateAccelTable(void);
// Free an accelerator table
void dvxFreeAccelTable(AccelTableT *table);
// Add an entry to an accelerator table
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId);
// Copy text to the shared clipboard
void dvxClipboardCopy(const char *text, int32_t len);
// Get clipboard contents (returns pointer to internal buffer, NULL if empty)
const char *dvxClipboardGet(int32_t *outLen);
#endif // DVX_APP_H #endif // DVX_APP_H

View file

@ -208,6 +208,48 @@ typedef struct {
int32_t length; // total length of scrollbar track int32_t length; // total length of scrollbar track
} ScrollbarT; } ScrollbarT;
// ============================================================
// Accelerator table (global hotkeys)
// ============================================================
#define MAX_ACCEL_ENTRIES 32
// Modifier flags for accelerators (match BIOS shift state bits)
#define ACCEL_SHIFT 0x03
#define ACCEL_CTRL 0x04
#define ACCEL_ALT 0x08
// Key codes for non-ASCII keys (scancode | 0x100)
#define KEY_F1 (0x3B | 0x100)
#define KEY_F2 (0x3C | 0x100)
#define KEY_F3 (0x3D | 0x100)
#define KEY_F4 (0x3E | 0x100)
#define KEY_F5 (0x3F | 0x100)
#define KEY_F6 (0x40 | 0x100)
#define KEY_F7 (0x41 | 0x100)
#define KEY_F8 (0x42 | 0x100)
#define KEY_F9 (0x43 | 0x100)
#define KEY_F10 (0x44 | 0x100)
#define KEY_F11 (0x57 | 0x100)
#define KEY_F12 (0x58 | 0x100)
#define KEY_INSERT (0x52 | 0x100)
#define KEY_DELETE (0x53 | 0x100)
#define KEY_HOME (0x47 | 0x100)
#define KEY_END (0x4F | 0x100)
#define KEY_PGUP (0x49 | 0x100)
#define KEY_PGDN (0x51 | 0x100)
typedef struct {
int32_t key; // key code: ASCII char or KEY_Fxx for extended
int32_t modifiers; // ACCEL_CTRL, ACCEL_SHIFT, ACCEL_ALT (OR'd together)
int32_t cmdId; // command ID passed to onMenu
} AccelEntryT;
typedef struct {
AccelEntryT entries[MAX_ACCEL_ENTRIES];
int32_t count;
} AccelTableT;
// ============================================================ // ============================================================
// Window // Window
// ============================================================ // ============================================================
@ -274,6 +316,12 @@ typedef struct WindowT {
// Widget tree root (NULL if no widgets) // Widget tree root (NULL if no widgets)
struct WidgetT *widgetRoot; struct WidgetT *widgetRoot;
// Context menu (NULL if none, caller owns the MenuT allocation)
MenuT *contextMenu;
// Accelerator table (NULL if none, caller owns allocation)
AccelTableT *accelTable;
// Callbacks // Callbacks
void *userData; void *userData;
void (*onPaint)(struct WindowT *win, RectT *dirtyArea); void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
@ -335,6 +383,7 @@ typedef struct {
typedef struct { typedef struct {
bool active; bool active;
bool isContextMenu; // true = context menu (no menu bar association)
int32_t windowId; // which window owns this popup chain int32_t windowId; // which window owns this popup chain
int32_t menuIdx; // which menu bar menu is open (top level) int32_t menuIdx; // which menu bar menu is open (top level)
int32_t popupX; // screen position of current (deepest) popup int32_t popupX; // screen position of current (deepest) popup

View file

@ -70,7 +70,8 @@ typedef enum {
WidgetAnsiTermE, WidgetAnsiTermE,
WidgetListViewE, WidgetListViewE,
WidgetSpinnerE, WidgetSpinnerE,
WidgetScrollPaneE WidgetScrollPaneE,
WidgetSplitterE
} WidgetTypeE; } WidgetTypeE;
// ============================================================ // ============================================================
@ -185,6 +186,8 @@ typedef struct WidgetT {
// User data and callbacks // User data and callbacks
void *userData; void *userData;
const char *tooltip; // tooltip text (NULL = none, caller owns string)
MenuT *contextMenu; // right-click context menu (NULL = none, caller owns)
void (*onClick)(struct WidgetT *w); void (*onClick)(struct WidgetT *w);
void (*onChange)(struct WidgetT *w); void (*onChange)(struct WidgetT *w);
void (*onDblClick)(struct WidgetT *w); void (*onDblClick)(struct WidgetT *w);
@ -256,6 +259,9 @@ typedef struct WidgetT {
bool multiSelect; bool multiSelect;
int32_t anchorIdx; // anchor for shift+click range selection int32_t anchorIdx; // anchor for shift+click range selection
uint8_t *selBits; // per-item selection flags (multi-select only) uint8_t *selBits; // per-item selection flags (multi-select only)
bool reorderable; // allow drag-reorder of items
int32_t dragIdx; // item being dragged (-1 = none)
int32_t dropIdx; // insertion point (-1 = none)
} listBox; } listBox;
struct { struct {
@ -312,6 +318,7 @@ typedef struct WidgetT {
struct { struct {
int32_t activeTab; int32_t activeTab;
int32_t scrollOffset; // horizontal scroll of tab headers
} tabControl; } tabControl;
struct { struct {
@ -322,11 +329,18 @@ typedef struct WidgetT {
int32_t scrollPos; int32_t scrollPos;
int32_t scrollPosH; int32_t scrollPosH;
struct WidgetT *selectedItem; struct WidgetT *selectedItem;
struct WidgetT *anchorItem; // anchor for shift+click range selection
bool multiSelect;
bool reorderable; // allow drag-reorder of items
struct WidgetT *dragItem; // item being dragged (NULL = none)
struct WidgetT *dropTarget; // insertion target (NULL = none)
bool dropAfter; // true = insert after target, false = before
} treeView; } treeView;
struct { struct {
const char *text; const char *text;
bool expanded; bool expanded;
bool selected; // per-item flag for multi-select
} treeItem; } treeItem;
struct { struct {
@ -424,6 +438,9 @@ typedef struct WidgetT {
bool multiSelect; bool multiSelect;
int32_t anchorIdx; int32_t anchorIdx;
uint8_t *selBits; uint8_t *selBits;
bool reorderable;
int32_t dragIdx;
int32_t dropIdx;
void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
} listView; } listView;
@ -448,6 +465,11 @@ typedef struct WidgetT {
int32_t scrollPosV; int32_t scrollPosV;
int32_t scrollPosH; int32_t scrollPosH;
} scrollPane; } scrollPane;
struct {
int32_t dividerPos; // pixels from left/top edge
bool vertical; // true = vertical divider (left|right panes)
} splitter;
} as; } as;
} WidgetT; } WidgetT;
@ -563,9 +585,13 @@ WidgetT *wgtToolbar(WidgetT *parent);
WidgetT *wgtTreeView(WidgetT *parent); WidgetT *wgtTreeView(WidgetT *parent);
WidgetT *wgtTreeViewGetSelected(const WidgetT *w); WidgetT *wgtTreeViewGetSelected(const WidgetT *w);
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item); void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item);
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi);
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable);
WidgetT *wgtTreeItem(WidgetT *parent, const char *text); WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded); void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
bool wgtTreeItemIsExpanded(const WidgetT *w); bool wgtTreeItemIsExpanded(const WidgetT *w);
bool wgtTreeItemIsSelected(const WidgetT *w);
void wgtTreeItemSetSelected(WidgetT *w, bool selected);
// ============================================================ // ============================================================
// ListView (multi-column list) // ListView (multi-column list)
@ -583,6 +609,7 @@ bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx);
void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected); void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected);
void wgtListViewSelectAll(WidgetT *w); void wgtListViewSelectAll(WidgetT *w);
void wgtListViewClearSelection(WidgetT *w); void wgtListViewClearSelection(WidgetT *w);
void wgtListViewSetReorderable(WidgetT *w, bool reorderable);
// ============================================================ // ============================================================
// ScrollPane // ScrollPane
@ -590,6 +617,16 @@ void wgtListViewClearSelection(WidgetT *w);
WidgetT *wgtScrollPane(WidgetT *parent); WidgetT *wgtScrollPane(WidgetT *parent);
// ============================================================
// Splitter (draggable divider between two child regions)
// ============================================================
// Create a splitter. If vertical is true, children are arranged left|right;
// if false, top|bottom. Add exactly two children.
WidgetT *wgtSplitter(WidgetT *parent, bool vertical);
void wgtSplitterSetPos(WidgetT *w, int32_t pos);
int32_t wgtSplitterGetPos(const WidgetT *w);
// ============================================================ // ============================================================
// ImageButton // ImageButton
// ============================================================ // ============================================================
@ -712,6 +749,15 @@ bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx);
void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected); void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected);
void wgtListBoxSelectAll(WidgetT *w); void wgtListBoxSelectAll(WidgetT *w);
void wgtListBoxClearSelection(WidgetT *w); void wgtListBoxClearSelection(WidgetT *w);
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable);
// ============================================================
// Tooltip
// ============================================================
// Set tooltip text for a widget (NULL to remove).
// Caller owns the string — it must outlive the widget.
static inline void wgtSetTooltip(WidgetT *w, const char *text) { w->tooltip = text; }
// ============================================================ // ============================================================
// Debug // Debug

View file

@ -1967,6 +1967,40 @@ void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
} }
// ============================================================
// wmSetTitle
// ============================================================
// ============================================================
// wmCreateMenu
// ============================================================
MenuT *wmCreateMenu(void) {
MenuT *m = (MenuT *)calloc(1, sizeof(MenuT));
return m;
}
// ============================================================
// wmFreeMenu
// ============================================================
void wmFreeMenu(MenuT *menu) {
if (!menu) {
return;
}
// Free submenus recursively
for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].subMenu) {
wmFreeMenu(menu->items[i].subMenu);
}
}
free(menu);
}
// ============================================================ // ============================================================
// wmSetTitle // wmSetTitle
// ============================================================ // ============================================================

View file

@ -122,4 +122,10 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win);
// Load an icon image for a window (converts to display pixel format) // Load an icon image for a window (converts to display pixel format)
int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d); int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d);
// Allocate a standalone menu (for use as a context menu). Free with wmFreeMenu().
MenuT *wmCreateMenu(void);
// Free a standalone menu allocated with wmCreateMenu()
void wmFreeMenu(MenuT *menu);
#endif // DVX_WM_H #endif // DVX_WM_H

View file

@ -55,8 +55,12 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
rectFill(d, ops, titleX - 2, titleY, rectFill(d, ops, titleX - 2, titleY,
titleW + 4, font->charHeight, bg); titleW + 4, font->charHeight, bg);
drawTextAccel(d, ops, font, titleX, titleY,
w->as.frame.title, fg, bg, true); if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, titleX, titleY, w->as.frame.title, colors);
} else {
drawTextAccel(d, ops, font, titleX, titleY, w->as.frame.title, fg, bg, true);
}
} }
} }

View file

@ -102,10 +102,11 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
textY++; textY++;
} }
drawTextAccel(d, ops, font, textX, textY, if (!w->enabled) {
w->as.button.text, drawTextAccelEmbossed(d, ops, font, textX, textY, w->as.button.text, colors);
w->enabled ? fg : colors->windowShadow, } else {
bgFace, true); drawTextAccel(d, ops, font, textX, textY, w->as.button.text, fg, bgFace, true);
}
if (w->focused) { if (w->focused) {
int32_t off = w->as.button.pressed ? 1 : 0; int32_t off = w->as.button.pressed ? 1 : 0;

View file

@ -108,10 +108,11 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t cx = w->x + 3; int32_t cx = w->x + 3;
int32_t cy = boxY + 3; int32_t cy = boxY + 3;
int32_t cs = CHECKBOX_BOX_SIZE - 6; int32_t cs = CHECKBOX_BOX_SIZE - 6;
uint32_t checkFg = w->enabled ? fg : colors->windowShadow;
for (int32_t i = 0; i < cs; i++) { for (int32_t i = 0; i < cs; i++) {
drawHLine(d, ops, cx + i, cy + i, 1, fg); drawHLine(d, ops, cx + i, cy + i, 1, checkFg);
drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, fg); drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, checkFg);
} }
} }
@ -119,7 +120,12 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP; int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
int32_t labelY = w->y + (w->h - font->charHeight) / 2; int32_t labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, w->as.checkbox.text); int32_t labelW = textWidthAccel(font, w->as.checkbox.text);
if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.checkbox.text, colors);
} else {
drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false); drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false);
}
if (w->focused) { if (w->focused) {
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);

View file

@ -383,6 +383,19 @@ static const WidgetClassT sClassScrollPane = {
.setText = NULL .setText = NULL
}; };
static const WidgetClassT sClassSplitter = {
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
.paint = widgetSplitterPaint,
.paintOverlay = NULL,
.calcMinSize = widgetSplitterCalcMinSize,
.layout = widgetSplitterLayout,
.onMouse = widgetSplitterOnMouse,
.onKey = NULL,
.destroy = NULL,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassSpinner = { static const WidgetClassT sClassSpinner = {
.flags = WCLASS_FOCUSABLE, .flags = WCLASS_FOCUSABLE,
.paint = widgetSpinnerPaint, .paint = widgetSpinnerPaint,
@ -430,5 +443,6 @@ const WidgetClassT *widgetClassTable[] = {
[WidgetAnsiTermE] = &sClassAnsiTerm, [WidgetAnsiTermE] = &sClassAnsiTerm,
[WidgetListViewE] = &sClassListView, [WidgetListViewE] = &sClassListView,
[WidgetSpinnerE] = &sClassSpinner, [WidgetSpinnerE] = &sClassSpinner,
[WidgetScrollPaneE] = &sClassScrollPane [WidgetScrollPaneE] = &sClassScrollPane,
[WidgetSplitterE] = &sClassSplitter
}; };

View file

@ -307,7 +307,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// ============================================================ // ============================================================
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken text area // Sunken text area
@ -362,7 +362,7 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
} }
// Draw cursor // Draw cursor
if (w->focused && !w->as.comboBox.open) { if (w->focused && w->enabled && !w->as.comboBox.open) {
int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth; int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth;
if (cursorX >= w->x + TEXT_INPUT_PAD && if (cursorX >= w->x + TEXT_INPUT_PAD &&
@ -381,11 +381,12 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel); drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
// Down arrow // Down arrow
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
int32_t arrowY = w->y + w->h / 2 - 1; int32_t arrowY = w->y + w->h / 2 - 1;
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg); drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
} }
} }

View file

@ -18,6 +18,9 @@ WidgetT *sResizeListView = NULL;
int32_t sResizeCol = -1; int32_t sResizeCol = -1;
int32_t sResizeStartX = 0; int32_t sResizeStartX = 0;
int32_t sResizeOrigW = 0; int32_t sResizeOrigW = 0;
WidgetT *sDragSplitter = NULL;
int32_t sDragSplitStart = 0;
WidgetT *sDragReorder = NULL;
// ============================================================ // ============================================================

View file

@ -210,9 +210,14 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
// Draw selected item text // Draw selected item text
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) { if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
drawText(d, ops, font, w->x + TEXT_INPUT_PAD, int32_t textX = w->x + TEXT_INPUT_PAD;
w->y + (w->h - font->charHeight) / 2, int32_t textY = w->y + (w->h - font->charHeight) / 2;
w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
if (!w->enabled) {
drawTextEmbossed(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], colors);
} else {
drawText(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
}
} }
// Drop button // Drop button
@ -224,11 +229,12 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel); drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
// Down arrow in button // Down arrow in button
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
int32_t arrowY = w->y + w->h / 2 - 1; int32_t arrowY = w->y + w->h / 2 - 1;
for (int32_t i = 0; i < 4; i++) { for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg); drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
} }
if (w->focused) { if (w->focused) {

View file

@ -121,6 +121,11 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return; return;
} }
// Don't dispatch keys to disabled widgets
if (!focus->enabled) {
return;
}
// Dispatch to per-widget onKey handler via vtable // Dispatch to per-widget onKey handler via vtable
if (focus->wclass && focus->wclass->onKey) { if (focus->wclass && focus->wclass->onKey) {
focus->wclass->onKey(focus, key, mod); focus->wclass->onKey(focus, key, mod);
@ -278,6 +283,52 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
return; return;
} }
// Handle drag-reorder release
if (sDragReorder && !(buttons & 1)) {
widgetReorderDrop(sDragReorder);
sDragReorder = NULL;
wgtInvalidatePaint(root);
return;
}
// Handle drag-reorder move
if (sDragReorder && (buttons & 1)) {
widgetReorderUpdate(sDragReorder, root, x, y);
wgtInvalidatePaint(root);
return;
}
// Handle splitter drag release
if (sDragSplitter && !(buttons & 1)) {
sDragSplitter = NULL;
return;
}
// Handle splitter drag
if (sDragSplitter && (buttons & 1)) {
int32_t pos;
if (sDragSplitter->as.splitter.vertical) {
pos = x - sDragSplitter->x - sDragSplitStart;
} else {
pos = y - sDragSplitter->y - sDragSplitStart;
}
widgetSplitterClampPos(sDragSplitter, &pos);
if (pos != sDragSplitter->as.splitter.dividerPos) {
sDragSplitter->as.splitter.dividerPos = pos;
if (sDragSplitter->onChange) {
sDragSplitter->onChange(sDragSplitter);
}
wgtInvalidate(sDragSplitter);
}
return;
}
// Handle button press release // Handle button press release
if (sPressedButton && !(buttons & 1)) { if (sPressedButton && !(buttons & 1)) {
if (sPressedButton->type == WidgetImageButtonE) { if (sPressedButton->type == WidgetImageButtonE) {
@ -537,3 +588,344 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
} }
} }
} }
// ============================================================
// widgetReorderDrop — finalize drag-reorder on mouse release
// ============================================================
void widgetReorderDrop(WidgetT *w) {
if (w->type == WidgetListBoxE) {
int32_t drag = w->as.listBox.dragIdx;
int32_t drop = w->as.listBox.dropIdx;
w->as.listBox.dragIdx = -1;
w->as.listBox.dropIdx = -1;
if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) {
return;
}
// Move item at dragIdx to before dropIdx
const char *temp = w->as.listBox.items[drag];
uint8_t selBit = 0;
if (w->as.listBox.multiSelect && w->as.listBox.selBits) {
selBit = w->as.listBox.selBits[drag];
}
if (drag < drop) {
for (int32_t i = drag; i < drop - 1; i++) {
w->as.listBox.items[i] = w->as.listBox.items[i + 1];
if (w->as.listBox.selBits) {
w->as.listBox.selBits[i] = w->as.listBox.selBits[i + 1];
}
}
w->as.listBox.items[drop - 1] = temp;
if (w->as.listBox.selBits) {
w->as.listBox.selBits[drop - 1] = selBit;
}
w->as.listBox.selectedIdx = drop - 1;
} else {
for (int32_t i = drag; i > drop; i--) {
w->as.listBox.items[i] = w->as.listBox.items[i - 1];
if (w->as.listBox.selBits) {
w->as.listBox.selBits[i] = w->as.listBox.selBits[i - 1];
}
}
w->as.listBox.items[drop] = temp;
if (w->as.listBox.selBits) {
w->as.listBox.selBits[drop] = selBit;
}
w->as.listBox.selectedIdx = drop;
}
if (w->onChange) {
w->onChange(w);
}
} else if (w->type == WidgetListViewE) {
int32_t drag = w->as.listView.dragIdx;
int32_t drop = w->as.listView.dropIdx;
int32_t colCnt = w->as.listView.colCount;
w->as.listView.dragIdx = -1;
w->as.listView.dropIdx = -1;
if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) {
return;
}
// Move row at dragIdx to before dropIdx
const char *temp[LISTVIEW_MAX_COLS];
for (int32_t c = 0; c < colCnt; c++) {
temp[c] = w->as.listView.cellData[drag * colCnt + c];
}
uint8_t selBit = 0;
if (w->as.listView.multiSelect && w->as.listView.selBits) {
selBit = w->as.listView.selBits[drag];
}
int32_t sortVal = 0;
if (w->as.listView.sortIndex) {
sortVal = w->as.listView.sortIndex[drag];
}
if (drag < drop) {
for (int32_t i = drag; i < drop - 1; i++) {
for (int32_t c = 0; c < colCnt; c++) {
w->as.listView.cellData[i * colCnt + c] = w->as.listView.cellData[(i + 1) * colCnt + c];
}
if (w->as.listView.selBits) {
w->as.listView.selBits[i] = w->as.listView.selBits[i + 1];
}
if (w->as.listView.sortIndex) {
w->as.listView.sortIndex[i] = w->as.listView.sortIndex[i + 1];
}
}
int32_t dest = drop - 1;
for (int32_t c = 0; c < colCnt; c++) {
w->as.listView.cellData[dest * colCnt + c] = temp[c];
}
if (w->as.listView.selBits) {
w->as.listView.selBits[dest] = selBit;
}
if (w->as.listView.sortIndex) {
w->as.listView.sortIndex[dest] = sortVal;
}
w->as.listView.selectedIdx = dest;
} else {
for (int32_t i = drag; i > drop; i--) {
for (int32_t c = 0; c < colCnt; c++) {
w->as.listView.cellData[i * colCnt + c] = w->as.listView.cellData[(i - 1) * colCnt + c];
}
if (w->as.listView.selBits) {
w->as.listView.selBits[i] = w->as.listView.selBits[i - 1];
}
if (w->as.listView.sortIndex) {
w->as.listView.sortIndex[i] = w->as.listView.sortIndex[i - 1];
}
}
for (int32_t c = 0; c < colCnt; c++) {
w->as.listView.cellData[drop * colCnt + c] = temp[c];
}
if (w->as.listView.selBits) {
w->as.listView.selBits[drop] = selBit;
}
if (w->as.listView.sortIndex) {
w->as.listView.sortIndex[drop] = sortVal;
}
w->as.listView.selectedIdx = drop;
}
if (w->onChange) {
w->onChange(w);
}
} else if (w->type == WidgetTreeViewE) {
WidgetT *drag = w->as.treeView.dragItem;
WidgetT *target = w->as.treeView.dropTarget;
bool after = w->as.treeView.dropAfter;
w->as.treeView.dragItem = NULL;
w->as.treeView.dropTarget = NULL;
if (!drag || !target || drag == target) {
return;
}
// Unlink drag from its current parent
WidgetT *oldParent = drag->parent;
if (oldParent->firstChild == drag) {
oldParent->firstChild = drag->nextSibling;
} else {
for (WidgetT *c = oldParent->firstChild; c; c = c->nextSibling) {
if (c->nextSibling == drag) {
c->nextSibling = drag->nextSibling;
break;
}
}
}
drag->nextSibling = NULL;
// Insert drag before or after target (same parent level)
WidgetT *newParent = target->parent;
drag->parent = newParent;
if (after) {
drag->nextSibling = target->nextSibling;
target->nextSibling = drag;
} else {
if (newParent->firstChild == target) {
drag->nextSibling = target;
newParent->firstChild = drag;
} else {
for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) {
if (c->nextSibling == target) {
c->nextSibling = drag;
drag->nextSibling = target;
break;
}
}
}
}
if (w->onChange) {
w->onChange(w);
}
}
}
// ============================================================
// widgetReorderUpdate — update drop position during drag
// ============================================================
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
(void)x;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
if (w->type == WidgetListBoxE) {
int32_t innerY = w->y + LISTBOX_BORDER;
int32_t innerH = w->h - LISTBOX_BORDER * 2;
int32_t visibleRows = innerH / font->charHeight;
int32_t maxScroll = w->as.listBox.itemCount - visibleRows;
if (maxScroll < 0) {
maxScroll = 0;
}
// Auto-scroll when dragging near edges
if (y < innerY + font->charHeight && w->as.listBox.scrollPos > 0) {
w->as.listBox.scrollPos--;
} else if (y > innerY + innerH - font->charHeight && w->as.listBox.scrollPos < maxScroll) {
w->as.listBox.scrollPos++;
}
int32_t relY = y - innerY;
int32_t row = w->as.listBox.scrollPos + relY / font->charHeight;
int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0;
int32_t drop = row + halfRow;
if (drop < 0) {
drop = 0;
}
if (drop > w->as.listBox.itemCount) {
drop = w->as.listBox.itemCount;
}
w->as.listBox.dropIdx = drop;
} else if (w->type == WidgetListViewE) {
int32_t headerH = font->charHeight + 4;
int32_t innerY = w->y + LISTVIEW_BORDER + headerH;
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
int32_t visibleRows = innerH / font->charHeight;
int32_t maxScroll = w->as.listView.rowCount - visibleRows;
if (maxScroll < 0) {
maxScroll = 0;
}
// Auto-scroll when dragging near edges
if (y < innerY + font->charHeight && w->as.listView.scrollPos > 0) {
w->as.listView.scrollPos--;
} else if (y > innerY + innerH - font->charHeight && w->as.listView.scrollPos < maxScroll) {
w->as.listView.scrollPos++;
}
int32_t relY = y - innerY;
int32_t row = w->as.listView.scrollPos + relY / font->charHeight;
int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0;
int32_t drop = row + halfRow;
if (drop < 0) {
drop = 0;
}
if (drop > w->as.listView.rowCount) {
drop = w->as.listView.rowCount;
}
w->as.listView.dropIdx = drop;
} else if (w->type == WidgetTreeViewE) {
int32_t innerY = w->y + TREE_BORDER;
int32_t innerH = w->h - TREE_BORDER * 2;
// Auto-scroll when dragging near edges (pixel-based scroll)
if (y < innerY + font->charHeight && w->as.treeView.scrollPos > 0) {
w->as.treeView.scrollPos -= font->charHeight;
if (w->as.treeView.scrollPos < 0) {
w->as.treeView.scrollPos = 0;
}
} else if (y > innerY + innerH - font->charHeight) {
w->as.treeView.scrollPos += font->charHeight;
// Paint will clamp to actual max
}
// Find which visible item the mouse is over
int32_t curY = w->y + TREE_BORDER - w->as.treeView.scrollPos;
// Walk visible items to find drop target
WidgetT *first = NULL;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTreeItemE && c->visible) {
first = c;
break;
}
}
WidgetT *target = NULL;
bool after = false;
for (WidgetT *v = first; v; v = widgetTreeViewNextVisible(v, w)) {
int32_t itemBot = curY + font->charHeight;
int32_t mid = curY + font->charHeight / 2;
if (y < mid) {
target = v;
after = false;
break;
}
curY = itemBot;
// Check if mouse is between this item and next
WidgetT *next = widgetTreeViewNextVisible(v, w);
if (!next || y < itemBot) {
target = v;
after = true;
break;
}
}
w->as.treeView.dropTarget = target;
w->as.treeView.dropAfter = after;
}
}

View file

@ -104,10 +104,11 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
(void)font; (void)font;
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
bool pressed = w->as.imageButton.pressed && w->enabled;
BevelStyleT bevel; BevelStyleT bevel;
bevel.highlight = w->as.imageButton.pressed ? colors->windowShadow : colors->windowHighlight; bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
bevel.shadow = w->as.imageButton.pressed ? colors->windowHighlight : colors->windowShadow; bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
bevel.face = bgFace; bevel.face = bgFace;
bevel.width = 2; bevel.width = 2;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
@ -116,7 +117,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
int32_t imgX = w->x + (w->w - w->as.imageButton.imgW) / 2; int32_t imgX = w->x + (w->w - w->as.imageButton.imgW) / 2;
int32_t imgY = w->y + (w->h - w->as.imageButton.imgH) / 2; int32_t imgY = w->y + (w->h - w->as.imageButton.imgH) / 2;
if (w->as.imageButton.pressed) { if (pressed) {
imgX++; imgX++;
imgY++; imgY++;
} }
@ -128,7 +129,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
if (w->focused) { if (w->focused) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
int32_t off = w->as.imageButton.pressed ? 1 : 0; int32_t off = pressed ? 1 : 0;
drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg); drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg);
} }
} }

View file

@ -63,6 +63,8 @@ extern const WidgetClassT *widgetClassTable[];
#define TAB_PAD_H 8 #define TAB_PAD_H 8
#define TAB_PAD_V 4 #define TAB_PAD_V 4
#define TAB_BORDER 2 #define TAB_BORDER 2
#define LISTBOX_BORDER 2
#define LISTVIEW_BORDER 2
#define TREE_INDENT 16 #define TREE_INDENT 16
#define TREE_EXPAND_SIZE 9 #define TREE_EXPAND_SIZE 9
#define TREE_ICON_GAP 4 #define TREE_ICON_GAP 4
@ -70,6 +72,8 @@ extern const WidgetClassT *widgetClassTable[];
#define TREE_SB_W 14 #define TREE_SB_W 14
#define TREE_MIN_ROWS 4 #define TREE_MIN_ROWS 4
#define SB_MIN_THUMB 14 #define SB_MIN_THUMB 14
#define SPLITTER_BAR_W 5
#define SPLITTER_MIN_PANE 20
// ============================================================ // ============================================================
// Inline helpers // Inline helpers
@ -81,6 +85,18 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
return val; return val;
} }
// Classic Windows 3.1 embossed (etched) text for disabled widgets:
// Draw text at +1,+1 in highlight, then at 0,0 in shadow.
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false);
}
static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
}
// ============================================================ // ============================================================
// Shared state (defined in widgetCore.c) // Shared state (defined in widgetCore.c)
// ============================================================ // ============================================================
@ -99,6 +115,9 @@ extern WidgetT *sResizeListView;
extern int32_t sResizeCol; extern int32_t sResizeCol;
extern int32_t sResizeStartX; extern int32_t sResizeStartX;
extern int32_t sResizeOrigW; extern int32_t sResizeOrigW;
extern WidgetT *sDragSplitter;
extern int32_t sDragSplitStart;
extern WidgetT *sDragReorder;
// ============================================================ // ============================================================
// Core functions (widgetCore.c) // Core functions (widgetCore.c)
@ -171,6 +190,7 @@ void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
@ -199,6 +219,7 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
@ -212,6 +233,7 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
// ============================================================ // ============================================================
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font); void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font);
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font);
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font); void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font); void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
@ -282,6 +304,8 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetSplitterClampPos(WidgetT *w, int32_t *pos);
void widgetSplitterOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod);
@ -301,4 +325,9 @@ int32_t wordStart(const char *buf, int32_t pos);
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
// Drag-reorder helpers (dispatch to ListBox/ListView/TreeView)
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
void widgetReorderDrop(WidgetT *w);
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView);
#endif // WIDGET_INTERNAL_H #endif // WIDGET_INTERNAL_H

View file

@ -53,9 +53,15 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t textY = w->y + (w->h - font->charHeight) / 2;
if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, w->x, textY, w->as.label.text, colors);
return;
}
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
drawTextAccel(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2, drawTextAccel(d, ops, font, w->x, textY, w->as.label.text, fg, bg, false);
w->as.label.text, fg, bg, false);
} }

View file

@ -5,7 +5,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#define LISTBOX_BORDER 2
#define LISTBOX_PAD 2 #define LISTBOX_PAD 2
#define LISTBOX_MIN_ROWS 4 #define LISTBOX_MIN_ROWS 4
#define LISTBOX_SB_W 14 #define LISTBOX_SB_W 14
@ -112,6 +111,8 @@ WidgetT *wgtListBox(WidgetT *parent) {
if (w) { if (w) {
w->as.listBox.selectedIdx = -1; w->as.listBox.selectedIdx = -1;
w->as.listBox.anchorIdx = -1; w->as.listBox.anchorIdx = -1;
w->as.listBox.dragIdx = -1;
w->as.listBox.dropIdx = -1;
} }
return w; return w;
@ -240,6 +241,19 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
} }
// ============================================================
// wgtListBoxSetReorderable
// ============================================================
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) {
if (!w || w->type != WidgetListBoxE) {
return;
}
w->as.listBox.reorderable = reorderable;
}
// ============================================================ // ============================================================
// wgtListBoxSetMultiSelect // wgtListBoxSetMultiSelect
// ============================================================ // ============================================================
@ -526,6 +540,13 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
hit->onDblClick(hit); hit->onDblClick(hit);
} }
// Initiate drag-reorder if enabled (not from modifier clicks)
if (hit->as.listBox.reorderable && !shift && !ctrl) {
hit->as.listBox.dragIdx = idx;
hit->as.listBox.dropIdx = idx;
sDragReorder = hit;
}
} }
@ -594,6 +615,17 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
} }
} }
// Draw drag-reorder insertion indicator
if (w->as.listBox.reorderable && w->as.listBox.dragIdx >= 0 && w->as.listBox.dropIdx >= 0) {
int32_t drop = w->as.listBox.dropIdx;
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
if (lineY >= innerY && lineY <= innerY + innerH) {
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY, contentW, colors->contentFg);
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY + 1, contentW, colors->contentFg);
}
}
// Draw scrollbar // Draw scrollbar
if (needSb) { if (needSb) {
int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W; int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W;

View file

@ -5,7 +5,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#define LISTVIEW_BORDER 2
#define LISTVIEW_PAD 3 #define LISTVIEW_PAD 3
#define LISTVIEW_SB_W 14 #define LISTVIEW_SB_W 14
#define LISTVIEW_MIN_ROWS 4 #define LISTVIEW_MIN_ROWS 4
@ -349,6 +348,8 @@ WidgetT *wgtListView(WidgetT *parent) {
w->as.listView.anchorIdx = -1; w->as.listView.anchorIdx = -1;
w->as.listView.sortCol = -1; w->as.listView.sortCol = -1;
w->as.listView.sortDir = ListViewSortNoneE; w->as.listView.sortDir = ListViewSortNoneE;
w->as.listView.dragIdx = -1;
w->as.listView.dropIdx = -1;
w->weight = 100; w->weight = 100;
} }
@ -506,6 +507,30 @@ void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
} }
// ============================================================
// wgtListViewSetReorderable
// ============================================================
void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
if (!w || w->type != WidgetListViewE) {
return;
}
w->as.listView.reorderable = reorderable;
// Disable sorting when reorderable — sort order conflicts with manual order
if (reorderable) {
w->as.listView.sortCol = -1;
w->as.listView.sortDir = ListViewSortNoneE;
if (w->as.listView.sortIndex) {
free(w->as.listView.sortIndex);
w->as.listView.sortIndex = NULL;
}
}
}
// ============================================================ // ============================================================
// wgtListViewSetMultiSelect // wgtListViewSetMultiSelect
// ============================================================ // ============================================================
@ -917,7 +942,8 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
colX += cw; colX += cw;
} }
// Not on a border — check for sort click // Not on a border — check for sort click (disabled when reorderable)
if (!hit->as.listView.reorderable) {
colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH; colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH;
for (int32_t c = 0; c < hit->as.listView.colCount; c++) { for (int32_t c = 0; c < hit->as.listView.colCount; c++) {
@ -948,6 +974,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
colX += cw; colX += cw;
} }
}
return; return;
} }
@ -1024,6 +1051,13 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
hit->onDblClick(hit); hit->onDblClick(hit);
} }
// Initiate drag-reorder if enabled (not from modifier clicks)
if (hit->as.listView.reorderable && !shift && !ctrl) {
hit->as.listView.dragIdx = dataRow;
hit->as.listView.dropIdx = dataRow;
sDragReorder = hit;
}
} }
wgtInvalidatePaint(hit); wgtInvalidatePaint(hit);
@ -1249,6 +1283,17 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
} }
} }
// Draw drag-reorder insertion indicator
if (w->as.listView.reorderable && w->as.listView.dragIdx >= 0 && w->as.listView.dropIdx >= 0) {
int32_t drop = w->as.listView.dropIdx;
int32_t lineY = dataY + (drop - w->as.listView.scrollPos) * font->charHeight;
if (lineY >= dataY && lineY <= dataY + innerH) {
drawHLine(d, ops, baseX, lineY, innerW, fg);
drawHLine(d, ops, baseX, lineY + 1, innerW, fg);
}
}
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
} }

View file

@ -318,6 +318,7 @@ void wgtSetDebugLayout(bool enabled) {
void wgtSetEnabled(WidgetT *w, bool enabled) { void wgtSetEnabled(WidgetT *w, bool enabled) {
if (w) { if (w) {
w->enabled = enabled; w->enabled = enabled;
wgtInvalidatePaint(w);
} }
} }

View file

@ -69,7 +69,7 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font; (void)font;
uint32_t fg = w->fgColor ? w->fgColor : colors->activeTitleBg; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->activeTitleBg) : colors->windowShadow;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken border // Sunken border

View file

@ -210,16 +210,25 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
// Draw filled diamond if selected // Draw filled diamond if selected
if (w->parent && w->parent->type == WidgetRadioGroupE && if (w->parent && w->parent->type == WidgetRadioGroupE &&
w->parent->as.radioGroup.selectedIdx == w->as.radio.index) { w->parent->as.radioGroup.selectedIdx == w->as.radio.index) {
for (int32_t i = -2; i <= 2; i++) { uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
int32_t span = 3 - (i < 0 ? -i : i);
drawHLine(d, ops, bx + mid - span, boxY + mid + i, span * 2 + 1, fg); static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
for (int32_t i = 0; i < 6; i++) {
int32_t dw = dotW[i];
drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
} }
} }
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP; int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
int32_t labelY = w->y + (w->h - font->charHeight) / 2; int32_t labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, w->as.radio.text); int32_t labelW = textWidthAccel(font, w->as.radio.text);
if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.radio.text, colors);
} else {
drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false); drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
}
if (w->focused) { if (w->focused) {
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);

View file

@ -195,6 +195,8 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font; (void)font;
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t tickFg = w->enabled ? fg : colors->windowShadow;
uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough;
int32_t range = w->as.slider.maxValue - w->as.slider.minValue; int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
@ -219,12 +221,12 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
BevelStyleT thumb; BevelStyleT thumb;
thumb.highlight = colors->windowHighlight; thumb.highlight = colors->windowHighlight;
thumb.shadow = colors->windowShadow; thumb.shadow = colors->windowShadow;
thumb.face = colors->buttonFace; thumb.face = thumbFg;
thumb.width = 2; thumb.width = 2;
drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb); drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);
// Center tick on thumb // Center tick on thumb
drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, fg); drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, tickFg);
} else { } else {
// Track groove // Track groove
int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2; int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
@ -242,12 +244,12 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
BevelStyleT thumb; BevelStyleT thumb;
thumb.highlight = colors->windowHighlight; thumb.highlight = colors->windowHighlight;
thumb.shadow = colors->windowShadow; thumb.shadow = colors->windowShadow;
thumb.face = colors->buttonFace; thumb.face = thumbFg;
thumb.width = 2; thumb.width = 2;
drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb); drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);
// Center tick on thumb // Center tick on thumb
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, fg); drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, tickFg);
} }
if (w->focused) { if (w->focused) {

View file

@ -292,7 +292,7 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
// ============================================================ // ============================================================
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
int32_t btnW = SPINNER_BTN_W; int32_t btnW = SPINNER_BTN_W;

View file

@ -0,0 +1,335 @@
// widgetSplitter.c — Splitter (draggable divider between two panes)
#include "widgetInternal.h"
// ============================================================
// Prototypes
// ============================================================
static WidgetT *spFirstChild(WidgetT *w);
static WidgetT *spSecondChild(WidgetT *w);
// ============================================================
// spFirstChild — get first visible child
// ============================================================
static WidgetT *spFirstChild(WidgetT *w) {
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
return c;
}
}
return NULL;
}
// ============================================================
// spSecondChild — get second visible child
// ============================================================
static WidgetT *spSecondChild(WidgetT *w) {
int32_t n = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
n++;
if (n == 2) {
return c;
}
}
}
return NULL;
}
// ============================================================
// widgetSplitterClampPos — clamp divider to child minimums
// ============================================================
void widgetSplitterClampPos(WidgetT *w, int32_t *pos) {
WidgetT *c1 = spFirstChild(w);
WidgetT *c2 = spSecondChild(w);
bool vert = w->as.splitter.vertical;
int32_t totalSize = vert ? w->w : w->h;
int32_t minFirst = c1 ? (vert ? c1->calcMinW : c1->calcMinH) : SPLITTER_MIN_PANE;
int32_t minSecond = c2 ? (vert ? c2->calcMinW : c2->calcMinH) : SPLITTER_MIN_PANE;
if (minFirst < SPLITTER_MIN_PANE) {
minFirst = SPLITTER_MIN_PANE;
}
if (minSecond < SPLITTER_MIN_PANE) {
minSecond = SPLITTER_MIN_PANE;
}
int32_t maxPos = totalSize - SPLITTER_BAR_W - minSecond;
if (maxPos < minFirst) {
maxPos = minFirst;
}
*pos = clampInt(*pos, minFirst, maxPos);
}
// ============================================================
// widgetSplitterCalcMinSize
// ============================================================
void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// Recursively measure children
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
widgetCalcMinSizeTree(c, font);
}
WidgetT *c1 = spFirstChild(w);
WidgetT *c2 = spSecondChild(w);
int32_t m1w = c1 ? c1->calcMinW : 0;
int32_t m1h = c1 ? c1->calcMinH : 0;
int32_t m2w = c2 ? c2->calcMinW : 0;
int32_t m2h = c2 ? c2->calcMinH : 0;
if (w->as.splitter.vertical) {
w->calcMinW = m1w + m2w + SPLITTER_BAR_W;
w->calcMinH = DVX_MAX(m1h, m2h);
} else {
w->calcMinW = DVX_MAX(m1w, m2w);
w->calcMinH = m1h + m2h + SPLITTER_BAR_W;
}
}
// ============================================================
// widgetSplitterLayout
// ============================================================
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
WidgetT *c1 = spFirstChild(w);
WidgetT *c2 = spSecondChild(w);
int32_t pos = w->as.splitter.dividerPos;
widgetSplitterClampPos(w, &pos);
w->as.splitter.dividerPos = pos;
if (w->as.splitter.vertical) {
// Left pane
if (c1) {
c1->x = w->x;
c1->y = w->y;
c1->w = pos;
c1->h = w->h;
widgetLayoutChildren(c1, font);
}
// Right pane
if (c2) {
c2->x = w->x + pos + SPLITTER_BAR_W;
c2->y = w->y;
c2->w = w->w - pos - SPLITTER_BAR_W;
c2->h = w->h;
if (c2->w < 0) {
c2->w = 0;
}
widgetLayoutChildren(c2, font);
}
} else {
// Top pane
if (c1) {
c1->x = w->x;
c1->y = w->y;
c1->w = w->w;
c1->h = pos;
widgetLayoutChildren(c1, font);
}
// Bottom pane
if (c2) {
c2->x = w->x;
c2->y = w->y + pos + SPLITTER_BAR_W;
c2->w = w->w;
c2->h = w->h - pos - SPLITTER_BAR_W;
if (c2->h < 0) {
c2->h = 0;
}
widgetLayoutChildren(c2, font);
}
}
}
// ============================================================
// widgetSplitterOnMouse
// ============================================================
void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
int32_t pos = hit->as.splitter.dividerPos;
// Check if click is on the divider bar
bool onDivider;
if (hit->as.splitter.vertical) {
int32_t barX = hit->x + pos;
onDivider = (vx >= barX && vx < barX + SPLITTER_BAR_W);
} else {
int32_t barY = hit->y + pos;
onDivider = (vy >= barY && vy < barY + SPLITTER_BAR_W);
}
if (onDivider) {
// Start dragging
sDragSplitter = hit;
if (hit->as.splitter.vertical) {
sDragSplitStart = vx - hit->x - pos;
} else {
sDragSplitStart = vy - hit->y - pos;
}
return;
}
// Forward click to child widgets
WidgetT *child = NULL;
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
WidgetT *ch = widgetHitTest(c, vx, vy);
if (ch) {
child = ch;
}
}
if (child && child->enabled && child->wclass && child->wclass->onMouse) {
if (sFocusedWidget && sFocusedWidget != child) {
sFocusedWidget->focused = false;
}
child->wclass->onMouse(child, root, vx, vy);
if (child->focused) {
sFocusedWidget = child;
}
}
wgtInvalidatePaint(hit);
}
// ============================================================
// widgetSplitterPaint
// ============================================================
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t pos = w->as.splitter.dividerPos;
// Paint first child with clip rect
WidgetT *c1 = spFirstChild(w);
WidgetT *c2 = spSecondChild(w);
int32_t oldClipX = d->clipX;
int32_t oldClipY = d->clipY;
int32_t oldClipW = d->clipW;
int32_t oldClipH = d->clipH;
if (c1) {
if (w->as.splitter.vertical) {
setClipRect(d, w->x, w->y, pos, w->h);
} else {
setClipRect(d, w->x, w->y, w->w, pos);
}
widgetPaintOne(c1, d, ops, font, colors);
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
}
if (c2) {
if (w->as.splitter.vertical) {
setClipRect(d, w->x + pos + SPLITTER_BAR_W, w->y, w->w - pos - SPLITTER_BAR_W, w->h);
} else {
setClipRect(d, w->x, w->y + pos + SPLITTER_BAR_W, w->w, w->h - pos - SPLITTER_BAR_W);
}
widgetPaintOne(c2, d, ops, font, colors);
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
}
// Draw divider bar — raised bevel
BevelStyleT bevel = BEVEL_RAISED(colors, 1);
if (w->as.splitter.vertical) {
int32_t barX = w->x + pos;
drawBevel(d, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel);
// Gripper — row of embossed 2x2 bumps centered vertically
int32_t gx = barX + 1;
int32_t midY = w->y + w->h / 2;
int32_t count = 5;
for (int32_t i = -count; i <= count; i++) {
int32_t dy = midY + i * 3;
drawHLine(d, ops, gx, dy, 2, colors->windowHighlight);
drawHLine(d, ops, gx + 1, dy + 1, 2, colors->windowShadow);
}
} else {
int32_t barY = w->y + pos;
drawBevel(d, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel);
// Gripper — row of embossed 2x2 bumps centered horizontally
int32_t gy = barY + 1;
int32_t midX = w->x + w->w / 2;
int32_t count = 5;
for (int32_t i = -count; i <= count; i++) {
int32_t dx = midX + i * 3;
drawHLine(d, ops, dx, gy, 1, colors->windowHighlight);
drawHLine(d, ops, dx + 1, gy, 1, colors->windowShadow);
drawHLine(d, ops, dx, gy + 1, 1, colors->windowHighlight);
drawHLine(d, ops, dx + 1, gy + 1, 1, colors->windowShadow);
}
}
}
// ============================================================
// wgtSplitter
// ============================================================
WidgetT *wgtSplitter(WidgetT *parent, bool vertical) {
WidgetT *w = widgetAlloc(parent, WidgetSplitterE);
if (w) {
w->as.splitter.vertical = vertical;
w->as.splitter.dividerPos = 0;
w->weight = 100;
}
return w;
}
// ============================================================
// wgtSplitterGetPos
// ============================================================
int32_t wgtSplitterGetPos(const WidgetT *w) {
return w->as.splitter.dividerPos;
}
// ============================================================
// wgtSplitterSetPos
// ============================================================
void wgtSplitterSetPos(WidgetT *w, int32_t pos) {
w->as.splitter.dividerPos = pos;
}

View file

@ -2,6 +2,118 @@
#include "widgetInternal.h" #include "widgetInternal.h"
#define TAB_ARROW_W 16
// ============================================================
// Prototypes
// ============================================================
static void tabClosePopup(void);
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font);
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font);
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font);
// ============================================================
// tabClosePopup — close any open dropdown/combobox popup
// ============================================================
static void tabClosePopup(void) {
if (sOpenPopup) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
}
// ============================================================
// tabEnsureVisible — scroll so active tab is visible
// ============================================================
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
if (!tabNeedScroll(w, font)) {
w->as.tabControl.scrollOffset = 0;
return;
}
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
if (headerW < 1) {
return;
}
// Find start and end X of the active tab
int32_t tabX = 0;
int32_t tabIdx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) {
continue;
}
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
if (tabIdx == w->as.tabControl.activeTab) {
int32_t tabLeft = tabX - w->as.tabControl.scrollOffset;
int32_t tabRight = tabLeft + tw;
if (tabLeft < 0) {
w->as.tabControl.scrollOffset += tabLeft;
} else if (tabRight > headerW) {
w->as.tabControl.scrollOffset += tabRight - headerW;
}
break;
}
tabX += tw;
tabIdx++;
}
// Clamp
int32_t totalW = tabHeaderTotalW(w, font);
int32_t maxOff = totalW - headerW;
if (maxOff < 0) {
maxOff = 0;
}
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
}
// ============================================================
// tabHeaderTotalW — total width of all tab headers
// ============================================================
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) {
int32_t total = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTabPageE) {
total += textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
}
}
return total;
}
// ============================================================
// tabNeedScroll — do tab headers overflow?
// ============================================================
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font) {
int32_t totalW = tabHeaderTotalW(w, font);
return totalW > (w->w - 4);
}
// ============================================================ // ============================================================
// wgtTabControl // wgtTabControl
@ -12,6 +124,7 @@ WidgetT *wgtTabControl(WidgetT *parent) {
if (w) { if (w) {
w->as.tabControl.activeTab = 0; w->as.tabControl.activeTab = 0;
w->as.tabControl.scrollOffset = 0;
w->weight = 100; w->weight = 100;
} }
@ -69,7 +182,6 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t tabH = font->charHeight + TAB_PAD_V * 2; int32_t tabH = font->charHeight + TAB_PAD_V * 2;
int32_t maxPageW = 0; int32_t maxPageW = 0;
int32_t maxPageH = 0; int32_t maxPageH = 0;
int32_t tabHeaderW = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTabPageE) { if (c->type != WidgetTabPageE) {
@ -79,9 +191,6 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
widgetCalcMinSizeTree(c, font); widgetCalcMinSizeTree(c, font);
maxPageW = DVX_MAX(maxPageW, c->calcMinW); maxPageW = DVX_MAX(maxPageW, c->calcMinW);
maxPageH = DVX_MAX(maxPageH, c->calcMinH); maxPageH = DVX_MAX(maxPageH, c->calcMinH);
int32_t labelW = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
tabHeaderW += labelW;
} }
w->calcMinW = maxPageW + TAB_BORDER * 2; w->calcMinW = maxPageW + TAB_BORDER * 2;
@ -103,6 +212,8 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
if (contentW < 0) { contentW = 0; } if (contentW < 0) { contentW = 0; }
if (contentH < 0) { contentH = 0; } if (contentH < 0) { contentH = 0; }
tabEnsureVisible(w, font);
int32_t idx = 0; int32_t idx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
@ -161,16 +272,7 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
if (active != w->as.tabControl.activeTab) { if (active != w->as.tabControl.activeTab) {
if (sOpenPopup) { tabClosePopup();
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
w->as.tabControl.activeTab = active; w->as.tabControl.activeTab = active;
if (w->onChange) { if (w->onChange) {
@ -193,8 +295,42 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
int32_t tabH = font->charHeight + TAB_PAD_V * 2; int32_t tabH = font->charHeight + TAB_PAD_V * 2;
// Only handle clicks in the tab header area // Only handle clicks in the tab header area
if (vy >= hit->y && vy < hit->y + tabH) { if (vy < hit->y || vy >= hit->y + tabH) {
int32_t tabX = hit->x + 2; return;
}
bool scroll = tabNeedScroll(hit, font);
// Check scroll arrow clicks
if (scroll) {
int32_t totalW = tabHeaderTotalW(hit, font);
int32_t headerW = hit->w - TAB_ARROW_W * 2 - 4;
int32_t maxOff = totalW - headerW;
if (maxOff < 0) {
maxOff = 0;
}
// Left arrow
if (vx >= hit->x && vx < hit->x + TAB_ARROW_W) {
hit->as.tabControl.scrollOffset -= font->charWidth * 4;
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
wgtInvalidatePaint(hit);
return;
}
// Right arrow
if (vx >= hit->x + hit->w - TAB_ARROW_W && vx < hit->x + hit->w) {
hit->as.tabControl.scrollOffset += font->charWidth * 4;
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
wgtInvalidatePaint(hit);
return;
}
}
// Click on tab header
int32_t headerLeft = hit->x + 2 + (scroll ? TAB_ARROW_W : 0);
int32_t tabX = headerLeft - hit->as.tabControl.scrollOffset;
int32_t tabIdx = 0; int32_t tabIdx = 0;
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
@ -204,19 +340,9 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2; int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
if (vx >= tabX && vx < tabX + tw) { if (vx >= tabX && vx < tabX + tw && vx >= headerLeft) {
if (tabIdx != hit->as.tabControl.activeTab) { if (tabIdx != hit->as.tabControl.activeTab) {
// Close any open dropdown/combobox popup tabClosePopup();
if (sOpenPopup) {
if (sOpenPopup->type == WidgetDropdownE) {
sOpenPopup->as.dropdown.open = false;
} else if (sOpenPopup->type == WidgetComboBoxE) {
sOpenPopup->as.comboBox.open = false;
}
sOpenPopup = NULL;
}
hit->as.tabControl.activeTab = tabIdx; hit->as.tabControl.activeTab = tabIdx;
if (hit->onChange) { if (hit->onChange) {
@ -231,7 +357,6 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
tabIdx++; tabIdx++;
} }
} }
}
// ============================================================ // ============================================================
@ -240,6 +365,7 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t tabH = font->charHeight + TAB_PAD_V * 2; int32_t tabH = font->charHeight + TAB_PAD_V * 2;
bool scroll = tabNeedScroll(w, font);
// Content panel // Content panel
BevelStyleT panelBevel; BevelStyleT panelBevel;
@ -249,8 +375,56 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
panelBevel.width = 2; panelBevel.width = 2;
drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel); drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel);
// Tab headers // Scroll arrows
int32_t tabX = w->x + 2; if (scroll) {
int32_t totalW = tabHeaderTotalW(w, font);
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
int32_t maxOff = totalW - headerW;
if (maxOff < 0) {
maxOff = 0;
}
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
uint32_t fg = colors->contentFg;
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
// Left arrow button
drawBevel(d, ops, w->x, w->y, TAB_ARROW_W, tabH, &btnBevel);
{
int32_t cx = w->x + TAB_ARROW_W / 2;
int32_t cy = w->y + tabH / 2;
for (int32_t i = 0; i < 4; i++) {
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
}
}
// Right arrow button
int32_t rx = w->x + w->w - TAB_ARROW_W;
drawBevel(d, ops, rx, w->y, TAB_ARROW_W, tabH, &btnBevel);
{
int32_t cx = rx + TAB_ARROW_W / 2;
int32_t cy = w->y + tabH / 2;
for (int32_t i = 0; i < 4; i++) {
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
}
}
}
// Tab headers — clip to header area
int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0);
int32_t headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w);
int32_t oldClipX = d->clipX;
int32_t oldClipY = d->clipY;
int32_t oldClipW = d->clipW;
int32_t oldClipH = d->clipH;
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2);
int32_t tabX = headerLeft - w->as.tabControl.scrollOffset;
int32_t tabIdx = 0; int32_t tabIdx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
@ -264,6 +438,8 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
int32_t th = isActive ? tabH + 2 : tabH; int32_t th = isActive ? tabH + 2 : tabH;
uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace; uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace;
// Only draw tabs that are at least partially visible
if (tabX + tw > headerLeft && tabX < headerRight) {
// Fill tab background // Fill tab background
rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace); rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace);
@ -295,17 +471,19 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
labelY++; labelY++;
} }
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, c->as.tabPage.title, colors->contentFg, tabFace, true);
c->as.tabPage.title, colors->contentFg, tabFace, true);
if (isActive && w->focused) { if (isActive && w->focused) {
drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg); drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg);
} }
}
tabX += tw; tabX += tw;
tabIdx++; tabIdx++;
} }
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
// Paint only active tab page's children // Paint only active tab page's children
tabIdx = 0; tabIdx = 0;

View file

@ -2224,7 +2224,7 @@ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// ============================================================ // ============================================================
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
// Sunken border // Sunken border
@ -2294,7 +2294,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
} }
// Draw cursor // Draw cursor
if (w->focused) { if (w->focused && w->enabled) {
int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth; int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth;
if (cursorX >= w->x + TEXT_INPUT_PAD && if (cursorX >= w->x + TEXT_INPUT_PAD &&

View file

@ -8,13 +8,16 @@
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font); static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font);
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth); static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth);
static void clearAllSelections(WidgetT *parent);
static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb); static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb);
static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH); static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH);
static WidgetT *firstVisibleItem(WidgetT *treeView); static WidgetT *firstVisibleItem(WidgetT *treeView);
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth); static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth);
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView); static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView);
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *selectedItem); static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t innerW);
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView);
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView); static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView);
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to);
static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb); static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb);
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font); static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font);
static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font); static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font);
@ -76,6 +79,25 @@ static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, i
} }
// ============================================================
// clearAllSelections
// ============================================================
static void clearAllSelections(WidgetT *parent) {
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE) {
continue;
}
c->as.treeItem.selected = false;
if (c->firstChild) {
clearAllSelections(c);
}
}
}
// ============================================================ // ============================================================
// drawTreeHScrollbar // drawTreeHScrollbar
// ============================================================ // ============================================================
@ -278,11 +300,42 @@ static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView) {
} }
// ============================================================
// paintReorderIndicator
// ============================================================
//
// Draw a 2px insertion line at the drop target position.
static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t innerW) {
if (!w->as.treeView.reorderable || !w->as.treeView.dropTarget || !w->as.treeView.dragItem) {
return;
}
int32_t dropY = treeItemYPos(w, w->as.treeView.dropTarget, font);
if (dropY < 0) {
return;
}
int32_t lineY = w->y + TREE_BORDER - w->as.treeView.scrollPos + dropY;
if (w->as.treeView.dropAfter) {
lineY += font->charHeight;
}
int32_t lineX = w->x + TREE_BORDER;
drawHLine(d, ops, lineX, lineY, innerW, colors->contentFg);
drawHLine(d, ops, lineX, lineY + 1, innerW, colors->contentFg);
}
// ============================================================ // ============================================================
// paintTreeItems // paintTreeItems
// ============================================================ // ============================================================
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *selectedItem) { static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView) {
bool multi = treeView->as.treeView.multiSelect;
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) { for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
if (c->type != WidgetTreeItemE || !c->visible) { if (c->type != WidgetTreeItemE || !c->visible) {
continue; continue;
@ -295,14 +348,21 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
// Skip items outside visible area // Skip items outside visible area
if (iy + font->charHeight <= clipTop || iy >= clipBottom) { if (iy + font->charHeight <= clipTop || iy >= clipBottom) {
if (c->as.treeItem.expanded && c->firstChild) { if (c->as.treeItem.expanded && c->firstChild) {
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, selectedItem); paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, treeView);
} }
continue; continue;
} }
// Highlight selected item // Highlight selected item(s)
bool isSelected = (c == selectedItem); bool isSelected;
if (multi) {
isSelected = c->as.treeItem.selected;
} else {
isSelected = (c == treeView->as.treeView.selectedItem);
}
uint32_t fg; uint32_t fg;
uint32_t bg; uint32_t bg;
@ -354,9 +414,15 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
// Draw text // Draw text
drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected); drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected);
// Draw focus rectangle around cursor item in multi-select mode
if (multi && c == treeView->as.treeView.selectedItem && treeView->focused) {
uint32_t focusFg = isSelected ? colors->menuHighlightFg : colors->contentFg;
drawFocusRect(d, ops, baseX, iy, d->clipW, font->charHeight, focusFg);
}
// Recurse into expanded children // Recurse into expanded children
if (c->as.treeItem.expanded && c->firstChild) { if (c->as.treeItem.expanded && c->firstChild) {
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, selectedItem); paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, treeView);
} }
} }
} }
@ -412,6 +478,53 @@ static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
} }
// ============================================================
// selectRange
// ============================================================
//
// Select all visible items between 'from' and 'to' (inclusive).
// Direction is auto-detected.
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to) {
if (!from || !to) {
return;
}
if (from == to) {
from->as.treeItem.selected = true;
return;
}
// Walk forward from 'from'. If we hit 'to', that's the direction.
// Otherwise, walk forward from 'to' to find 'from'.
WidgetT *start = from;
WidgetT *end = to;
// Check if 'to' comes after 'from'
bool forward = false;
for (WidgetT *v = nextVisibleItem(from, treeView); v; v = nextVisibleItem(v, treeView)) {
if (v == to) {
forward = true;
break;
}
}
if (!forward) {
start = to;
end = from;
}
for (WidgetT *v = start; v; v = nextVisibleItem(v, treeView)) {
v->as.treeItem.selected = true;
if (v == end) {
break;
}
}
}
// ============================================================ // ============================================================
// treeCalcScrollbarNeeds // treeCalcScrollbarNeeds
// ============================================================ // ============================================================
@ -630,6 +743,76 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
} }
w->as.treeView.selectedItem = item; w->as.treeView.selectedItem = item;
if (w->as.treeView.multiSelect && item) {
clearAllSelections(w);
item->as.treeItem.selected = true;
w->as.treeView.anchorItem = item;
}
}
// ============================================================
// wgtTreeViewSetMultiSelect
// ============================================================
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi) {
if (!w || w->type != WidgetTreeViewE) {
return;
}
w->as.treeView.multiSelect = multi;
}
// ============================================================
// wgtTreeViewSetReorderable
// ============================================================
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable) {
if (!w || w->type != WidgetTreeViewE) {
return;
}
w->as.treeView.reorderable = reorderable;
}
// ============================================================
// widgetTreeViewNextVisible
// ============================================================
//
// Non-static wrapper around nextVisibleItem for use by
// widgetReorderUpdate() in widgetEvent.c.
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) {
return nextVisibleItem(item, treeView);
}
// ============================================================
// wgtTreeItemIsSelected
// ============================================================
bool wgtTreeItemIsSelected(const WidgetT *w) {
if (!w || w->type != WidgetTreeItemE) {
return false;
}
return w->as.treeItem.selected;
}
// ============================================================
// wgtTreeItemSetSelected
// ============================================================
void wgtTreeItemSetSelected(WidgetT *w, bool selected) {
if (!w || w->type != WidgetTreeItemE) {
return;
}
w->as.treeItem.selected = selected;
} }
@ -650,12 +833,12 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
if (!w || w->type != WidgetTreeViewE) { if (!w || w->type != WidgetTreeViewE) {
return; return;
} }
bool multi = w->as.treeView.multiSelect;
bool shift = (mod & KEY_MOD_SHIFT) != 0;
WidgetT *sel = w->as.treeView.selectedItem; WidgetT *sel = w->as.treeView.selectedItem;
if (key == (0x50 | 0x100)) { if (key == (0x50 | 0x100)) {
@ -745,10 +928,33 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
} }
} }
} }
} else if (key == ' ' && multi) {
// Space — toggle selection of current item in multi-select
if (sel) {
sel->as.treeItem.selected = !sel->as.treeItem.selected;
w->as.treeView.anchorItem = sel;
}
} else { } else {
return; return;
} }
// Update multi-select state after Up/Down navigation
WidgetT *newSel = w->as.treeView.selectedItem;
if (multi && newSel != sel && newSel) {
if (shift && w->as.treeView.anchorItem) {
// Shift+arrow: range from anchor to new cursor
clearAllSelections(w);
selectRange(w, w->as.treeView.anchorItem, newSel);
}
// Plain arrow: just move cursor, leave selections untouched
}
// Set anchor on first selection if not set
if (multi && !w->as.treeView.anchorItem && newSel) {
w->as.treeView.anchorItem = newSel;
}
// Scroll to keep selected item visible // Scroll to keep selected item visible
sel = w->as.treeView.selectedItem; sel = w->as.treeView.selectedItem;
@ -786,7 +992,13 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) { void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
// Auto-select first item if nothing is selected // Auto-select first item if nothing is selected
if (!w->as.treeView.selectedItem) { if (!w->as.treeView.selectedItem) {
w->as.treeView.selectedItem = firstVisibleItem(w); WidgetT *first = firstVisibleItem(w);
w->as.treeView.selectedItem = first;
if (w->as.treeView.multiSelect && first) {
first->as.treeItem.selected = true;
w->as.treeView.anchorItem = first;
}
} }
int32_t totalH; int32_t totalH;
@ -942,11 +1154,33 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
return; return;
} }
// Set selection // Update selection
bool multi = hit->as.treeView.multiSelect;
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
hit->as.treeView.selectedItem = item; hit->as.treeView.selectedItem = item;
if (multi) {
if (ctrl) {
// Ctrl+click: toggle item, update anchor
item->as.treeItem.selected = !item->as.treeItem.selected;
hit->as.treeView.anchorItem = item;
} else if (shift && hit->as.treeView.anchorItem) {
// Shift+click: range from anchor to clicked
clearAllSelections(hit);
selectRange(hit, hit->as.treeView.anchorItem, item);
} else {
// Plain click: select only this item, update anchor
clearAllSelections(hit);
item->as.treeItem.selected = true;
hit->as.treeView.anchorItem = item;
}
}
// Check if click is on expand/collapse icon // Check if click is on expand/collapse icon
bool hasChildren = false; bool hasChildren = false;
bool clickedExpandIcon = false;
for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) { for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) {
if (gc->type == WidgetTreeItemE) { if (gc->type == WidgetTreeItemE) {
@ -968,6 +1202,7 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT - hit->as.treeView.scrollPosH; int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT - hit->as.treeView.scrollPosH;
if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) { if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) {
clickedExpandIcon = true;
item->as.treeItem.expanded = !item->as.treeItem.expanded; item->as.treeItem.expanded = !item->as.treeItem.expanded;
// Clamp scroll positions if collapsing reduced content size // Clamp scroll positions if collapsing reduced content size
@ -1009,6 +1244,13 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
item->onClick(item); item->onClick(item);
} }
} }
// Initiate drag-reorder if enabled (not from expand icon or modifier clicks)
if (hit->as.treeView.reorderable && !clickedExpandIcon && !shift && !ctrl) {
hit->as.treeView.dragItem = item;
hit->as.treeView.dropTarget = NULL;
sDragReorder = hit;
}
} }
@ -1061,7 +1303,10 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
paintTreeItems(w, d, ops, font, colors, paintTreeItems(w, d, ops, font, colors,
baseX, &itemY, 0, baseX, &itemY, 0,
w->y + TREE_BORDER, w->y + TREE_BORDER + innerH, w->y + TREE_BORDER, w->y + TREE_BORDER + innerH,
w->as.treeView.selectedItem); w);
// Draw drag-reorder insertion indicator (while still clipped)
paintReorderIndicator(w, d, ops, font, colors, innerW);
// Restore clip rect // Restore clip rect
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);

View file

@ -33,6 +33,12 @@
#define CMD_VIEW_SIZE_MED 308 #define CMD_VIEW_SIZE_MED 308
#define CMD_VIEW_SIZE_LARGE 309 #define CMD_VIEW_SIZE_LARGE 309
#define CMD_HELP_ABOUT 400 #define CMD_HELP_ABOUT 400
#define CMD_CTX_CUT 500
#define CMD_CTX_COPY 501
#define CMD_CTX_PASTE 502
#define CMD_CTX_DELETE 503
#define CMD_CTX_SELALL 504
#define CMD_CTX_PROPS 505
// ============================================================ // ============================================================
// Prototypes // Prototypes
@ -205,6 +211,30 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
"A DESQview/X-style windowing system for DOS.", "A DESQview/X-style windowing system for DOS.",
MB_OK | MB_ICONINFO); MB_OK | MB_ICONINFO);
break; break;
case CMD_CTX_CUT:
dvxMessageBox(ctx, "Context Menu", "Cut selected.", MB_OK | MB_ICONINFO);
break;
case CMD_CTX_COPY:
dvxMessageBox(ctx, "Context Menu", "Copied to clipboard.", MB_OK | MB_ICONINFO);
break;
case CMD_CTX_PASTE:
dvxMessageBox(ctx, "Context Menu", "Pasted from clipboard.", MB_OK | MB_ICONINFO);
break;
case CMD_CTX_DELETE:
dvxMessageBox(ctx, "Context Menu", "Deleted.", MB_OK | MB_ICONINFO);
break;
case CMD_CTX_SELALL:
dvxMessageBox(ctx, "Context Menu", "Selected all.", MB_OK | MB_ICONINFO);
break;
case CMD_CTX_PROPS:
dvxMessageBox(ctx, "Properties", "Window properties dialog would appear here.", MB_OK | MB_ICONINFO);
break;
} }
} }
@ -440,21 +470,25 @@ static void setupControlsWindow(AppContextT *ctx) {
WidgetT *dd = wgtDropdown(ddRow); WidgetT *dd = wgtDropdown(ddRow);
wgtDropdownSetItems(dd, colorItems, 6); wgtDropdownSetItems(dd, colorItems, 6);
wgtDropdownSetSelected(dd, 0); wgtDropdownSetSelected(dd, 0);
wgtSetTooltip(dd, "Choose a color");
WidgetT *cbRow = wgtHBox(page1); WidgetT *cbRow = wgtHBox(page1);
wgtLabel(cbRow, "Si&ze:"); wgtLabel(cbRow, "Si&ze:");
WidgetT *cb = wgtComboBox(cbRow, 32); WidgetT *cb = wgtComboBox(cbRow, 32);
wgtComboBoxSetItems(cb, sizeItems, 4); wgtComboBoxSetItems(cb, sizeItems, 4);
wgtComboBoxSetSelected(cb, 1); wgtComboBoxSetSelected(cb, 1);
wgtSetTooltip(cb, "Type or select a size");
wgtHSeparator(page1); wgtHSeparator(page1);
wgtLabel(page1, "&Progress:"); wgtLabel(page1, "&Progress:");
WidgetT *pb = wgtProgressBar(page1); WidgetT *pb = wgtProgressBar(page1);
wgtProgressBarSetValue(pb, 65); wgtProgressBarSetValue(pb, 65);
wgtSetTooltip(pb, "Task progress: 65%");
wgtLabel(page1, "&Volume:"); wgtLabel(page1, "&Volume:");
wgtSlider(page1, 0, 100); WidgetT *slider = wgtSlider(page1, 0, 100);
wgtSetTooltip(slider, "Adjust volume level");
WidgetT *spinRow = wgtHBox(page1); WidgetT *spinRow = wgtHBox(page1);
spinRow->maxH = wgtPixels(30); spinRow->maxH = wgtPixels(30);
@ -462,11 +496,13 @@ static void setupControlsWindow(AppContextT *ctx) {
WidgetT *spin = wgtSpinner(spinRow, 0, 999, 1); WidgetT *spin = wgtSpinner(spinRow, 0, 999, 1);
wgtSpinnerSetValue(spin, 42); wgtSpinnerSetValue(spin, 42);
spin->weight = 50; spin->weight = 50;
wgtSetTooltip(spin, "Enter quantity (0-999)");
// --- Tab 2: Tree --- // --- Tab 2: Tree ---
WidgetT *page2 = wgtTabPage(tabs, "&Tree"); WidgetT *page2 = wgtTabPage(tabs, "&Tree");
WidgetT *tree = wgtTreeView(page2); WidgetT *tree = wgtTreeView(page2);
wgtTreeViewSetReorderable(tree, true);
WidgetT *docs = wgtTreeItem(tree, "Documents"); WidgetT *docs = wgtTreeItem(tree, "Documents");
wgtTreeItemSetExpanded(docs, true); wgtTreeItemSetExpanded(docs, true);
wgtTreeItem(docs, "README.md"); wgtTreeItem(docs, "README.md");
@ -511,6 +547,7 @@ static void setupControlsWindow(AppContextT *ctx) {
wgtListViewSetData(lv, lvData, 10); wgtListViewSetData(lv, lvData, 10);
wgtListViewSetSelected(lv, 0); wgtListViewSetSelected(lv, 0);
wgtListViewSetMultiSelect(lv, true); wgtListViewSetMultiSelect(lv, true);
wgtListViewSetReorderable(lv, true);
lv->weight = 100; lv->weight = 100;
// --- Tab 4: ScrollPane --- // --- Tab 4: ScrollPane ---
@ -623,6 +660,131 @@ static void setupControlsWindow(AppContextT *ctx) {
wgtCanvasDrawLine(cv, 70, 5, 130, 70); wgtCanvasDrawLine(cv, 70, 5, 130, 70);
wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 0)); wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 0));
// --- Tab 8: Splitter ---
WidgetT *page8s = wgtTabPage(tabs, "S&plit");
// Outer horizontal splitter: explorer on top, detail on bottom
WidgetT *hSplit = wgtSplitter(page8s, false);
hSplit->weight = 100;
wgtSplitterSetPos(hSplit, 120);
// Top pane: vertical splitter (tree | list)
WidgetT *vSplit = wgtSplitter(hSplit, true);
wgtSplitterSetPos(vSplit, 120);
// Left pane: multi-select tree view
WidgetT *leftTree = wgtTreeView(vSplit);
wgtTreeViewSetMultiSelect(leftTree, true);
WidgetT *tiFolders = wgtTreeItem(leftTree, "Folders");
wgtTreeItemSetExpanded(tiFolders, true);
wgtTreeItem(tiFolders, "Documents");
wgtTreeItem(tiFolders, "Pictures");
wgtTreeItem(tiFolders, "Music");
WidgetT *tiSystem = wgtTreeItem(leftTree, "System");
wgtTreeItemSetExpanded(tiSystem, true);
wgtTreeItem(tiSystem, "Config");
wgtTreeItem(tiSystem, "Drivers");
// Right pane: list box
WidgetT *rightList = wgtListBox(vSplit);
static const char *explorerItems[] = {
"README.TXT", "SETUP.EXE", "CONFIG.SYS",
"AUTOEXEC.BAT", "COMMAND.COM", "HIMEM.SYS",
"EMM386.EXE", "MOUSE.COM"
};
wgtListBoxSetItems(rightList, explorerItems, 8);
// Bottom pane: detail/preview area
WidgetT *detailFrame = wgtFrame(hSplit, "Preview");
wgtLabel(detailFrame, "Select a file above to preview.");
// --- Tab 9: Disabled ---
WidgetT *page9d = wgtTabPage(tabs, "&Disabled");
// Left column: enabled widgets Right column: disabled widgets
WidgetT *disRow = wgtHBox(page9d);
disRow->weight = 100;
// Enabled column
WidgetT *enCol = wgtVBox(disRow);
enCol->weight = 100;
wgtLabel(enCol, "Enabled:");
wgtHSeparator(enCol);
wgtLabel(enCol, "A &label");
wgtButton(enCol, "A &button");
WidgetT *enChk = wgtCheckbox(enCol, "&Check me");
enChk->as.checkbox.checked = true;
WidgetT *enRg = wgtRadioGroup(enCol);
wgtRadio(enRg, "&Radio 1");
WidgetT *enR2 = wgtRadio(enRg, "R&adio 2");
enRg->as.radioGroup.selectedIdx = enR2->as.radio.index;
static const char *disItems[] = {"Alpha", "Beta", "Gamma"};
WidgetT *enDd = wgtDropdown(enCol);
wgtDropdownSetItems(enDd, disItems, 3);
wgtDropdownSetSelected(enDd, 0);
WidgetT *enSlider = wgtSlider(enCol, 0, 100);
wgtSliderSetValue(enSlider, 40);
WidgetT *enProg = wgtProgressBar(enCol);
wgtProgressBarSetValue(enProg, 60);
WidgetT *enSpin = wgtSpinner(enCol, 0, 100, 1);
wgtSpinnerSetValue(enSpin, 42);
WidgetT *enText = wgtTextInput(enCol, 64);
wgtSetText(enText, "Editable");
// Disabled column
WidgetT *disCol = wgtVBox(disRow);
disCol->weight = 100;
wgtLabel(disCol, "Disabled:");
wgtHSeparator(disCol);
WidgetT *dLbl = wgtLabel(disCol, "A &label");
wgtSetEnabled(dLbl, false);
WidgetT *dBtn = wgtButton(disCol, "A &button");
wgtSetEnabled(dBtn, false);
WidgetT *dChk = wgtCheckbox(disCol, "&Check me");
dChk->as.checkbox.checked = true;
wgtSetEnabled(dChk, false);
WidgetT *dRg = wgtRadioGroup(disCol);
WidgetT *dR1 = wgtRadio(dRg, "&Radio 1");
WidgetT *dR2 = wgtRadio(dRg, "R&adio 2");
dRg->as.radioGroup.selectedIdx = dR2->as.radio.index;
wgtSetEnabled(dR1, false);
wgtSetEnabled(dR2, false);
WidgetT *dDd = wgtDropdown(disCol);
wgtDropdownSetItems(dDd, disItems, 3);
wgtDropdownSetSelected(dDd, 0);
wgtSetEnabled(dDd, false);
WidgetT *dSlider = wgtSlider(disCol, 0, 100);
wgtSliderSetValue(dSlider, 40);
wgtSetEnabled(dSlider, false);
WidgetT *dProg = wgtProgressBar(disCol);
wgtProgressBarSetValue(dProg, 60);
wgtSetEnabled(dProg, false);
WidgetT *dSpin = wgtSpinner(disCol, 0, 100, 1);
wgtSpinnerSetValue(dSpin, 42);
wgtSetEnabled(dSpin, false);
WidgetT *dText = wgtTextInput(disCol, 64);
wgtSetText(dText, "Read-only");
wgtSetEnabled(dText, false);
// Status bar at bottom (outside tabs) // Status bar at bottom (outside tabs)
WidgetT *sb = wgtStatusBar(root); WidgetT *sb = wgtStatusBar(root);
WidgetT *sbLabel = wgtLabel(sb, "Ready"); WidgetT *sbLabel = wgtLabel(sb, "Ready");
@ -700,6 +862,24 @@ static void setupMainWindow(AppContextT *ctx) {
} }
} }
// Accelerator table (global hotkeys)
AccelTableT *accel = dvxCreateAccelTable();
dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW);
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_FILE_SAVE);
dvxAddAccel(accel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT);
dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT);
win1->accelTable = accel;
// Window-level context menu
MenuT *winCtx = wmCreateMenu();
wmAddMenuItem(winCtx, "Cu&t", CMD_CTX_CUT);
wmAddMenuItem(winCtx, "&Copy", CMD_CTX_COPY);
wmAddMenuItem(winCtx, "&Paste", CMD_CTX_PASTE);
wmAddMenuSeparator(winCtx);
wmAddMenuItem(winCtx, "&Properties...", CMD_CTX_PROPS);
win1->contextMenu = winCtx;
wmUpdateContentRect(win1); wmUpdateContentRect(win1);
wmReallocContentBuf(win1, &ctx->display); wmReallocContentBuf(win1, &ctx->display);
@ -832,6 +1012,7 @@ static void setupWidgetDemo(AppContextT *ctx) {
win->userData = ctx; win->userData = ctx;
win->onClose = onCloseCb; win->onClose = onCloseCb;
win->onMenu = onMenuCb;
WidgetT *root = wgtInitWindow(ctx, win); WidgetT *root = wgtInitWindow(ctx, win);
@ -878,7 +1059,19 @@ static void setupWidgetDemo(AppContextT *ctx) {
wgtLabel(listRow, "&Items:"); wgtLabel(listRow, "&Items:");
WidgetT *lb = wgtListBox(listRow); WidgetT *lb = wgtListBox(listRow);
wgtListBoxSetItems(lb, listItems, 5); wgtListBoxSetItems(lb, listItems, 5);
wgtListBoxSetReorderable(lb, true);
lb->weight = 100; lb->weight = 100;
// Context menu on the list box
MenuT *lbCtx = wmCreateMenu();
wmAddMenuItem(lbCtx, "Cu&t", CMD_CTX_CUT);
wmAddMenuItem(lbCtx, "&Copy", CMD_CTX_COPY);
wmAddMenuItem(lbCtx, "&Paste", CMD_CTX_PASTE);
wmAddMenuSeparator(lbCtx);
wmAddMenuItem(lbCtx, "&Delete", CMD_CTX_DELETE);
wmAddMenuSeparator(lbCtx);
wmAddMenuItem(lbCtx, "Select &All", CMD_CTX_SELALL);
lb->contextMenu = lbCtx;
wgtLabel(listRow, "&Multi:"); wgtLabel(listRow, "&Multi:");
WidgetT *mlb = wgtListBox(listRow); WidgetT *mlb = wgtListBox(listRow);
wgtListBoxSetMultiSelect(mlb, true); wgtListBoxSetMultiSelect(mlb, true);