diff --git a/dvx/Makefile b/dvx/Makefile index ce20799..31ecc04 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -34,6 +34,7 @@ WSRCS = widgets/widgetAnsiTerm.c \ widgets/widgetRadio.c \ widgets/widgetScrollPane.c \ widgets/widgetSeparator.c \ + widgets/widgetSplitter.c \ widgets/widgetSlider.c \ widgets/widgetSpacer.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)/widgetScrollPane.o: widgets/widgetScrollPane.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)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 93dcc12..b034276 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -17,12 +17,15 @@ #define KB_MOVE_STEP 8 #define MENU_CHECK_WIDTH 14 #define SUBMENU_ARROW_WIDTH 12 +#define TOOLTIP_DELAY_MS 500 +#define TOOLTIP_PAD 3 // ============================================================ // Prototypes // ============================================================ 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 closeAllPopups(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 initColorScheme(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 openSubMenu(AppContextT *ctx); static void openSysMenu(AppContextT *ctx, WindowT *win); @@ -47,6 +51,7 @@ static void pollKeyboard(AppContextT *ctx); static void pollMouse(AppContextT *ctx); static void refreshMinimizedIcons(AppContextT *ctx); static void updateCursorShape(AppContextT *ctx); +static void updateTooltip(AppContextT *ctx); // Button pressed via keyboard — shared with widgetEvent.c for Space/Enter 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 // ============================================================ @@ -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); // 6. Flush this dirty rect to LFB @@ -692,35 +760,41 @@ static void dispatchEvents(AppContextT *ctx) { } if (!inParent) { - // Check if mouse is on the menu bar (for switching top-level menus) - WindowT *win = findWindowById(ctx, ctx->popup.windowId); - - if (win && win->menuBar) { - int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; - - if (my >= barY && my < barY + CHROME_MENU_HEIGHT && - mx >= win->x + CHROME_TOTAL_SIDE && - mx < win->x + win->w - CHROME_TOTAL_SIDE) { - - int32_t relX = mx - win->x; - - for (int32_t i = 0; i < win->menuBar->menuCount; i++) { - MenuT *menu = &win->menuBar->menus[i]; - - if (relX >= menu->barX && relX < menu->barX + menu->barW) { - if (i != ctx->popup.menuIdx || ctx->popup.depth > 0) { - openPopupAtMenu(ctx, win, i); - } - - break; - } - } - } else if ((buttons & 1) && !(prevBtn & 1)) { - // Click outside all popups and menu bar — close everything + 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); + + if (win && win->menuBar) { + int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; + + if (my >= barY && my < barY + CHROME_MENU_HEIGHT && + mx >= win->x + CHROME_TOTAL_SIDE && + mx < win->x + win->w - CHROME_TOTAL_SIDE) { + + int32_t relX = mx - win->x; + + for (int32_t i = 0; i < win->menuBar->menuCount; i++) { + MenuT *menu = &win->menuBar->menus[i]; + + if (relX >= menu->barX && relX < menu->barX + menu->barW) { + if (i != ctx->popup.menuIdx || ctx->popup.depth > 0) { + openPopupAtMenu(ctx, win, i); + } + + break; + } + } + } else if ((buttons & 1) && !(prevBtn & 1)) { + closeAllPopups(ctx); + } + } else if ((buttons & 1) && !(prevBtn & 1)) { closeAllPopups(ctx); } - } else if ((buttons & 1) && !(prevBtn & 1)) { - closeAllPopups(ctx); } } } @@ -728,11 +802,56 @@ static void dispatchEvents(AppContextT *ctx) { return; } - // Handle button press + // Handle left button press if ((buttons & 1) && !(prevBtn & 1)) { 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 if (!(buttons & 1) && (prevBtn & 1)) { 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 // ============================================================ @@ -939,6 +1102,15 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) { } +// ============================================================ +// dvxFreeAccelTable +// ============================================================ + +void dvxFreeAccelTable(AccelTableT *table) { + free(table); +} + + // ============================================================ // dvxGetBlitOps // ============================================================ @@ -1101,6 +1273,7 @@ bool dvxUpdate(AppContextT *ctx) { pollMouse(ctx); pollKeyboard(ctx); dispatchEvents(ctx); + updateTooltip(ctx); pollAnsiTermWidgets(ctx); // 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 // ============================================================ @@ -1480,14 +1692,15 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) { MenuT *menu = &win->menuBar->menus[menuIdx]; - ctx->popup.active = true; - ctx->popup.windowId = win->id; - ctx->popup.menuIdx = menuIdx; - ctx->popup.menu = menu; - ctx->popup.popupX = win->x + menu->barX; - ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT; - ctx->popup.hoverItem = -1; - ctx->popup.depth = 0; + ctx->popup.active = true; + ctx->popup.isContextMenu = false; + ctx->popup.windowId = win->id; + ctx->popup.menuIdx = menuIdx; + ctx->popup.menu = menu; + ctx->popup.popupX = win->x + menu->barX; + ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT; + ctx->popup.hoverItem = -1; + ctx->popup.depth = 0; calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH); @@ -2215,6 +2428,16 @@ static void pollKeyboard(AppContextT *ctx) { 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: scancode=0x0F, ascii=0x09 // Shift-Tab: scancode=0x0F, ascii=0x00 @@ -2438,6 +2661,10 @@ static void updateCursorShape(AppContextT *ctx) { else if (sResizeListView) { 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 else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) { int32_t hitPart; @@ -2479,6 +2706,36 @@ static void updateCursorShape(AppContextT *ctx) { if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) { 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; } } + + +// ============================================================ +// 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); +} diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index e347f7b..a218fd9 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -45,6 +45,13 @@ typedef struct AppContextT { void (*idleCallback)(void *ctx); // called instead of yield when non-NULL void *idleCtx; 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; // 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 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 diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 5e41c45..07d1fbc 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -208,6 +208,48 @@ typedef struct { int32_t length; // total length of scrollbar track } 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 // ============================================================ @@ -274,6 +316,12 @@ typedef struct WindowT { // Widget tree root (NULL if no widgets) 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 void *userData; void (*onPaint)(struct WindowT *win, RectT *dirtyArea); @@ -335,6 +383,7 @@ typedef struct { typedef struct { bool active; + bool isContextMenu; // true = context menu (no menu bar association) int32_t windowId; // which window owns this popup chain int32_t menuIdx; // which menu bar menu is open (top level) int32_t popupX; // screen position of current (deepest) popup diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index ab946b2..0ac243d 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -70,7 +70,8 @@ typedef enum { WidgetAnsiTermE, WidgetListViewE, WidgetSpinnerE, - WidgetScrollPaneE + WidgetScrollPaneE, + WidgetSplitterE } WidgetTypeE; // ============================================================ @@ -185,6 +186,8 @@ typedef struct WidgetT { // User data and callbacks 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 (*onChange)(struct WidgetT *w); void (*onDblClick)(struct WidgetT *w); @@ -256,6 +259,9 @@ typedef struct WidgetT { bool multiSelect; int32_t anchorIdx; // anchor for shift+click range selection 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; struct { @@ -312,6 +318,7 @@ typedef struct WidgetT { struct { int32_t activeTab; + int32_t scrollOffset; // horizontal scroll of tab headers } tabControl; struct { @@ -322,11 +329,18 @@ typedef struct WidgetT { int32_t scrollPos; int32_t scrollPosH; 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; struct { const char *text; bool expanded; + bool selected; // per-item flag for multi-select } treeItem; struct { @@ -424,6 +438,9 @@ typedef struct WidgetT { bool multiSelect; int32_t anchorIdx; uint8_t *selBits; + bool reorderable; + int32_t dragIdx; + int32_t dropIdx; void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); } listView; @@ -448,6 +465,11 @@ typedef struct WidgetT { int32_t scrollPosV; int32_t scrollPosH; } scrollPane; + + struct { + int32_t dividerPos; // pixels from left/top edge + bool vertical; // true = vertical divider (left|right panes) + } splitter; } as; } WidgetT; @@ -563,9 +585,13 @@ WidgetT *wgtToolbar(WidgetT *parent); WidgetT *wgtTreeView(WidgetT *parent); WidgetT *wgtTreeViewGetSelected(const WidgetT *w); 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); void wgtTreeItemSetExpanded(WidgetT *w, bool expanded); bool wgtTreeItemIsExpanded(const WidgetT *w); +bool wgtTreeItemIsSelected(const WidgetT *w); +void wgtTreeItemSetSelected(WidgetT *w, bool selected); // ============================================================ // 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 wgtListViewSelectAll(WidgetT *w); void wgtListViewClearSelection(WidgetT *w); +void wgtListViewSetReorderable(WidgetT *w, bool reorderable); // ============================================================ // ScrollPane @@ -590,6 +617,16 @@ void wgtListViewClearSelection(WidgetT *w); 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 // ============================================================ @@ -712,6 +749,15 @@ bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx); void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected); void wgtListBoxSelectAll(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 diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index c053a93..a0237f1 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -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 // ============================================================ diff --git a/dvx/dvxWm.h b/dvx/dvxWm.h index 92c5569..186d1d1 100644 --- a/dvx/dvxWm.h +++ b/dvx/dvxWm.h @@ -122,4 +122,10 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win); // Load an icon image for a window (converts to display pixel format) 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 diff --git a/dvx/widgets/widgetBox.c b/dvx/widgets/widgetBox.c index 29b73cc..06f46df 100644 --- a/dvx/widgets/widgetBox.c +++ b/dvx/widgets/widgetBox.c @@ -55,8 +55,12 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap rectFill(d, ops, titleX - 2, titleY, 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); + } } } diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index a1bfae7..a3a3acc 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -102,10 +102,11 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma textY++; } - drawTextAccel(d, ops, font, textX, textY, - w->as.button.text, - w->enabled ? fg : colors->windowShadow, - bgFace, true); + if (!w->enabled) { + drawTextAccelEmbossed(d, ops, font, textX, textY, w->as.button.text, colors); + } else { + drawTextAccel(d, ops, font, textX, textY, w->as.button.text, fg, bgFace, true); + } if (w->focused) { int32_t off = w->as.button.pressed ? 1 : 0; diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index 4ce1ac1..9f8396d 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -105,13 +105,14 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Draw check mark if checked if (w->as.checkbox.checked) { - int32_t cx = w->x + 3; - int32_t cy = boxY + 3; - int32_t cs = CHECKBOX_BOX_SIZE - 6; + int32_t cx = w->x + 3; + int32_t cy = boxY + 3; + int32_t cs = CHECKBOX_BOX_SIZE - 6; + uint32_t checkFg = w->enabled ? fg : colors->windowShadow; for (int32_t i = 0; i < cs; i++) { - drawHLine(d, ops, cx + i, cy + i, 1, fg); - drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, fg); + drawHLine(d, ops, cx + i, cy + i, 1, checkFg); + 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 labelY = w->y + (w->h - font->charHeight) / 2; int32_t labelW = textWidthAccel(font, w->as.checkbox.text); - drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false); + + 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); + } if (w->focused) { drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c index fc54cce..d9e2588 100644 --- a/dvx/widgets/widgetClass.c +++ b/dvx/widgets/widgetClass.c @@ -383,6 +383,19 @@ static const WidgetClassT sClassScrollPane = { .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 = { .flags = WCLASS_FOCUSABLE, .paint = widgetSpinnerPaint, @@ -430,5 +443,6 @@ const WidgetClassT *widgetClassTable[] = { [WidgetAnsiTermE] = &sClassAnsiTerm, [WidgetListViewE] = &sClassListView, [WidgetSpinnerE] = &sClassSpinner, - [WidgetScrollPaneE] = &sClassScrollPane + [WidgetScrollPaneE] = &sClassScrollPane, + [WidgetSplitterE] = &sClassSplitter }; diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 7fe4d99..0e2ab31 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -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) { - 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; // Sunken text area @@ -362,7 +362,7 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } // 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; 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); // Down arrow - int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; - int32_t arrowY = w->y + w->h / 2 - 1; + uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow; + int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; + int32_t arrowY = w->y + w->h / 2 - 1; 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); } } diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 8031a40..40e3154 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -18,6 +18,9 @@ WidgetT *sResizeListView = NULL; int32_t sResizeCol = -1; int32_t sResizeStartX = 0; int32_t sResizeOrigW = 0; +WidgetT *sDragSplitter = NULL; +int32_t sDragSplitStart = 0; +WidgetT *sDragReorder = NULL; // ============================================================ diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index ffd7bca..0bf192e 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -210,9 +210,14 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Draw selected item text if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) { - drawText(d, ops, font, w->x + TEXT_INPUT_PAD, - w->y + (w->h - font->charHeight) / 2, - w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true); + int32_t textX = w->x + TEXT_INPUT_PAD; + int32_t textY = w->y + (w->h - font->charHeight) / 2; + + 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 @@ -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); // Down arrow in button - int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; - int32_t arrowY = w->y + w->h / 2 - 1; + uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow; + int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; + int32_t arrowY = w->y + w->h / 2 - 1; 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) { diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index b05d1f7..825500e 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -121,6 +121,11 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { return; } + // Don't dispatch keys to disabled widgets + if (!focus->enabled) { + return; + } + // Dispatch to per-widget onKey handler via vtable if (focus->wclass && focus->wclass->onKey) { focus->wclass->onKey(focus, key, mod); @@ -278,6 +283,52 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { 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 if (sPressedButton && !(buttons & 1)) { 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; + } +} diff --git a/dvx/widgets/widgetImageButton.c b/dvx/widgets/widgetImageButton.c index 5303d3d..4a054f4 100644 --- a/dvx/widgets/widgetImageButton.c +++ b/dvx/widgets/widgetImageButton.c @@ -104,10 +104,11 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const (void)font; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; + bool pressed = w->as.imageButton.pressed && w->enabled; BevelStyleT bevel; - bevel.highlight = w->as.imageButton.pressed ? colors->windowShadow : colors->windowHighlight; - bevel.shadow = w->as.imageButton.pressed ? colors->windowHighlight : colors->windowShadow; + bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight; + bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow; bevel.face = bgFace; bevel.width = 2; 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 imgY = w->y + (w->h - w->as.imageButton.imgH) / 2; - if (w->as.imageButton.pressed) { + if (pressed) { imgX++; imgY++; } @@ -128,7 +129,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const if (w->focused) { 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); } } diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 01ccd27..f0e2a22 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -63,6 +63,8 @@ extern const WidgetClassT *widgetClassTable[]; #define TAB_PAD_H 8 #define TAB_PAD_V 4 #define TAB_BORDER 2 +#define LISTBOX_BORDER 2 +#define LISTVIEW_BORDER 2 #define TREE_INDENT 16 #define TREE_EXPAND_SIZE 9 #define TREE_ICON_GAP 4 @@ -70,6 +72,8 @@ extern const WidgetClassT *widgetClassTable[]; #define TREE_SB_W 14 #define TREE_MIN_ROWS 4 #define SB_MIN_THUMB 14 +#define SPLITTER_BAR_W 5 +#define SPLITTER_MIN_PANE 20 // ============================================================ // Inline helpers @@ -81,6 +85,18 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) { 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) // ============================================================ @@ -99,6 +115,9 @@ extern WidgetT *sResizeListView; extern int32_t sResizeCol; extern int32_t sResizeStartX; extern int32_t sResizeOrigW; +extern WidgetT *sDragSplitter; +extern int32_t sDragSplitStart; +extern WidgetT *sDragReorder; // ============================================================ // 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 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 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 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); @@ -199,6 +219,7 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSeparatorCalcMinSize(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 widgetSpinnerCalcMinSize(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 widgetSplitterLayout(WidgetT *w, const BitmapFontT *font); void widgetTabControlLayout(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 widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod); 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 widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); 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 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 diff --git a/dvx/widgets/widgetLabel.c b/dvx/widgets/widgetLabel.c index 81cdc20..948d41a 100644 --- a/dvx/widgets/widgetLabel.c +++ b/dvx/widgets/widgetLabel.c @@ -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) { + 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 bg = w->bgColor ? w->bgColor : colors->contentBg; - drawTextAccel(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2, - w->as.label.text, fg, bg, false); + drawTextAccel(d, ops, font, w->x, textY, w->as.label.text, fg, bg, false); } diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index 183e0eb..e887c1e 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -5,7 +5,6 @@ #include #include -#define LISTBOX_BORDER 2 #define LISTBOX_PAD 2 #define LISTBOX_MIN_ROWS 4 #define LISTBOX_SB_W 14 @@ -112,6 +111,8 @@ WidgetT *wgtListBox(WidgetT *parent) { if (w) { w->as.listBox.selectedIdx = -1; w->as.listBox.anchorIdx = -1; + w->as.listBox.dragIdx = -1; + w->as.listBox.dropIdx = -1; } 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 // ============================================================ @@ -526,6 +540,13 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { 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 if (needSb) { int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W; diff --git a/dvx/widgets/widgetListView.c b/dvx/widgets/widgetListView.c index 0163e55..fef0207 100644 --- a/dvx/widgets/widgetListView.c +++ b/dvx/widgets/widgetListView.c @@ -5,7 +5,6 @@ #include #include -#define LISTVIEW_BORDER 2 #define LISTVIEW_PAD 3 #define LISTVIEW_SB_W 14 #define LISTVIEW_MIN_ROWS 4 @@ -349,6 +348,8 @@ WidgetT *wgtListView(WidgetT *parent) { w->as.listView.anchorIdx = -1; w->as.listView.sortCol = -1; w->as.listView.sortDir = ListViewSortNoneE; + w->as.listView.dragIdx = -1; + w->as.listView.dropIdx = -1; 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 // ============================================================ @@ -917,36 +942,38 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) colX += cw; } - // Not on a border — check for sort click - colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH; + // 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; - for (int32_t c = 0; c < hit->as.listView.colCount; c++) { - int32_t cw = hit->as.listView.resolvedColW[c]; + for (int32_t c = 0; c < hit->as.listView.colCount; c++) { + int32_t cw = hit->as.listView.resolvedColW[c]; - if (vx >= colX && vx < colX + cw) { - // Toggle sort direction for this column - if (hit->as.listView.sortCol == c) { - if (hit->as.listView.sortDir == ListViewSortAscE) { - hit->as.listView.sortDir = ListViewSortDescE; + if (vx >= colX && vx < colX + cw) { + // Toggle sort direction for this column + if (hit->as.listView.sortCol == c) { + if (hit->as.listView.sortDir == ListViewSortAscE) { + hit->as.listView.sortDir = ListViewSortDescE; + } else { + hit->as.listView.sortDir = ListViewSortAscE; + } } else { + hit->as.listView.sortCol = c; hit->as.listView.sortDir = ListViewSortAscE; } - } else { - hit->as.listView.sortCol = c; - hit->as.listView.sortDir = ListViewSortAscE; + + listViewBuildSortIndex(hit); + + if (hit->as.listView.onHeaderClick) { + hit->as.listView.onHeaderClick(hit, c, hit->as.listView.sortDir); + } + + wgtInvalidatePaint(hit); + return; } - listViewBuildSortIndex(hit); - - if (hit->as.listView.onHeaderClick) { - hit->as.listView.onHeaderClick(hit, c, hit->as.listView.sortDir); - } - - wgtInvalidatePaint(hit); - return; + colX += cw; } - - colX += cw; } return; @@ -1024,6 +1051,13 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { 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); @@ -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); } diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index 57fdb5c..9f5e6aa 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -318,6 +318,7 @@ void wgtSetDebugLayout(bool enabled) { void wgtSetEnabled(WidgetT *w, bool enabled) { if (w) { w->enabled = enabled; + wgtInvalidatePaint(w); } } diff --git a/dvx/widgets/widgetProgressBar.c b/dvx/widgets/widgetProgressBar.c index fbed7b6..8f58bf4 100644 --- a/dvx/widgets/widgetProgressBar.c +++ b/dvx/widgets/widgetProgressBar.c @@ -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)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; // Sunken border diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c index 4f2e5de..1d7a8a0 100644 --- a/dvx/widgets/widgetRadio.c +++ b/dvx/widgets/widgetRadio.c @@ -210,16 +210,25 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap // Draw filled diamond if selected if (w->parent && w->parent->type == WidgetRadioGroupE && w->parent->as.radioGroup.selectedIdx == w->as.radio.index) { - for (int32_t i = -2; i <= 2; i++) { - int32_t span = 3 - (i < 0 ? -i : i); - drawHLine(d, ops, bx + mid - span, boxY + mid + i, span * 2 + 1, fg); + uint32_t dotFg = w->enabled ? fg : colors->windowShadow; + + 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 labelY = w->y + (w->h - font->charHeight) / 2; int32_t labelW = textWidthAccel(font, w->as.radio.text); - drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false); + + 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); + } if (w->focused) { drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c index 629cdf6..4818542 100644 --- a/dvx/widgets/widgetSlider.c +++ b/dvx/widgets/widgetSlider.c @@ -194,7 +194,9 @@ 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)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; @@ -219,12 +221,12 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma BevelStyleT thumb; thumb.highlight = colors->windowHighlight; thumb.shadow = colors->windowShadow; - thumb.face = colors->buttonFace; + thumb.face = thumbFg; thumb.width = 2; drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &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 { // Track groove 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; thumb.highlight = colors->windowHighlight; thumb.shadow = colors->windowShadow; - thumb.face = colors->buttonFace; + thumb.face = thumbFg; thumb.width = 2; drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &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) { diff --git a/dvx/widgets/widgetSpinner.c b/dvx/widgets/widgetSpinner.c index 80dccb1..4c990e3 100644 --- a/dvx/widgets/widgetSpinner.c +++ b/dvx/widgets/widgetSpinner.c @@ -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) { - 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; int32_t btnW = SPINNER_BTN_W; diff --git a/dvx/widgets/widgetSplitter.c b/dvx/widgets/widgetSplitter.c new file mode 100644 index 0000000..5419afb --- /dev/null +++ b/dvx/widgets/widgetSplitter.c @@ -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; +} diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index 595f5f3..34d96d7 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -2,6 +2,118 @@ #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 @@ -11,7 +123,8 @@ WidgetT *wgtTabControl(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, WidgetTabControlE); if (w) { - w->as.tabControl.activeTab = 0; + w->as.tabControl.activeTab = 0; + w->as.tabControl.scrollOffset = 0; w->weight = 100; } @@ -66,10 +179,9 @@ WidgetT *wgtTabPage(WidgetT *parent, const char *title) { // ============================================================ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) { - int32_t tabH = font->charHeight + TAB_PAD_V * 2; - int32_t maxPageW = 0; - int32_t maxPageH = 0; - int32_t tabHeaderW = 0; + int32_t tabH = font->charHeight + TAB_PAD_V * 2; + int32_t maxPageW = 0; + int32_t maxPageH = 0; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->type != WidgetTabPageE) { @@ -79,9 +191,6 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) { widgetCalcMinSizeTree(c, font); maxPageW = DVX_MAX(maxPageW, c->calcMinW); 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; @@ -103,6 +212,8 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) { if (contentW < 0) { contentW = 0; } if (contentH < 0) { contentH = 0; } + tabEnsureVisible(w, font); + int32_t idx = 0; 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 (sOpenPopup) { - if (sOpenPopup->type == WidgetDropdownE) { - sOpenPopup->as.dropdown.open = false; - } else if (sOpenPopup->type == WidgetComboBoxE) { - sOpenPopup->as.comboBox.open = false; - } - - sOpenPopup = NULL; - } - + tabClosePopup(); w->as.tabControl.activeTab = active; if (w->onChange) { @@ -193,43 +295,66 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy int32_t tabH = font->charHeight + TAB_PAD_V * 2; // Only handle clicks in the tab header area - if (vy >= hit->y && vy < hit->y + tabH) { - int32_t tabX = hit->x + 2; - int32_t tabIdx = 0; + if (vy < hit->y || vy >= hit->y + tabH) { + return; + } - for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { - if (c->type != WidgetTabPageE) { - continue; - } + bool scroll = tabNeedScroll(hit, font); - int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2; + // 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 (vx >= tabX && vx < tabX + tw) { - if (tabIdx != hit->as.tabControl.activeTab) { - // Close any open dropdown/combobox popup - 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; - - if (hit->onChange) { - hit->onChange(hit); - } - } - - break; - } - - tabX += tw; - tabIdx++; + 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; + + for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { + if (c->type != WidgetTabPageE) { + continue; + } + + int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2; + + if (vx >= tabX && vx < tabX + tw && vx >= headerLeft) { + if (tabIdx != hit->as.tabControl.activeTab) { + tabClosePopup(); + hit->as.tabControl.activeTab = tabIdx; + + if (hit->onChange) { + hit->onChange(hit); + } + } + + break; + } + + tabX += tw; + tabIdx++; } } @@ -239,7 +364,8 @@ 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) { - 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 BevelStyleT panelBevel; @@ -249,8 +375,56 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B panelBevel.width = 2; drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel); - // Tab headers - int32_t tabX = w->x + 2; + // Scroll arrows + 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; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { @@ -264,48 +438,52 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B int32_t th = isActive ? tabH + 2 : tabH; uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace; - // Fill tab background - rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace); + // Only draw tabs that are at least partially visible + if (tabX + tw > headerLeft && tabX < headerRight) { + // Fill tab background + rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace); - // Top edge - drawHLine(d, ops, tabX + 2, ty, tw - 4, colors->windowHighlight); - drawHLine(d, ops, tabX + 2, ty + 1, tw - 4, colors->windowHighlight); + // Top edge + drawHLine(d, ops, tabX + 2, ty, tw - 4, colors->windowHighlight); + drawHLine(d, ops, tabX + 2, ty + 1, tw - 4, colors->windowHighlight); - // Left edge - drawVLine(d, ops, tabX, ty + 2, th - 2, colors->windowHighlight); - drawVLine(d, ops, tabX + 1, ty + 2, th - 2, colors->windowHighlight); + // Left edge + drawVLine(d, ops, tabX, ty + 2, th - 2, colors->windowHighlight); + drawVLine(d, ops, tabX + 1, ty + 2, th - 2, colors->windowHighlight); - // Right edge - drawVLine(d, ops, tabX + tw - 1, ty + 2, th - 2, colors->windowShadow); - drawVLine(d, ops, tabX + tw - 2, ty + 2, th - 2, colors->windowShadow); + // Right edge + drawVLine(d, ops, tabX + tw - 1, ty + 2, th - 2, colors->windowShadow); + drawVLine(d, ops, tabX + tw - 2, ty + 2, th - 2, colors->windowShadow); - if (isActive) { - // Erase panel top border under active tab - rectFill(d, ops, tabX + 2, w->y + tabH, tw - 4, 2, colors->contentBg); - } else { - // Bottom edge for inactive tab - drawHLine(d, ops, tabX, ty + th - 1, tw, colors->windowShadow); - drawHLine(d, ops, tabX + 1, ty + th - 2, tw - 2, colors->windowShadow); - } + if (isActive) { + // Erase panel top border under active tab + rectFill(d, ops, tabX + 2, w->y + tabH, tw - 4, 2, colors->contentBg); + } else { + // Bottom edge for inactive tab + drawHLine(d, ops, tabX, ty + th - 1, tw, colors->windowShadow); + drawHLine(d, ops, tabX + 1, ty + th - 2, tw - 2, colors->windowShadow); + } - // Tab label - int32_t labelY = ty + TAB_PAD_V; + // Tab label + int32_t labelY = ty + TAB_PAD_V; - if (!isActive) { - labelY++; - } + if (!isActive) { + labelY++; + } - drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, - c->as.tabPage.title, colors->contentFg, tabFace, true); + drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, c->as.tabPage.title, colors->contentFg, tabFace, true); - if (isActive && w->focused) { - drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg); + if (isActive && w->focused) { + drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg); + } } tabX += tw; tabIdx++; } + setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); + // Paint only active tab page's children tabIdx = 0; diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index 5a6a72e..9357fa3 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -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) { - 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; // Sunken border @@ -2294,7 +2294,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi } // Draw cursor - if (w->focused) { + if (w->focused && w->enabled) { int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth; if (cursorX >= w->x + TEXT_INPUT_PAD && diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index 362307d..f97227c 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -8,13 +8,16 @@ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font); 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 drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH); 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 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 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 WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, 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 // ============================================================ @@ -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 // ============================================================ -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) { if (c->type != WidgetTreeItemE || !c->visible) { continue; @@ -295,14 +348,21 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co // Skip items outside visible area if (iy + font->charHeight <= clipTop || iy >= clipBottom) { 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; } - // Highlight selected item - bool isSelected = (c == selectedItem); + // Highlight selected item(s) + bool isSelected; + + if (multi) { + isSelected = c->as.treeItem.selected; + } else { + isSelected = (c == treeView->as.treeView.selectedItem); + } + uint32_t fg; uint32_t bg; @@ -354,9 +414,15 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co // Draw text 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 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 // ============================================================ @@ -630,6 +743,76 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *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,13 +833,13 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { - (void)mod; - if (!w || w->type != WidgetTreeViewE) { return; } - WidgetT *sel = w->as.treeView.selectedItem; + bool multi = w->as.treeView.multiSelect; + bool shift = (mod & KEY_MOD_SHIFT) != 0; + WidgetT *sel = w->as.treeView.selectedItem; if (key == (0x50 | 0x100)) { // Down arrow — next visible item @@ -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 { 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 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) { // Auto-select first item if nothing is selected 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; @@ -942,11 +1154,33 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) 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; + 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 - bool hasChildren = false; + bool hasChildren = false; + bool clickedExpandIcon = false; for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) { 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; if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) { + clickedExpandIcon = true; item->as.treeItem.expanded = !item->as.treeItem.expanded; // 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); } } + + // 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, baseX, &itemY, 0, 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 setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index cd06743..bbebae3 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -33,6 +33,12 @@ #define CMD_VIEW_SIZE_MED 308 #define CMD_VIEW_SIZE_LARGE 309 #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 @@ -205,6 +211,30 @@ static void onMenuCb(WindowT *win, int32_t menuId) { "A DESQview/X-style windowing system for DOS.", MB_OK | MB_ICONINFO); 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); wgtDropdownSetItems(dd, colorItems, 6); wgtDropdownSetSelected(dd, 0); + wgtSetTooltip(dd, "Choose a color"); WidgetT *cbRow = wgtHBox(page1); wgtLabel(cbRow, "Si&ze:"); WidgetT *cb = wgtComboBox(cbRow, 32); wgtComboBoxSetItems(cb, sizeItems, 4); wgtComboBoxSetSelected(cb, 1); + wgtSetTooltip(cb, "Type or select a size"); wgtHSeparator(page1); wgtLabel(page1, "&Progress:"); WidgetT *pb = wgtProgressBar(page1); wgtProgressBarSetValue(pb, 65); + wgtSetTooltip(pb, "Task progress: 65%"); wgtLabel(page1, "&Volume:"); - wgtSlider(page1, 0, 100); + WidgetT *slider = wgtSlider(page1, 0, 100); + wgtSetTooltip(slider, "Adjust volume level"); WidgetT *spinRow = wgtHBox(page1); spinRow->maxH = wgtPixels(30); @@ -462,11 +496,13 @@ static void setupControlsWindow(AppContextT *ctx) { WidgetT *spin = wgtSpinner(spinRow, 0, 999, 1); wgtSpinnerSetValue(spin, 42); spin->weight = 50; + wgtSetTooltip(spin, "Enter quantity (0-999)"); // --- Tab 2: Tree --- WidgetT *page2 = wgtTabPage(tabs, "&Tree"); WidgetT *tree = wgtTreeView(page2); + wgtTreeViewSetReorderable(tree, true); WidgetT *docs = wgtTreeItem(tree, "Documents"); wgtTreeItemSetExpanded(docs, true); wgtTreeItem(docs, "README.md"); @@ -511,6 +547,7 @@ static void setupControlsWindow(AppContextT *ctx) { wgtListViewSetData(lv, lvData, 10); wgtListViewSetSelected(lv, 0); wgtListViewSetMultiSelect(lv, true); + wgtListViewSetReorderable(lv, true); lv->weight = 100; // --- Tab 4: ScrollPane --- @@ -623,6 +660,131 @@ static void setupControlsWindow(AppContextT *ctx) { wgtCanvasDrawLine(cv, 70, 5, 130, 70); 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) WidgetT *sb = wgtStatusBar(root); 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); wmReallocContentBuf(win1, &ctx->display); @@ -832,6 +1012,7 @@ static void setupWidgetDemo(AppContextT *ctx) { win->userData = ctx; win->onClose = onCloseCb; + win->onMenu = onMenuCb; WidgetT *root = wgtInitWindow(ctx, win); @@ -878,7 +1059,19 @@ static void setupWidgetDemo(AppContextT *ctx) { wgtLabel(listRow, "&Items:"); WidgetT *lb = wgtListBox(listRow); wgtListBoxSetItems(lb, listItems, 5); + wgtListBoxSetReorderable(lb, true); 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:"); WidgetT *mlb = wgtListBox(listRow); wgtListBoxSetMultiSelect(mlb, true);