File Open/Save dialogs, vertical and horizontal splitter panes, tooltips, disabled widgets, context menus, list/tree item drag reordering, multiselect treeview, keyboard accelerators, clipboard API. Whew.
This commit is contained in:
parent
705fa1e99a
commit
7129035bed
30 changed files with 2240 additions and 201 deletions
|
|
@ -34,6 +34,7 @@ WSRCS = widgets/widgetAnsiTerm.c \
|
||||||
widgets/widgetRadio.c \
|
widgets/widgetRadio.c \
|
||||||
widgets/widgetScrollPane.c \
|
widgets/widgetScrollPane.c \
|
||||||
widgets/widgetSeparator.c \
|
widgets/widgetSeparator.c \
|
||||||
|
widgets/widgetSplitter.c \
|
||||||
widgets/widgetSlider.c \
|
widgets/widgetSlider.c \
|
||||||
widgets/widgetSpacer.c \
|
widgets/widgetSpacer.c \
|
||||||
widgets/widgetSpinner.c \
|
widgets/widgetSpinner.c \
|
||||||
|
|
@ -103,6 +104,7 @@ $(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetScrollPane.o: widgets/widgetScrollPane.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetScrollPane.o: widgets/widgetScrollPane.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)
|
||||||
|
$(WOBJDIR)/widgetSplitter.o: widgets/widgetSplitter.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS)
|
||||||
$(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS)
|
$(WOBJDIR)/widgetSpinner.o: widgets/widgetSpinner.c $(WIDGET_DEPS)
|
||||||
|
|
|
||||||
385
dvx/dvxApp.c
385
dvx/dvxApp.c
|
|
@ -17,12 +17,15 @@
|
||||||
#define KB_MOVE_STEP 8
|
#define KB_MOVE_STEP 8
|
||||||
#define MENU_CHECK_WIDTH 14
|
#define MENU_CHECK_WIDTH 14
|
||||||
#define SUBMENU_ARROW_WIDTH 12
|
#define SUBMENU_ARROW_WIDTH 12
|
||||||
|
#define TOOLTIP_DELAY_MS 500
|
||||||
|
#define TOOLTIP_PAD 3
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Prototypes
|
// Prototypes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
|
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
|
||||||
|
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers);
|
||||||
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
|
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
|
||||||
static void closeAllPopups(AppContextT *ctx);
|
static void closeAllPopups(AppContextT *ctx);
|
||||||
static void closePopupLevel(AppContextT *ctx);
|
static void closePopupLevel(AppContextT *ctx);
|
||||||
|
|
@ -38,6 +41,7 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id);
|
||||||
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
|
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
|
||||||
static void initColorScheme(AppContextT *ctx);
|
static void initColorScheme(AppContextT *ctx);
|
||||||
static void initMouse(AppContextT *ctx);
|
static void initMouse(AppContextT *ctx);
|
||||||
|
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
|
||||||
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
|
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
|
||||||
static void openSubMenu(AppContextT *ctx);
|
static void openSubMenu(AppContextT *ctx);
|
||||||
static void openSysMenu(AppContextT *ctx, WindowT *win);
|
static void openSysMenu(AppContextT *ctx, WindowT *win);
|
||||||
|
|
@ -47,6 +51,7 @@ static void pollKeyboard(AppContextT *ctx);
|
||||||
static void pollMouse(AppContextT *ctx);
|
static void pollMouse(AppContextT *ctx);
|
||||||
static void refreshMinimizedIcons(AppContextT *ctx);
|
static void refreshMinimizedIcons(AppContextT *ctx);
|
||||||
static void updateCursorShape(AppContextT *ctx);
|
static void updateCursorShape(AppContextT *ctx);
|
||||||
|
static void updateTooltip(AppContextT *ctx);
|
||||||
|
|
||||||
// Button pressed via keyboard — shared with widgetEvent.c for Space/Enter
|
// Button pressed via keyboard — shared with widgetEvent.c for Space/Enter
|
||||||
WidgetT *sKeyPressedBtn = NULL;
|
WidgetT *sKeyPressedBtn = NULL;
|
||||||
|
|
@ -99,6 +104,54 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// checkAccelTable — test key against window's accelerator table
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t modifiers) {
|
||||||
|
if (!win->accelTable || !win->onMenu) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Ctrl+letter: BIOS returns ASCII 0x01-0x1A for Ctrl+A..Z
|
||||||
|
// Map back to uppercase letter for matching
|
||||||
|
int32_t matchKey = key;
|
||||||
|
|
||||||
|
if ((modifiers & ACCEL_CTRL) && matchKey >= 0x01 && matchKey <= 0x1A) {
|
||||||
|
matchKey = matchKey + 'A' - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uppercase for case-insensitive letter matching
|
||||||
|
if (matchKey >= 'a' && matchKey <= 'z') {
|
||||||
|
matchKey = matchKey - 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT);
|
||||||
|
AccelTableT *table = win->accelTable;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < table->count; i++) {
|
||||||
|
AccelEntryT *e = &table->entries[i];
|
||||||
|
|
||||||
|
// Uppercase the entry key too
|
||||||
|
int32_t entryKey = e->key;
|
||||||
|
|
||||||
|
if (entryKey >= 'a' && entryKey <= 'z') {
|
||||||
|
entryKey = entryKey - 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t entryMods = e->modifiers & (ACCEL_CTRL | ACCEL_ALT);
|
||||||
|
|
||||||
|
if (entryKey == matchKey && entryMods == requiredMods) {
|
||||||
|
win->onMenu(win, e->cmdId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)ctx;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// clickMenuCheckRadio — toggle check or select radio on click
|
// clickMenuCheckRadio — toggle check or select radio on click
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -305,7 +358,22 @@ static void compositeAndFlush(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Draw cursor
|
// 5. Draw tooltip
|
||||||
|
if (ctx->tooltipText) {
|
||||||
|
RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH };
|
||||||
|
RectT ttIsect;
|
||||||
|
|
||||||
|
if (rectIntersect(dr, &ttRect, &ttIsect)) {
|
||||||
|
rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg);
|
||||||
|
drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->colors.contentFg);
|
||||||
|
drawHLine(d, ops, ctx->tooltipX, ctx->tooltipY + ctx->tooltipH - 1, ctx->tooltipW, ctx->colors.contentFg);
|
||||||
|
drawVLine(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg);
|
||||||
|
drawVLine(d, ops, ctx->tooltipX + ctx->tooltipW - 1, ctx->tooltipY, ctx->tooltipH, ctx->colors.contentFg);
|
||||||
|
drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Draw cursor
|
||||||
drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
|
drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
|
||||||
|
|
||||||
// 6. Flush this dirty rect to LFB
|
// 6. Flush this dirty rect to LFB
|
||||||
|
|
@ -692,7 +760,13 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inParent) {
|
if (!inParent) {
|
||||||
// Check if mouse is on the menu bar (for switching top-level menus)
|
if (ctx->popup.isContextMenu) {
|
||||||
|
// Context menu: any click outside closes it
|
||||||
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
||||||
|
closeAllPopups(ctx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Menu bar popup: check if mouse is on the menu bar for switching
|
||||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||||
|
|
||||||
if (win && win->menuBar) {
|
if (win && win->menuBar) {
|
||||||
|
|
@ -716,7 +790,6 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
||||||
// Click outside all popups and menu bar — close everything
|
|
||||||
closeAllPopups(ctx);
|
closeAllPopups(ctx);
|
||||||
}
|
}
|
||||||
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
||||||
|
|
@ -724,15 +797,61 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle button press
|
// Handle left button press
|
||||||
if ((buttons & 1) && !(prevBtn & 1)) {
|
if ((buttons & 1) && !(prevBtn & 1)) {
|
||||||
handleMouseButton(ctx, mx, my, buttons);
|
handleMouseButton(ctx, mx, my, buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle right button press — context menus
|
||||||
|
if ((buttons & 2) && !(prevBtn & 2)) {
|
||||||
|
int32_t hitPart;
|
||||||
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
||||||
|
|
||||||
|
if (hitIdx >= 0 && hitPart == 0) {
|
||||||
|
WindowT *win = ctx->stack.windows[hitIdx];
|
||||||
|
|
||||||
|
// Raise and focus if not already
|
||||||
|
if (hitIdx != ctx->stack.focusedIdx) {
|
||||||
|
wmRaiseWindow(&ctx->stack, &ctx->dirty, hitIdx);
|
||||||
|
hitIdx = ctx->stack.count - 1;
|
||||||
|
wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
|
||||||
|
win = ctx->stack.windows[hitIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check widget context menu first
|
||||||
|
MenuT *ctxMenu = NULL;
|
||||||
|
|
||||||
|
if (win->widgetRoot) {
|
||||||
|
int32_t relX = mx - win->x - win->contentX;
|
||||||
|
int32_t relY = my - win->y - win->contentY;
|
||||||
|
WidgetT *hit = widgetHitTest(win->widgetRoot, relX, relY);
|
||||||
|
|
||||||
|
// Walk up the tree to find a context menu
|
||||||
|
while (hit && !hit->contextMenu) {
|
||||||
|
hit = hit->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
ctxMenu = hit->contextMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to window context menu
|
||||||
|
if (!ctxMenu) {
|
||||||
|
ctxMenu = win->contextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctxMenu) {
|
||||||
|
openContextMenu(ctx, win, ctxMenu, mx, my);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle button release on content — send to focused window
|
// Handle button release on content — send to focused window
|
||||||
if (!(buttons & 1) && (prevBtn & 1)) {
|
if (!(buttons & 1) && (prevBtn & 1)) {
|
||||||
if (ctx->stack.focusedIdx >= 0) {
|
if (ctx->stack.focusedIdx >= 0) {
|
||||||
|
|
@ -886,6 +1005,50 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxAddAccel
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId) {
|
||||||
|
if (!table || table->count >= MAX_ACCEL_ENTRIES) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccelEntryT *e = &table->entries[table->count++];
|
||||||
|
e->key = key;
|
||||||
|
e->modifiers = modifiers;
|
||||||
|
e->cmdId = cmdId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxClipboardCopy
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void dvxClipboardCopy(const char *text, int32_t len) {
|
||||||
|
clipboardCopy(text, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxClipboardGet
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const char *dvxClipboardGet(int32_t *outLen) {
|
||||||
|
return clipboardGet(outLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxCreateAccelTable
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
AccelTableT *dvxCreateAccelTable(void) {
|
||||||
|
AccelTableT *table = (AccelTableT *)calloc(1, sizeof(AccelTableT));
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxDestroyWindow
|
// dvxDestroyWindow
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -939,6 +1102,15 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxFreeAccelTable
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void dvxFreeAccelTable(AccelTableT *table) {
|
||||||
|
free(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxGetBlitOps
|
// dvxGetBlitOps
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1101,6 +1273,7 @@ bool dvxUpdate(AppContextT *ctx) {
|
||||||
pollMouse(ctx);
|
pollMouse(ctx);
|
||||||
pollKeyboard(ctx);
|
pollKeyboard(ctx);
|
||||||
dispatchEvents(ctx);
|
dispatchEvents(ctx);
|
||||||
|
updateTooltip(ctx);
|
||||||
pollAnsiTermWidgets(ctx);
|
pollAnsiTermWidgets(ctx);
|
||||||
|
|
||||||
// Periodically refresh one minimized window thumbnail (staggered)
|
// Periodically refresh one minimized window thumbnail (staggered)
|
||||||
|
|
@ -1466,6 +1639,45 @@ static void initMouse(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// openContextMenu — open a context menu at a screen position
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
|
||||||
|
if (!menu || menu->itemCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllPopups(ctx);
|
||||||
|
closeSysMenu(ctx);
|
||||||
|
|
||||||
|
ctx->popup.active = true;
|
||||||
|
ctx->popup.isContextMenu = true;
|
||||||
|
ctx->popup.windowId = win->id;
|
||||||
|
ctx->popup.menuIdx = -1;
|
||||||
|
ctx->popup.menu = menu;
|
||||||
|
ctx->popup.hoverItem = -1;
|
||||||
|
ctx->popup.depth = 0;
|
||||||
|
|
||||||
|
calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);
|
||||||
|
|
||||||
|
// Position at mouse, clamped to screen
|
||||||
|
ctx->popup.popupX = screenX;
|
||||||
|
ctx->popup.popupY = screenY;
|
||||||
|
|
||||||
|
if (ctx->popup.popupX + ctx->popup.popupW > ctx->display.width) {
|
||||||
|
ctx->popup.popupX = ctx->display.width - ctx->popup.popupW;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->popup.popupY + ctx->popup.popupH > ctx->display.height) {
|
||||||
|
ctx->popup.popupY = ctx->display.height - ctx->popup.popupH;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
||||||
|
ctx->popup.popupW, ctx->popup.popupH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// openPopupAtMenu — open top-level popup for a menu bar menu
|
// openPopupAtMenu — open top-level popup for a menu bar menu
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1481,6 +1693,7 @@ static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
|
||||||
MenuT *menu = &win->menuBar->menus[menuIdx];
|
MenuT *menu = &win->menuBar->menus[menuIdx];
|
||||||
|
|
||||||
ctx->popup.active = true;
|
ctx->popup.active = true;
|
||||||
|
ctx->popup.isContextMenu = false;
|
||||||
ctx->popup.windowId = win->id;
|
ctx->popup.windowId = win->id;
|
||||||
ctx->popup.menuIdx = menuIdx;
|
ctx->popup.menuIdx = menuIdx;
|
||||||
ctx->popup.menu = menu;
|
ctx->popup.menu = menu;
|
||||||
|
|
@ -2215,6 +2428,16 @@ static void pollKeyboard(AppContextT *ctx) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check accelerator table on focused window
|
||||||
|
if (ctx->stack.focusedIdx >= 0) {
|
||||||
|
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
||||||
|
int32_t key = ascii ? ascii : (scancode | 0x100);
|
||||||
|
|
||||||
|
if (checkAccelTable(ctx, win, key, shiftFlags)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tab / Shift-Tab — cycle focus between widgets
|
// Tab / Shift-Tab — cycle focus between widgets
|
||||||
// Tab: scancode=0x0F, ascii=0x09
|
// Tab: scancode=0x0F, ascii=0x09
|
||||||
// Shift-Tab: scancode=0x0F, ascii=0x00
|
// Shift-Tab: scancode=0x0F, ascii=0x00
|
||||||
|
|
@ -2438,6 +2661,10 @@ static void updateCursorShape(AppContextT *ctx) {
|
||||||
else if (sResizeListView) {
|
else if (sResizeListView) {
|
||||||
newCursor = CURSOR_RESIZE_H;
|
newCursor = CURSOR_RESIZE_H;
|
||||||
}
|
}
|
||||||
|
// Active splitter drag
|
||||||
|
else if (sDragSplitter) {
|
||||||
|
newCursor = sDragSplitter->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
|
||||||
|
}
|
||||||
// Not in an active drag/resize — check what we're hovering
|
// Not in an active drag/resize — check what we're hovering
|
||||||
else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) {
|
else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) {
|
||||||
int32_t hitPart;
|
int32_t hitPart;
|
||||||
|
|
@ -2479,6 +2706,36 @@ static void updateCursorShape(AppContextT *ctx) {
|
||||||
if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) {
|
if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) {
|
||||||
newCursor = CURSOR_RESIZE_H;
|
newCursor = CURSOR_RESIZE_H;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Walk into splitters (NO_HIT_RECURSE stops widgetHitTest at the outermost one)
|
||||||
|
while (hit && hit->type == WidgetSplitterE) {
|
||||||
|
int32_t pos = hit->as.splitter.dividerPos;
|
||||||
|
bool onBar;
|
||||||
|
|
||||||
|
if (hit->as.splitter.vertical) {
|
||||||
|
int32_t barX = hit->x + pos;
|
||||||
|
onBar = (vx >= barX && vx < barX + SPLITTER_BAR_W);
|
||||||
|
} else {
|
||||||
|
int32_t barY = hit->y + pos;
|
||||||
|
onBar = (vy >= barY && vy < barY + SPLITTER_BAR_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onBar) {
|
||||||
|
newCursor = hit->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not on this splitter's bar — check children for nested splitters
|
||||||
|
WidgetT *inner = NULL;
|
||||||
|
|
||||||
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
|
||||||
|
inner = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hit = inner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2489,3 +2746,123 @@ static void updateCursorShape(AppContextT *ctx) {
|
||||||
ctx->cursorId = newCursor;
|
ctx->cursorId = newCursor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// updateTooltip — show/hide tooltip based on hover state
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void updateTooltip(AppContextT *ctx) {
|
||||||
|
clock_t now = clock();
|
||||||
|
clock_t threshold = (clock_t)TOOLTIP_DELAY_MS * CLOCKS_PER_SEC / 1000;
|
||||||
|
int32_t mx = ctx->mouseX;
|
||||||
|
int32_t my = ctx->mouseY;
|
||||||
|
|
||||||
|
// Mouse moved or button pressed — hide tooltip and reset timer
|
||||||
|
if (mx != ctx->prevMouseX || my != ctx->prevMouseY || ctx->mouseButtons) {
|
||||||
|
if (ctx->tooltipText) {
|
||||||
|
// Dirty old tooltip area
|
||||||
|
dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH);
|
||||||
|
ctx->tooltipText = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->tooltipHoverStart = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already showing a tooltip
|
||||||
|
if (ctx->tooltipText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not enough time has passed
|
||||||
|
if ((now - ctx->tooltipHoverStart) < threshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show tooltips while popups/menus are active
|
||||||
|
if (ctx->popup.active || ctx->sysMenu.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the widget under the cursor
|
||||||
|
int32_t hitPart;
|
||||||
|
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
|
||||||
|
|
||||||
|
if (hitIdx < 0 || hitPart != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowT *win = ctx->stack.windows[hitIdx];
|
||||||
|
|
||||||
|
if (!win->widgetRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t cx = mx - win->x - win->contentX;
|
||||||
|
int32_t cy = my - win->y - win->contentY;
|
||||||
|
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
||||||
|
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
||||||
|
int32_t vx = cx + scrollX;
|
||||||
|
int32_t vy = cy + scrollY;
|
||||||
|
|
||||||
|
WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy);
|
||||||
|
|
||||||
|
// Walk into NO_HIT_RECURSE containers to find deepest child
|
||||||
|
while (hit && hit->wclass && (hit->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
||||||
|
WidgetT *inner = NULL;
|
||||||
|
|
||||||
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) {
|
||||||
|
inner = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inner) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the inner child has a tooltip, use it; otherwise check if it's another container
|
||||||
|
if (inner->tooltip) {
|
||||||
|
hit = inner;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inner->wclass && (inner->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
||||||
|
hit = inner;
|
||||||
|
} else {
|
||||||
|
WidgetT *deep = widgetHitTest(inner, vx, vy);
|
||||||
|
hit = deep ? deep : inner;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hit || !hit->tooltip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the tooltip
|
||||||
|
ctx->tooltipText = hit->tooltip;
|
||||||
|
|
||||||
|
int32_t tw = textWidth(&ctx->font, hit->tooltip) + TOOLTIP_PAD * 2;
|
||||||
|
int32_t th = ctx->font.charHeight + TOOLTIP_PAD * 2;
|
||||||
|
|
||||||
|
// Position below and right of cursor
|
||||||
|
ctx->tooltipX = mx + 12;
|
||||||
|
ctx->tooltipY = my + 16;
|
||||||
|
|
||||||
|
// Keep on screen
|
||||||
|
if (ctx->tooltipX + tw > ctx->display.width) {
|
||||||
|
ctx->tooltipX = ctx->display.width - tw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->tooltipY + th > ctx->display.height) {
|
||||||
|
ctx->tooltipY = my - th - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->tooltipW = tw;
|
||||||
|
ctx->tooltipH = th;
|
||||||
|
|
||||||
|
// Dirty the tooltip area
|
||||||
|
dirtyListAdd(&ctx->dirty, ctx->tooltipX, ctx->tooltipY, tw, th);
|
||||||
|
}
|
||||||
|
|
|
||||||
22
dvx/dvxApp.h
22
dvx/dvxApp.h
|
|
@ -45,6 +45,13 @@ typedef struct AppContextT {
|
||||||
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
|
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
|
||||||
void *idleCtx;
|
void *idleCtx;
|
||||||
WindowT *modalWindow; // if non-NULL, only this window receives input
|
WindowT *modalWindow; // if non-NULL, only this window receives input
|
||||||
|
// Tooltip state
|
||||||
|
clock_t tooltipHoverStart; // when mouse stopped moving
|
||||||
|
const char *tooltipText; // text to show (NULL = hidden)
|
||||||
|
int32_t tooltipX; // screen position
|
||||||
|
int32_t tooltipY;
|
||||||
|
int32_t tooltipW; // size (pre-computed)
|
||||||
|
int32_t tooltipH;
|
||||||
} AppContextT;
|
} AppContextT;
|
||||||
|
|
||||||
// Initialize the application (VESA mode, input, etc.)
|
// Initialize the application (VESA mode, input, etc.)
|
||||||
|
|
@ -102,4 +109,19 @@ const BlitOpsT *dvxGetBlitOps(const AppContextT *ctx);
|
||||||
// Load an icon for a window from an image file
|
// Load an icon for a window from an image file
|
||||||
int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path);
|
int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path);
|
||||||
|
|
||||||
|
// Create an accelerator table (caller must free with dvxFreeAccelTable)
|
||||||
|
AccelTableT *dvxCreateAccelTable(void);
|
||||||
|
|
||||||
|
// Free an accelerator table
|
||||||
|
void dvxFreeAccelTable(AccelTableT *table);
|
||||||
|
|
||||||
|
// Add an entry to an accelerator table
|
||||||
|
void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmdId);
|
||||||
|
|
||||||
|
// Copy text to the shared clipboard
|
||||||
|
void dvxClipboardCopy(const char *text, int32_t len);
|
||||||
|
|
||||||
|
// Get clipboard contents (returns pointer to internal buffer, NULL if empty)
|
||||||
|
const char *dvxClipboardGet(int32_t *outLen);
|
||||||
|
|
||||||
#endif // DVX_APP_H
|
#endif // DVX_APP_H
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,48 @@ typedef struct {
|
||||||
int32_t length; // total length of scrollbar track
|
int32_t length; // total length of scrollbar track
|
||||||
} ScrollbarT;
|
} ScrollbarT;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Accelerator table (global hotkeys)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#define MAX_ACCEL_ENTRIES 32
|
||||||
|
|
||||||
|
// Modifier flags for accelerators (match BIOS shift state bits)
|
||||||
|
#define ACCEL_SHIFT 0x03
|
||||||
|
#define ACCEL_CTRL 0x04
|
||||||
|
#define ACCEL_ALT 0x08
|
||||||
|
|
||||||
|
// Key codes for non-ASCII keys (scancode | 0x100)
|
||||||
|
#define KEY_F1 (0x3B | 0x100)
|
||||||
|
#define KEY_F2 (0x3C | 0x100)
|
||||||
|
#define KEY_F3 (0x3D | 0x100)
|
||||||
|
#define KEY_F4 (0x3E | 0x100)
|
||||||
|
#define KEY_F5 (0x3F | 0x100)
|
||||||
|
#define KEY_F6 (0x40 | 0x100)
|
||||||
|
#define KEY_F7 (0x41 | 0x100)
|
||||||
|
#define KEY_F8 (0x42 | 0x100)
|
||||||
|
#define KEY_F9 (0x43 | 0x100)
|
||||||
|
#define KEY_F10 (0x44 | 0x100)
|
||||||
|
#define KEY_F11 (0x57 | 0x100)
|
||||||
|
#define KEY_F12 (0x58 | 0x100)
|
||||||
|
#define KEY_INSERT (0x52 | 0x100)
|
||||||
|
#define KEY_DELETE (0x53 | 0x100)
|
||||||
|
#define KEY_HOME (0x47 | 0x100)
|
||||||
|
#define KEY_END (0x4F | 0x100)
|
||||||
|
#define KEY_PGUP (0x49 | 0x100)
|
||||||
|
#define KEY_PGDN (0x51 | 0x100)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t key; // key code: ASCII char or KEY_Fxx for extended
|
||||||
|
int32_t modifiers; // ACCEL_CTRL, ACCEL_SHIFT, ACCEL_ALT (OR'd together)
|
||||||
|
int32_t cmdId; // command ID passed to onMenu
|
||||||
|
} AccelEntryT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
AccelEntryT entries[MAX_ACCEL_ENTRIES];
|
||||||
|
int32_t count;
|
||||||
|
} AccelTableT;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Window
|
// Window
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -274,6 +316,12 @@ typedef struct WindowT {
|
||||||
// Widget tree root (NULL if no widgets)
|
// Widget tree root (NULL if no widgets)
|
||||||
struct WidgetT *widgetRoot;
|
struct WidgetT *widgetRoot;
|
||||||
|
|
||||||
|
// Context menu (NULL if none, caller owns the MenuT allocation)
|
||||||
|
MenuT *contextMenu;
|
||||||
|
|
||||||
|
// Accelerator table (NULL if none, caller owns allocation)
|
||||||
|
AccelTableT *accelTable;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
void *userData;
|
void *userData;
|
||||||
void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
|
void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
|
||||||
|
|
@ -335,6 +383,7 @@ typedef struct {
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool active;
|
bool active;
|
||||||
|
bool isContextMenu; // true = context menu (no menu bar association)
|
||||||
int32_t windowId; // which window owns this popup chain
|
int32_t windowId; // which window owns this popup chain
|
||||||
int32_t menuIdx; // which menu bar menu is open (top level)
|
int32_t menuIdx; // which menu bar menu is open (top level)
|
||||||
int32_t popupX; // screen position of current (deepest) popup
|
int32_t popupX; // screen position of current (deepest) popup
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ typedef enum {
|
||||||
WidgetAnsiTermE,
|
WidgetAnsiTermE,
|
||||||
WidgetListViewE,
|
WidgetListViewE,
|
||||||
WidgetSpinnerE,
|
WidgetSpinnerE,
|
||||||
WidgetScrollPaneE
|
WidgetScrollPaneE,
|
||||||
|
WidgetSplitterE
|
||||||
} WidgetTypeE;
|
} WidgetTypeE;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -185,6 +186,8 @@ typedef struct WidgetT {
|
||||||
|
|
||||||
// User data and callbacks
|
// User data and callbacks
|
||||||
void *userData;
|
void *userData;
|
||||||
|
const char *tooltip; // tooltip text (NULL = none, caller owns string)
|
||||||
|
MenuT *contextMenu; // right-click context menu (NULL = none, caller owns)
|
||||||
void (*onClick)(struct WidgetT *w);
|
void (*onClick)(struct WidgetT *w);
|
||||||
void (*onChange)(struct WidgetT *w);
|
void (*onChange)(struct WidgetT *w);
|
||||||
void (*onDblClick)(struct WidgetT *w);
|
void (*onDblClick)(struct WidgetT *w);
|
||||||
|
|
@ -256,6 +259,9 @@ typedef struct WidgetT {
|
||||||
bool multiSelect;
|
bool multiSelect;
|
||||||
int32_t anchorIdx; // anchor for shift+click range selection
|
int32_t anchorIdx; // anchor for shift+click range selection
|
||||||
uint8_t *selBits; // per-item selection flags (multi-select only)
|
uint8_t *selBits; // per-item selection flags (multi-select only)
|
||||||
|
bool reorderable; // allow drag-reorder of items
|
||||||
|
int32_t dragIdx; // item being dragged (-1 = none)
|
||||||
|
int32_t dropIdx; // insertion point (-1 = none)
|
||||||
} listBox;
|
} listBox;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|
@ -312,6 +318,7 @@ typedef struct WidgetT {
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
int32_t activeTab;
|
int32_t activeTab;
|
||||||
|
int32_t scrollOffset; // horizontal scroll of tab headers
|
||||||
} tabControl;
|
} tabControl;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|
@ -322,11 +329,18 @@ typedef struct WidgetT {
|
||||||
int32_t scrollPos;
|
int32_t scrollPos;
|
||||||
int32_t scrollPosH;
|
int32_t scrollPosH;
|
||||||
struct WidgetT *selectedItem;
|
struct WidgetT *selectedItem;
|
||||||
|
struct WidgetT *anchorItem; // anchor for shift+click range selection
|
||||||
|
bool multiSelect;
|
||||||
|
bool reorderable; // allow drag-reorder of items
|
||||||
|
struct WidgetT *dragItem; // item being dragged (NULL = none)
|
||||||
|
struct WidgetT *dropTarget; // insertion target (NULL = none)
|
||||||
|
bool dropAfter; // true = insert after target, false = before
|
||||||
} treeView;
|
} treeView;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
const char *text;
|
const char *text;
|
||||||
bool expanded;
|
bool expanded;
|
||||||
|
bool selected; // per-item flag for multi-select
|
||||||
} treeItem;
|
} treeItem;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|
@ -424,6 +438,9 @@ typedef struct WidgetT {
|
||||||
bool multiSelect;
|
bool multiSelect;
|
||||||
int32_t anchorIdx;
|
int32_t anchorIdx;
|
||||||
uint8_t *selBits;
|
uint8_t *selBits;
|
||||||
|
bool reorderable;
|
||||||
|
int32_t dragIdx;
|
||||||
|
int32_t dropIdx;
|
||||||
void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
|
void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
|
||||||
} listView;
|
} listView;
|
||||||
|
|
||||||
|
|
@ -448,6 +465,11 @@ typedef struct WidgetT {
|
||||||
int32_t scrollPosV;
|
int32_t scrollPosV;
|
||||||
int32_t scrollPosH;
|
int32_t scrollPosH;
|
||||||
} scrollPane;
|
} scrollPane;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
int32_t dividerPos; // pixels from left/top edge
|
||||||
|
bool vertical; // true = vertical divider (left|right panes)
|
||||||
|
} splitter;
|
||||||
} as;
|
} as;
|
||||||
} WidgetT;
|
} WidgetT;
|
||||||
|
|
||||||
|
|
@ -563,9 +585,13 @@ WidgetT *wgtToolbar(WidgetT *parent);
|
||||||
WidgetT *wgtTreeView(WidgetT *parent);
|
WidgetT *wgtTreeView(WidgetT *parent);
|
||||||
WidgetT *wgtTreeViewGetSelected(const WidgetT *w);
|
WidgetT *wgtTreeViewGetSelected(const WidgetT *w);
|
||||||
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item);
|
void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item);
|
||||||
|
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi);
|
||||||
|
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable);
|
||||||
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
|
||||||
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
|
||||||
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
bool wgtTreeItemIsExpanded(const WidgetT *w);
|
||||||
|
bool wgtTreeItemIsSelected(const WidgetT *w);
|
||||||
|
void wgtTreeItemSetSelected(WidgetT *w, bool selected);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ListView (multi-column list)
|
// ListView (multi-column list)
|
||||||
|
|
@ -583,6 +609,7 @@ bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx);
|
||||||
void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
||||||
void wgtListViewSelectAll(WidgetT *w);
|
void wgtListViewSelectAll(WidgetT *w);
|
||||||
void wgtListViewClearSelection(WidgetT *w);
|
void wgtListViewClearSelection(WidgetT *w);
|
||||||
|
void wgtListViewSetReorderable(WidgetT *w, bool reorderable);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ScrollPane
|
// ScrollPane
|
||||||
|
|
@ -590,6 +617,16 @@ void wgtListViewClearSelection(WidgetT *w);
|
||||||
|
|
||||||
WidgetT *wgtScrollPane(WidgetT *parent);
|
WidgetT *wgtScrollPane(WidgetT *parent);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Splitter (draggable divider between two child regions)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Create a splitter. If vertical is true, children are arranged left|right;
|
||||||
|
// if false, top|bottom. Add exactly two children.
|
||||||
|
WidgetT *wgtSplitter(WidgetT *parent, bool vertical);
|
||||||
|
void wgtSplitterSetPos(WidgetT *w, int32_t pos);
|
||||||
|
int32_t wgtSplitterGetPos(const WidgetT *w);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ImageButton
|
// ImageButton
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -712,6 +749,15 @@ bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx);
|
||||||
void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected);
|
||||||
void wgtListBoxSelectAll(WidgetT *w);
|
void wgtListBoxSelectAll(WidgetT *w);
|
||||||
void wgtListBoxClearSelection(WidgetT *w);
|
void wgtListBoxClearSelection(WidgetT *w);
|
||||||
|
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Tooltip
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Set tooltip text for a widget (NULL to remove).
|
||||||
|
// Caller owns the string — it must outlive the widget.
|
||||||
|
static inline void wgtSetTooltip(WidgetT *w, const char *text) { w->tooltip = text; }
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Debug
|
// Debug
|
||||||
|
|
|
||||||
34
dvx/dvxWm.c
34
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
|
// wmSetTitle
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -122,4 +122,10 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win);
|
||||||
// Load an icon image for a window (converts to display pixel format)
|
// Load an icon image for a window (converts to display pixel format)
|
||||||
int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d);
|
int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d);
|
||||||
|
|
||||||
|
// Allocate a standalone menu (for use as a context menu). Free with wmFreeMenu().
|
||||||
|
MenuT *wmCreateMenu(void);
|
||||||
|
|
||||||
|
// Free a standalone menu allocated with wmCreateMenu()
|
||||||
|
void wmFreeMenu(MenuT *menu);
|
||||||
|
|
||||||
#endif // DVX_WM_H
|
#endif // DVX_WM_H
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,12 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
||||||
|
|
||||||
rectFill(d, ops, titleX - 2, titleY,
|
rectFill(d, ops, titleX - 2, titleY,
|
||||||
titleW + 4, font->charHeight, bg);
|
titleW + 4, font->charHeight, bg);
|
||||||
drawTextAccel(d, ops, font, titleX, titleY,
|
|
||||||
w->as.frame.title, fg, bg, true);
|
if (!w->enabled) {
|
||||||
|
drawTextAccelEmbossed(d, ops, font, titleX, titleY, w->as.frame.title, colors);
|
||||||
|
} else {
|
||||||
|
drawTextAccel(d, ops, font, titleX, titleY, w->as.frame.title, fg, bg, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,10 +102,11 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
||||||
textY++;
|
textY++;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTextAccel(d, ops, font, textX, textY,
|
if (!w->enabled) {
|
||||||
w->as.button.text,
|
drawTextAccelEmbossed(d, ops, font, textX, textY, w->as.button.text, colors);
|
||||||
w->enabled ? fg : colors->windowShadow,
|
} else {
|
||||||
bgFace, true);
|
drawTextAccel(d, ops, font, textX, textY, w->as.button.text, fg, bgFace, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (w->focused) {
|
if (w->focused) {
|
||||||
int32_t off = w->as.button.pressed ? 1 : 0;
|
int32_t off = w->as.button.pressed ? 1 : 0;
|
||||||
|
|
|
||||||
|
|
@ -108,10 +108,11 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
int32_t cx = w->x + 3;
|
int32_t cx = w->x + 3;
|
||||||
int32_t cy = boxY + 3;
|
int32_t cy = boxY + 3;
|
||||||
int32_t cs = CHECKBOX_BOX_SIZE - 6;
|
int32_t cs = CHECKBOX_BOX_SIZE - 6;
|
||||||
|
uint32_t checkFg = w->enabled ? fg : colors->windowShadow;
|
||||||
|
|
||||||
for (int32_t i = 0; i < cs; i++) {
|
for (int32_t i = 0; i < cs; i++) {
|
||||||
drawHLine(d, ops, cx + i, cy + i, 1, fg);
|
drawHLine(d, ops, cx + i, cy + i, 1, checkFg);
|
||||||
drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, fg);
|
drawHLine(d, ops, cx + cs - 1 - i, cy + i, 1, checkFg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +120,12 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
||||||
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
||||||
int32_t labelW = textWidthAccel(font, w->as.checkbox.text);
|
int32_t labelW = textWidthAccel(font, w->as.checkbox.text);
|
||||||
|
|
||||||
|
if (!w->enabled) {
|
||||||
|
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.checkbox.text, colors);
|
||||||
|
} else {
|
||||||
drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false);
|
drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (w->focused) {
|
if (w->focused) {
|
||||||
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
||||||
|
|
|
||||||
|
|
@ -383,6 +383,19 @@ static const WidgetClassT sClassScrollPane = {
|
||||||
.setText = NULL
|
.setText = NULL
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const WidgetClassT sClassSplitter = {
|
||||||
|
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
||||||
|
.paint = widgetSplitterPaint,
|
||||||
|
.paintOverlay = NULL,
|
||||||
|
.calcMinSize = widgetSplitterCalcMinSize,
|
||||||
|
.layout = widgetSplitterLayout,
|
||||||
|
.onMouse = widgetSplitterOnMouse,
|
||||||
|
.onKey = NULL,
|
||||||
|
.destroy = NULL,
|
||||||
|
.getText = NULL,
|
||||||
|
.setText = NULL
|
||||||
|
};
|
||||||
|
|
||||||
static const WidgetClassT sClassSpinner = {
|
static const WidgetClassT sClassSpinner = {
|
||||||
.flags = WCLASS_FOCUSABLE,
|
.flags = WCLASS_FOCUSABLE,
|
||||||
.paint = widgetSpinnerPaint,
|
.paint = widgetSpinnerPaint,
|
||||||
|
|
@ -430,5 +443,6 @@ const WidgetClassT *widgetClassTable[] = {
|
||||||
[WidgetAnsiTermE] = &sClassAnsiTerm,
|
[WidgetAnsiTermE] = &sClassAnsiTerm,
|
||||||
[WidgetListViewE] = &sClassListView,
|
[WidgetListViewE] = &sClassListView,
|
||||||
[WidgetSpinnerE] = &sClassSpinner,
|
[WidgetSpinnerE] = &sClassSpinner,
|
||||||
[WidgetScrollPaneE] = &sClassScrollPane
|
[WidgetScrollPaneE] = &sClassScrollPane,
|
||||||
|
[WidgetSplitterE] = &sClassSplitter
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
// Sunken text area
|
// Sunken text area
|
||||||
|
|
@ -362,7 +362,7 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw cursor
|
// Draw cursor
|
||||||
if (w->focused && !w->as.comboBox.open) {
|
if (w->focused && w->enabled && !w->as.comboBox.open) {
|
||||||
int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth;
|
int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth;
|
||||||
|
|
||||||
if (cursorX >= w->x + TEXT_INPUT_PAD &&
|
if (cursorX >= w->x + TEXT_INPUT_PAD &&
|
||||||
|
|
@ -381,11 +381,12 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
||||||
|
|
||||||
// Down arrow
|
// Down arrow
|
||||||
|
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
||||||
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
||||||
int32_t arrowY = w->y + w->h / 2 - 1;
|
int32_t arrowY = w->y + w->h / 2 - 1;
|
||||||
|
|
||||||
for (int32_t i = 0; i < 4; i++) {
|
for (int32_t i = 0; i < 4; i++) {
|
||||||
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg);
|
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ WidgetT *sResizeListView = NULL;
|
||||||
int32_t sResizeCol = -1;
|
int32_t sResizeCol = -1;
|
||||||
int32_t sResizeStartX = 0;
|
int32_t sResizeStartX = 0;
|
||||||
int32_t sResizeOrigW = 0;
|
int32_t sResizeOrigW = 0;
|
||||||
|
WidgetT *sDragSplitter = NULL;
|
||||||
|
int32_t sDragSplitStart = 0;
|
||||||
|
WidgetT *sDragReorder = NULL;
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,14 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
|
|
||||||
// Draw selected item text
|
// Draw selected item text
|
||||||
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
|
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
|
||||||
drawText(d, ops, font, w->x + TEXT_INPUT_PAD,
|
int32_t textX = w->x + TEXT_INPUT_PAD;
|
||||||
w->y + (w->h - font->charHeight) / 2,
|
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||||
w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
|
|
||||||
|
if (!w->enabled) {
|
||||||
|
drawTextEmbossed(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], colors);
|
||||||
|
} else {
|
||||||
|
drawText(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop button
|
// Drop button
|
||||||
|
|
@ -224,11 +229,12 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
||||||
|
|
||||||
// Down arrow in button
|
// Down arrow in button
|
||||||
|
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
||||||
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
||||||
int32_t arrowY = w->y + w->h / 2 - 1;
|
int32_t arrowY = w->y + w->h / 2 - 1;
|
||||||
|
|
||||||
for (int32_t i = 0; i < 4; i++) {
|
for (int32_t i = 0; i < 4; i++) {
|
||||||
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, colors->contentFg);
|
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (w->focused) {
|
if (w->focused) {
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,11 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't dispatch keys to disabled widgets
|
||||||
|
if (!focus->enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatch to per-widget onKey handler via vtable
|
// Dispatch to per-widget onKey handler via vtable
|
||||||
if (focus->wclass && focus->wclass->onKey) {
|
if (focus->wclass && focus->wclass->onKey) {
|
||||||
focus->wclass->onKey(focus, key, mod);
|
focus->wclass->onKey(focus, key, mod);
|
||||||
|
|
@ -278,6 +283,52 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle drag-reorder release
|
||||||
|
if (sDragReorder && !(buttons & 1)) {
|
||||||
|
widgetReorderDrop(sDragReorder);
|
||||||
|
sDragReorder = NULL;
|
||||||
|
wgtInvalidatePaint(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drag-reorder move
|
||||||
|
if (sDragReorder && (buttons & 1)) {
|
||||||
|
widgetReorderUpdate(sDragReorder, root, x, y);
|
||||||
|
wgtInvalidatePaint(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle splitter drag release
|
||||||
|
if (sDragSplitter && !(buttons & 1)) {
|
||||||
|
sDragSplitter = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle splitter drag
|
||||||
|
if (sDragSplitter && (buttons & 1)) {
|
||||||
|
int32_t pos;
|
||||||
|
|
||||||
|
if (sDragSplitter->as.splitter.vertical) {
|
||||||
|
pos = x - sDragSplitter->x - sDragSplitStart;
|
||||||
|
} else {
|
||||||
|
pos = y - sDragSplitter->y - sDragSplitStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetSplitterClampPos(sDragSplitter, &pos);
|
||||||
|
|
||||||
|
if (pos != sDragSplitter->as.splitter.dividerPos) {
|
||||||
|
sDragSplitter->as.splitter.dividerPos = pos;
|
||||||
|
|
||||||
|
if (sDragSplitter->onChange) {
|
||||||
|
sDragSplitter->onChange(sDragSplitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtInvalidate(sDragSplitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle button press release
|
// Handle button press release
|
||||||
if (sPressedButton && !(buttons & 1)) {
|
if (sPressedButton && !(buttons & 1)) {
|
||||||
if (sPressedButton->type == WidgetImageButtonE) {
|
if (sPressedButton->type == WidgetImageButtonE) {
|
||||||
|
|
@ -537,3 +588,344 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetReorderDrop — finalize drag-reorder on mouse release
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetReorderDrop(WidgetT *w) {
|
||||||
|
if (w->type == WidgetListBoxE) {
|
||||||
|
int32_t drag = w->as.listBox.dragIdx;
|
||||||
|
int32_t drop = w->as.listBox.dropIdx;
|
||||||
|
w->as.listBox.dragIdx = -1;
|
||||||
|
w->as.listBox.dropIdx = -1;
|
||||||
|
|
||||||
|
if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move item at dragIdx to before dropIdx
|
||||||
|
const char *temp = w->as.listBox.items[drag];
|
||||||
|
uint8_t selBit = 0;
|
||||||
|
|
||||||
|
if (w->as.listBox.multiSelect && w->as.listBox.selBits) {
|
||||||
|
selBit = w->as.listBox.selBits[drag];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drag < drop) {
|
||||||
|
for (int32_t i = drag; i < drop - 1; i++) {
|
||||||
|
w->as.listBox.items[i] = w->as.listBox.items[i + 1];
|
||||||
|
|
||||||
|
if (w->as.listBox.selBits) {
|
||||||
|
w->as.listBox.selBits[i] = w->as.listBox.selBits[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listBox.items[drop - 1] = temp;
|
||||||
|
|
||||||
|
if (w->as.listBox.selBits) {
|
||||||
|
w->as.listBox.selBits[drop - 1] = selBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listBox.selectedIdx = drop - 1;
|
||||||
|
} else {
|
||||||
|
for (int32_t i = drag; i > drop; i--) {
|
||||||
|
w->as.listBox.items[i] = w->as.listBox.items[i - 1];
|
||||||
|
|
||||||
|
if (w->as.listBox.selBits) {
|
||||||
|
w->as.listBox.selBits[i] = w->as.listBox.selBits[i - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listBox.items[drop] = temp;
|
||||||
|
|
||||||
|
if (w->as.listBox.selBits) {
|
||||||
|
w->as.listBox.selBits[drop] = selBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listBox.selectedIdx = drop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->onChange) {
|
||||||
|
w->onChange(w);
|
||||||
|
}
|
||||||
|
} else if (w->type == WidgetListViewE) {
|
||||||
|
int32_t drag = w->as.listView.dragIdx;
|
||||||
|
int32_t drop = w->as.listView.dropIdx;
|
||||||
|
int32_t colCnt = w->as.listView.colCount;
|
||||||
|
w->as.listView.dragIdx = -1;
|
||||||
|
w->as.listView.dropIdx = -1;
|
||||||
|
|
||||||
|
if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move row at dragIdx to before dropIdx
|
||||||
|
const char *temp[LISTVIEW_MAX_COLS];
|
||||||
|
|
||||||
|
for (int32_t c = 0; c < colCnt; c++) {
|
||||||
|
temp[c] = w->as.listView.cellData[drag * colCnt + c];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t selBit = 0;
|
||||||
|
|
||||||
|
if (w->as.listView.multiSelect && w->as.listView.selBits) {
|
||||||
|
selBit = w->as.listView.selBits[drag];
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t sortVal = 0;
|
||||||
|
|
||||||
|
if (w->as.listView.sortIndex) {
|
||||||
|
sortVal = w->as.listView.sortIndex[drag];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drag < drop) {
|
||||||
|
for (int32_t i = drag; i < drop - 1; i++) {
|
||||||
|
for (int32_t c = 0; c < colCnt; c++) {
|
||||||
|
w->as.listView.cellData[i * colCnt + c] = w->as.listView.cellData[(i + 1) * colCnt + c];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.selBits) {
|
||||||
|
w->as.listView.selBits[i] = w->as.listView.selBits[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.sortIndex) {
|
||||||
|
w->as.listView.sortIndex[i] = w->as.listView.sortIndex[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t dest = drop - 1;
|
||||||
|
|
||||||
|
for (int32_t c = 0; c < colCnt; c++) {
|
||||||
|
w->as.listView.cellData[dest * colCnt + c] = temp[c];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.selBits) {
|
||||||
|
w->as.listView.selBits[dest] = selBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.sortIndex) {
|
||||||
|
w->as.listView.sortIndex[dest] = sortVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listView.selectedIdx = dest;
|
||||||
|
} else {
|
||||||
|
for (int32_t i = drag; i > drop; i--) {
|
||||||
|
for (int32_t c = 0; c < colCnt; c++) {
|
||||||
|
w->as.listView.cellData[i * colCnt + c] = w->as.listView.cellData[(i - 1) * colCnt + c];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.selBits) {
|
||||||
|
w->as.listView.selBits[i] = w->as.listView.selBits[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.sortIndex) {
|
||||||
|
w->as.listView.sortIndex[i] = w->as.listView.sortIndex[i - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t c = 0; c < colCnt; c++) {
|
||||||
|
w->as.listView.cellData[drop * colCnt + c] = temp[c];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.selBits) {
|
||||||
|
w->as.listView.selBits[drop] = selBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->as.listView.sortIndex) {
|
||||||
|
w->as.listView.sortIndex[drop] = sortVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listView.selectedIdx = drop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->onChange) {
|
||||||
|
w->onChange(w);
|
||||||
|
}
|
||||||
|
} else if (w->type == WidgetTreeViewE) {
|
||||||
|
WidgetT *drag = w->as.treeView.dragItem;
|
||||||
|
WidgetT *target = w->as.treeView.dropTarget;
|
||||||
|
bool after = w->as.treeView.dropAfter;
|
||||||
|
w->as.treeView.dragItem = NULL;
|
||||||
|
w->as.treeView.dropTarget = NULL;
|
||||||
|
|
||||||
|
if (!drag || !target || drag == target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlink drag from its current parent
|
||||||
|
WidgetT *oldParent = drag->parent;
|
||||||
|
|
||||||
|
if (oldParent->firstChild == drag) {
|
||||||
|
oldParent->firstChild = drag->nextSibling;
|
||||||
|
} else {
|
||||||
|
for (WidgetT *c = oldParent->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->nextSibling == drag) {
|
||||||
|
c->nextSibling = drag->nextSibling;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drag->nextSibling = NULL;
|
||||||
|
|
||||||
|
// Insert drag before or after target (same parent level)
|
||||||
|
WidgetT *newParent = target->parent;
|
||||||
|
drag->parent = newParent;
|
||||||
|
|
||||||
|
if (after) {
|
||||||
|
drag->nextSibling = target->nextSibling;
|
||||||
|
target->nextSibling = drag;
|
||||||
|
} else {
|
||||||
|
if (newParent->firstChild == target) {
|
||||||
|
drag->nextSibling = target;
|
||||||
|
newParent->firstChild = drag;
|
||||||
|
} else {
|
||||||
|
for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->nextSibling == target) {
|
||||||
|
c->nextSibling = drag;
|
||||||
|
drag->nextSibling = target;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w->onChange) {
|
||||||
|
w->onChange(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetReorderUpdate — update drop position during drag
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
||||||
|
(void)x;
|
||||||
|
AppContextT *ctx = (AppContextT *)root->userData;
|
||||||
|
const BitmapFontT *font = &ctx->font;
|
||||||
|
|
||||||
|
if (w->type == WidgetListBoxE) {
|
||||||
|
int32_t innerY = w->y + LISTBOX_BORDER;
|
||||||
|
int32_t innerH = w->h - LISTBOX_BORDER * 2;
|
||||||
|
int32_t visibleRows = innerH / font->charHeight;
|
||||||
|
int32_t maxScroll = w->as.listBox.itemCount - visibleRows;
|
||||||
|
|
||||||
|
if (maxScroll < 0) {
|
||||||
|
maxScroll = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll when dragging near edges
|
||||||
|
if (y < innerY + font->charHeight && w->as.listBox.scrollPos > 0) {
|
||||||
|
w->as.listBox.scrollPos--;
|
||||||
|
} else if (y > innerY + innerH - font->charHeight && w->as.listBox.scrollPos < maxScroll) {
|
||||||
|
w->as.listBox.scrollPos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t relY = y - innerY;
|
||||||
|
int32_t row = w->as.listBox.scrollPos + relY / font->charHeight;
|
||||||
|
int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0;
|
||||||
|
int32_t drop = row + halfRow;
|
||||||
|
|
||||||
|
if (drop < 0) {
|
||||||
|
drop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drop > w->as.listBox.itemCount) {
|
||||||
|
drop = w->as.listBox.itemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listBox.dropIdx = drop;
|
||||||
|
} else if (w->type == WidgetListViewE) {
|
||||||
|
int32_t headerH = font->charHeight + 4;
|
||||||
|
int32_t innerY = w->y + LISTVIEW_BORDER + headerH;
|
||||||
|
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
|
||||||
|
int32_t visibleRows = innerH / font->charHeight;
|
||||||
|
int32_t maxScroll = w->as.listView.rowCount - visibleRows;
|
||||||
|
|
||||||
|
if (maxScroll < 0) {
|
||||||
|
maxScroll = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll when dragging near edges
|
||||||
|
if (y < innerY + font->charHeight && w->as.listView.scrollPos > 0) {
|
||||||
|
w->as.listView.scrollPos--;
|
||||||
|
} else if (y > innerY + innerH - font->charHeight && w->as.listView.scrollPos < maxScroll) {
|
||||||
|
w->as.listView.scrollPos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t relY = y - innerY;
|
||||||
|
int32_t row = w->as.listView.scrollPos + relY / font->charHeight;
|
||||||
|
int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0;
|
||||||
|
int32_t drop = row + halfRow;
|
||||||
|
|
||||||
|
if (drop < 0) {
|
||||||
|
drop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drop > w->as.listView.rowCount) {
|
||||||
|
drop = w->as.listView.rowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listView.dropIdx = drop;
|
||||||
|
} else if (w->type == WidgetTreeViewE) {
|
||||||
|
int32_t innerY = w->y + TREE_BORDER;
|
||||||
|
int32_t innerH = w->h - TREE_BORDER * 2;
|
||||||
|
|
||||||
|
// Auto-scroll when dragging near edges (pixel-based scroll)
|
||||||
|
if (y < innerY + font->charHeight && w->as.treeView.scrollPos > 0) {
|
||||||
|
w->as.treeView.scrollPos -= font->charHeight;
|
||||||
|
|
||||||
|
if (w->as.treeView.scrollPos < 0) {
|
||||||
|
w->as.treeView.scrollPos = 0;
|
||||||
|
}
|
||||||
|
} else if (y > innerY + innerH - font->charHeight) {
|
||||||
|
w->as.treeView.scrollPos += font->charHeight;
|
||||||
|
// Paint will clamp to actual max
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find which visible item the mouse is over
|
||||||
|
int32_t curY = w->y + TREE_BORDER - w->as.treeView.scrollPos;
|
||||||
|
|
||||||
|
// Walk visible items to find drop target
|
||||||
|
WidgetT *first = NULL;
|
||||||
|
|
||||||
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTreeItemE && c->visible) {
|
||||||
|
first = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetT *target = NULL;
|
||||||
|
bool after = false;
|
||||||
|
|
||||||
|
for (WidgetT *v = first; v; v = widgetTreeViewNextVisible(v, w)) {
|
||||||
|
int32_t itemBot = curY + font->charHeight;
|
||||||
|
int32_t mid = curY + font->charHeight / 2;
|
||||||
|
|
||||||
|
if (y < mid) {
|
||||||
|
target = v;
|
||||||
|
after = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
curY = itemBot;
|
||||||
|
|
||||||
|
// Check if mouse is between this item and next
|
||||||
|
WidgetT *next = widgetTreeViewNextVisible(v, w);
|
||||||
|
|
||||||
|
if (!next || y < itemBot) {
|
||||||
|
target = v;
|
||||||
|
after = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.treeView.dropTarget = target;
|
||||||
|
w->as.treeView.dropAfter = after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,11 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
|
||||||
(void)font;
|
(void)font;
|
||||||
|
|
||||||
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
||||||
|
bool pressed = w->as.imageButton.pressed && w->enabled;
|
||||||
|
|
||||||
BevelStyleT bevel;
|
BevelStyleT bevel;
|
||||||
bevel.highlight = w->as.imageButton.pressed ? colors->windowShadow : colors->windowHighlight;
|
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
|
||||||
bevel.shadow = w->as.imageButton.pressed ? colors->windowHighlight : colors->windowShadow;
|
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
|
||||||
bevel.face = bgFace;
|
bevel.face = bgFace;
|
||||||
bevel.width = 2;
|
bevel.width = 2;
|
||||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||||
|
|
@ -116,7 +117,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
|
||||||
int32_t imgX = w->x + (w->w - w->as.imageButton.imgW) / 2;
|
int32_t imgX = w->x + (w->w - w->as.imageButton.imgW) / 2;
|
||||||
int32_t imgY = w->y + (w->h - w->as.imageButton.imgH) / 2;
|
int32_t imgY = w->y + (w->h - w->as.imageButton.imgH) / 2;
|
||||||
|
|
||||||
if (w->as.imageButton.pressed) {
|
if (pressed) {
|
||||||
imgX++;
|
imgX++;
|
||||||
imgY++;
|
imgY++;
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +129,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
|
||||||
|
|
||||||
if (w->focused) {
|
if (w->focused) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
int32_t off = w->as.imageButton.pressed ? 1 : 0;
|
int32_t off = pressed ? 1 : 0;
|
||||||
drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg);
|
drawFocusRect(d, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ extern const WidgetClassT *widgetClassTable[];
|
||||||
#define TAB_PAD_H 8
|
#define TAB_PAD_H 8
|
||||||
#define TAB_PAD_V 4
|
#define TAB_PAD_V 4
|
||||||
#define TAB_BORDER 2
|
#define TAB_BORDER 2
|
||||||
|
#define LISTBOX_BORDER 2
|
||||||
|
#define LISTVIEW_BORDER 2
|
||||||
#define TREE_INDENT 16
|
#define TREE_INDENT 16
|
||||||
#define TREE_EXPAND_SIZE 9
|
#define TREE_EXPAND_SIZE 9
|
||||||
#define TREE_ICON_GAP 4
|
#define TREE_ICON_GAP 4
|
||||||
|
|
@ -70,6 +72,8 @@ extern const WidgetClassT *widgetClassTable[];
|
||||||
#define TREE_SB_W 14
|
#define TREE_SB_W 14
|
||||||
#define TREE_MIN_ROWS 4
|
#define TREE_MIN_ROWS 4
|
||||||
#define SB_MIN_THUMB 14
|
#define SB_MIN_THUMB 14
|
||||||
|
#define SPLITTER_BAR_W 5
|
||||||
|
#define SPLITTER_MIN_PANE 20
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Inline helpers
|
// Inline helpers
|
||||||
|
|
@ -81,6 +85,18 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Classic Windows 3.1 embossed (etched) text for disabled widgets:
|
||||||
|
// Draw text at +1,+1 in highlight, then at 0,0 in shadow.
|
||||||
|
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
|
||||||
|
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
||||||
|
drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
|
||||||
|
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
||||||
|
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Shared state (defined in widgetCore.c)
|
// Shared state (defined in widgetCore.c)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -99,6 +115,9 @@ extern WidgetT *sResizeListView;
|
||||||
extern int32_t sResizeCol;
|
extern int32_t sResizeCol;
|
||||||
extern int32_t sResizeStartX;
|
extern int32_t sResizeStartX;
|
||||||
extern int32_t sResizeOrigW;
|
extern int32_t sResizeOrigW;
|
||||||
|
extern WidgetT *sDragSplitter;
|
||||||
|
extern int32_t sDragSplitStart;
|
||||||
|
extern WidgetT *sDragReorder;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Core functions (widgetCore.c)
|
// Core functions (widgetCore.c)
|
||||||
|
|
@ -171,6 +190,7 @@ void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
|
||||||
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
|
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
||||||
|
|
@ -199,6 +219,7 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
|
void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
|
|
@ -212,6 +233,7 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font);
|
void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font);
|
||||||
|
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
|
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
|
||||||
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
|
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font);
|
||||||
|
|
||||||
|
|
@ -282,6 +304,8 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
void widgetSplitterClampPos(WidgetT *w, int32_t *pos);
|
||||||
|
void widgetSplitterOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
|
|
@ -301,4 +325,9 @@ int32_t wordStart(const char *buf, int32_t pos);
|
||||||
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod);
|
||||||
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
|
||||||
|
|
||||||
|
// Drag-reorder helpers (dispatch to ListBox/ListView/TreeView)
|
||||||
|
void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
|
||||||
|
void widgetReorderDrop(WidgetT *w);
|
||||||
|
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView);
|
||||||
|
|
||||||
#endif // WIDGET_INTERNAL_H
|
#endif // WIDGET_INTERNAL_H
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,15 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
|
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||||
|
|
||||||
|
if (!w->enabled) {
|
||||||
|
drawTextAccelEmbossed(d, ops, font, w->x, textY, w->as.label.text, colors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
drawTextAccel(d, ops, font, w->x, w->y + (w->h - font->charHeight) / 2,
|
drawTextAccel(d, ops, font, w->x, textY, w->as.label.text, fg, bg, false);
|
||||||
w->as.label.text, fg, bg, false);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define LISTBOX_BORDER 2
|
|
||||||
#define LISTBOX_PAD 2
|
#define LISTBOX_PAD 2
|
||||||
#define LISTBOX_MIN_ROWS 4
|
#define LISTBOX_MIN_ROWS 4
|
||||||
#define LISTBOX_SB_W 14
|
#define LISTBOX_SB_W 14
|
||||||
|
|
@ -112,6 +111,8 @@ WidgetT *wgtListBox(WidgetT *parent) {
|
||||||
if (w) {
|
if (w) {
|
||||||
w->as.listBox.selectedIdx = -1;
|
w->as.listBox.selectedIdx = -1;
|
||||||
w->as.listBox.anchorIdx = -1;
|
w->as.listBox.anchorIdx = -1;
|
||||||
|
w->as.listBox.dragIdx = -1;
|
||||||
|
w->as.listBox.dropIdx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return w;
|
return w;
|
||||||
|
|
@ -240,6 +241,19 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtListBoxSetReorderable
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) {
|
||||||
|
if (!w || w->type != WidgetListBoxE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listBox.reorderable = reorderable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtListBoxSetMultiSelect
|
// wgtListBoxSetMultiSelect
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -526,6 +540,13 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
||||||
hit->onDblClick(hit);
|
hit->onDblClick(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initiate drag-reorder if enabled (not from modifier clicks)
|
||||||
|
if (hit->as.listBox.reorderable && !shift && !ctrl) {
|
||||||
|
hit->as.listBox.dragIdx = idx;
|
||||||
|
hit->as.listBox.dropIdx = idx;
|
||||||
|
sDragReorder = hit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -594,6 +615,17 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw drag-reorder insertion indicator
|
||||||
|
if (w->as.listBox.reorderable && w->as.listBox.dragIdx >= 0 && w->as.listBox.dropIdx >= 0) {
|
||||||
|
int32_t drop = w->as.listBox.dropIdx;
|
||||||
|
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
|
||||||
|
|
||||||
|
if (lineY >= innerY && lineY <= innerY + innerH) {
|
||||||
|
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY, contentW, colors->contentFg);
|
||||||
|
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY + 1, contentW, colors->contentFg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw scrollbar
|
// Draw scrollbar
|
||||||
if (needSb) {
|
if (needSb) {
|
||||||
int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W;
|
int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define LISTVIEW_BORDER 2
|
|
||||||
#define LISTVIEW_PAD 3
|
#define LISTVIEW_PAD 3
|
||||||
#define LISTVIEW_SB_W 14
|
#define LISTVIEW_SB_W 14
|
||||||
#define LISTVIEW_MIN_ROWS 4
|
#define LISTVIEW_MIN_ROWS 4
|
||||||
|
|
@ -349,6 +348,8 @@ WidgetT *wgtListView(WidgetT *parent) {
|
||||||
w->as.listView.anchorIdx = -1;
|
w->as.listView.anchorIdx = -1;
|
||||||
w->as.listView.sortCol = -1;
|
w->as.listView.sortCol = -1;
|
||||||
w->as.listView.sortDir = ListViewSortNoneE;
|
w->as.listView.sortDir = ListViewSortNoneE;
|
||||||
|
w->as.listView.dragIdx = -1;
|
||||||
|
w->as.listView.dropIdx = -1;
|
||||||
w->weight = 100;
|
w->weight = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -506,6 +507,30 @@ void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtListViewSetReorderable
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
|
||||||
|
if (!w || w->type != WidgetListViewE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.listView.reorderable = reorderable;
|
||||||
|
|
||||||
|
// Disable sorting when reorderable — sort order conflicts with manual order
|
||||||
|
if (reorderable) {
|
||||||
|
w->as.listView.sortCol = -1;
|
||||||
|
w->as.listView.sortDir = ListViewSortNoneE;
|
||||||
|
|
||||||
|
if (w->as.listView.sortIndex) {
|
||||||
|
free(w->as.listView.sortIndex);
|
||||||
|
w->as.listView.sortIndex = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtListViewSetMultiSelect
|
// wgtListViewSetMultiSelect
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -917,7 +942,8 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
colX += cw;
|
colX += cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not on a border — check for sort click
|
// Not on a border — check for sort click (disabled when reorderable)
|
||||||
|
if (!hit->as.listView.reorderable) {
|
||||||
colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH;
|
colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH;
|
||||||
|
|
||||||
for (int32_t c = 0; c < hit->as.listView.colCount; c++) {
|
for (int32_t c = 0; c < hit->as.listView.colCount; c++) {
|
||||||
|
|
@ -948,6 +974,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
|
|
||||||
colX += cw;
|
colX += cw;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1024,6 +1051,13 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
||||||
hit->onDblClick(hit);
|
hit->onDblClick(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initiate drag-reorder if enabled (not from modifier clicks)
|
||||||
|
if (hit->as.listView.reorderable && !shift && !ctrl) {
|
||||||
|
hit->as.listView.dragIdx = dataRow;
|
||||||
|
hit->as.listView.dropIdx = dataRow;
|
||||||
|
sDragReorder = hit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wgtInvalidatePaint(hit);
|
wgtInvalidatePaint(hit);
|
||||||
|
|
@ -1249,6 +1283,17 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw drag-reorder insertion indicator
|
||||||
|
if (w->as.listView.reorderable && w->as.listView.dragIdx >= 0 && w->as.listView.dropIdx >= 0) {
|
||||||
|
int32_t drop = w->as.listView.dropIdx;
|
||||||
|
int32_t lineY = dataY + (drop - w->as.listView.scrollPos) * font->charHeight;
|
||||||
|
|
||||||
|
if (lineY >= dataY && lineY <= dataY + innerH) {
|
||||||
|
drawHLine(d, ops, baseX, lineY, innerW, fg);
|
||||||
|
drawHLine(d, ops, baseX, lineY + 1, innerW, fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,7 @@ void wgtSetDebugLayout(bool enabled) {
|
||||||
void wgtSetEnabled(WidgetT *w, bool enabled) {
|
void wgtSetEnabled(WidgetT *w, bool enabled) {
|
||||||
if (w) {
|
if (w) {
|
||||||
w->enabled = enabled;
|
w->enabled = enabled;
|
||||||
|
wgtInvalidatePaint(w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
|
|
||||||
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->activeTitleBg;
|
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->activeTitleBg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
// Sunken border
|
// Sunken border
|
||||||
|
|
|
||||||
|
|
@ -210,16 +210,25 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
||||||
// Draw filled diamond if selected
|
// Draw filled diamond if selected
|
||||||
if (w->parent && w->parent->type == WidgetRadioGroupE &&
|
if (w->parent && w->parent->type == WidgetRadioGroupE &&
|
||||||
w->parent->as.radioGroup.selectedIdx == w->as.radio.index) {
|
w->parent->as.radioGroup.selectedIdx == w->as.radio.index) {
|
||||||
for (int32_t i = -2; i <= 2; i++) {
|
uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
|
||||||
int32_t span = 3 - (i < 0 ? -i : i);
|
|
||||||
drawHLine(d, ops, bx + mid - span, boxY + mid + i, span * 2 + 1, fg);
|
static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < 6; i++) {
|
||||||
|
int32_t dw = dotW[i];
|
||||||
|
drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP;
|
||||||
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
||||||
int32_t labelW = textWidthAccel(font, w->as.radio.text);
|
int32_t labelW = textWidthAccel(font, w->as.radio.text);
|
||||||
|
|
||||||
|
if (!w->enabled) {
|
||||||
|
drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.radio.text, colors);
|
||||||
|
} else {
|
||||||
drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
|
drawTextAccel(d, ops, font, labelX, labelY, w->as.radio.text, fg, bg, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (w->focused) {
|
if (w->focused) {
|
||||||
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,8 @@ void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
(void)font;
|
(void)font;
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||||
|
uint32_t tickFg = w->enabled ? fg : colors->windowShadow;
|
||||||
|
uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough;
|
||||||
|
|
||||||
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
|
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
|
||||||
|
|
||||||
|
|
@ -219,12 +221,12 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
||||||
BevelStyleT thumb;
|
BevelStyleT thumb;
|
||||||
thumb.highlight = colors->windowHighlight;
|
thumb.highlight = colors->windowHighlight;
|
||||||
thumb.shadow = colors->windowShadow;
|
thumb.shadow = colors->windowShadow;
|
||||||
thumb.face = colors->buttonFace;
|
thumb.face = thumbFg;
|
||||||
thumb.width = 2;
|
thumb.width = 2;
|
||||||
drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);
|
drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);
|
||||||
|
|
||||||
// Center tick on thumb
|
// Center tick on thumb
|
||||||
drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, fg);
|
drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, tickFg);
|
||||||
} else {
|
} else {
|
||||||
// Track groove
|
// Track groove
|
||||||
int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
|
int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
|
||||||
|
|
@ -242,12 +244,12 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
||||||
BevelStyleT thumb;
|
BevelStyleT thumb;
|
||||||
thumb.highlight = colors->windowHighlight;
|
thumb.highlight = colors->windowHighlight;
|
||||||
thumb.shadow = colors->windowShadow;
|
thumb.shadow = colors->windowShadow;
|
||||||
thumb.face = colors->buttonFace;
|
thumb.face = thumbFg;
|
||||||
thumb.width = 2;
|
thumb.width = 2;
|
||||||
drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);
|
drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);
|
||||||
|
|
||||||
// Center tick on thumb
|
// Center tick on thumb
|
||||||
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, fg);
|
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, tickFg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (w->focused) {
|
if (w->focused) {
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
int32_t btnW = SPINNER_BTN_W;
|
int32_t btnW = SPINNER_BTN_W;
|
||||||
|
|
|
||||||
335
dvx/widgets/widgetSplitter.c
Normal file
335
dvx/widgets/widgetSplitter.c
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
// widgetSplitter.c — Splitter (draggable divider between two panes)
|
||||||
|
|
||||||
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Prototypes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static WidgetT *spFirstChild(WidgetT *w);
|
||||||
|
static WidgetT *spSecondChild(WidgetT *w);
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// spFirstChild — get first visible child
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static WidgetT *spFirstChild(WidgetT *w) {
|
||||||
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->visible) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// spSecondChild — get second visible child
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static WidgetT *spSecondChild(WidgetT *w) {
|
||||||
|
int32_t n = 0;
|
||||||
|
|
||||||
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->visible) {
|
||||||
|
n++;
|
||||||
|
|
||||||
|
if (n == 2) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetSplitterClampPos — clamp divider to child minimums
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetSplitterClampPos(WidgetT *w, int32_t *pos) {
|
||||||
|
WidgetT *c1 = spFirstChild(w);
|
||||||
|
WidgetT *c2 = spSecondChild(w);
|
||||||
|
bool vert = w->as.splitter.vertical;
|
||||||
|
int32_t totalSize = vert ? w->w : w->h;
|
||||||
|
|
||||||
|
int32_t minFirst = c1 ? (vert ? c1->calcMinW : c1->calcMinH) : SPLITTER_MIN_PANE;
|
||||||
|
int32_t minSecond = c2 ? (vert ? c2->calcMinW : c2->calcMinH) : SPLITTER_MIN_PANE;
|
||||||
|
|
||||||
|
if (minFirst < SPLITTER_MIN_PANE) {
|
||||||
|
minFirst = SPLITTER_MIN_PANE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minSecond < SPLITTER_MIN_PANE) {
|
||||||
|
minSecond = SPLITTER_MIN_PANE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t maxPos = totalSize - SPLITTER_BAR_W - minSecond;
|
||||||
|
|
||||||
|
if (maxPos < minFirst) {
|
||||||
|
maxPos = minFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pos = clampInt(*pos, minFirst, maxPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetSplitterCalcMinSize
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
|
// Recursively measure children
|
||||||
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
widgetCalcMinSizeTree(c, font);
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetT *c1 = spFirstChild(w);
|
||||||
|
WidgetT *c2 = spSecondChild(w);
|
||||||
|
int32_t m1w = c1 ? c1->calcMinW : 0;
|
||||||
|
int32_t m1h = c1 ? c1->calcMinH : 0;
|
||||||
|
int32_t m2w = c2 ? c2->calcMinW : 0;
|
||||||
|
int32_t m2h = c2 ? c2->calcMinH : 0;
|
||||||
|
|
||||||
|
if (w->as.splitter.vertical) {
|
||||||
|
w->calcMinW = m1w + m2w + SPLITTER_BAR_W;
|
||||||
|
w->calcMinH = DVX_MAX(m1h, m2h);
|
||||||
|
} else {
|
||||||
|
w->calcMinW = DVX_MAX(m1w, m2w);
|
||||||
|
w->calcMinH = m1h + m2h + SPLITTER_BAR_W;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetSplitterLayout
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
|
WidgetT *c1 = spFirstChild(w);
|
||||||
|
WidgetT *c2 = spSecondChild(w);
|
||||||
|
int32_t pos = w->as.splitter.dividerPos;
|
||||||
|
|
||||||
|
widgetSplitterClampPos(w, &pos);
|
||||||
|
w->as.splitter.dividerPos = pos;
|
||||||
|
|
||||||
|
if (w->as.splitter.vertical) {
|
||||||
|
// Left pane
|
||||||
|
if (c1) {
|
||||||
|
c1->x = w->x;
|
||||||
|
c1->y = w->y;
|
||||||
|
c1->w = pos;
|
||||||
|
c1->h = w->h;
|
||||||
|
widgetLayoutChildren(c1, font);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right pane
|
||||||
|
if (c2) {
|
||||||
|
c2->x = w->x + pos + SPLITTER_BAR_W;
|
||||||
|
c2->y = w->y;
|
||||||
|
c2->w = w->w - pos - SPLITTER_BAR_W;
|
||||||
|
c2->h = w->h;
|
||||||
|
|
||||||
|
if (c2->w < 0) {
|
||||||
|
c2->w = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetLayoutChildren(c2, font);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Top pane
|
||||||
|
if (c1) {
|
||||||
|
c1->x = w->x;
|
||||||
|
c1->y = w->y;
|
||||||
|
c1->w = w->w;
|
||||||
|
c1->h = pos;
|
||||||
|
widgetLayoutChildren(c1, font);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom pane
|
||||||
|
if (c2) {
|
||||||
|
c2->x = w->x;
|
||||||
|
c2->y = w->y + pos + SPLITTER_BAR_W;
|
||||||
|
c2->w = w->w;
|
||||||
|
c2->h = w->h - pos - SPLITTER_BAR_W;
|
||||||
|
|
||||||
|
if (c2->h < 0) {
|
||||||
|
c2->h = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetLayoutChildren(c2, font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetSplitterOnMouse
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
|
int32_t pos = hit->as.splitter.dividerPos;
|
||||||
|
|
||||||
|
// Check if click is on the divider bar
|
||||||
|
bool onDivider;
|
||||||
|
|
||||||
|
if (hit->as.splitter.vertical) {
|
||||||
|
int32_t barX = hit->x + pos;
|
||||||
|
onDivider = (vx >= barX && vx < barX + SPLITTER_BAR_W);
|
||||||
|
} else {
|
||||||
|
int32_t barY = hit->y + pos;
|
||||||
|
onDivider = (vy >= barY && vy < barY + SPLITTER_BAR_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDivider) {
|
||||||
|
// Start dragging
|
||||||
|
sDragSplitter = hit;
|
||||||
|
|
||||||
|
if (hit->as.splitter.vertical) {
|
||||||
|
sDragSplitStart = vx - hit->x - pos;
|
||||||
|
} else {
|
||||||
|
sDragSplitStart = vy - hit->y - pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward click to child widgets
|
||||||
|
WidgetT *child = NULL;
|
||||||
|
|
||||||
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||||
|
WidgetT *ch = widgetHitTest(c, vx, vy);
|
||||||
|
|
||||||
|
if (ch) {
|
||||||
|
child = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child && child->enabled && child->wclass && child->wclass->onMouse) {
|
||||||
|
if (sFocusedWidget && sFocusedWidget != child) {
|
||||||
|
sFocusedWidget->focused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
child->wclass->onMouse(child, root, vx, vy);
|
||||||
|
|
||||||
|
if (child->focused) {
|
||||||
|
sFocusedWidget = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtInvalidatePaint(hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetSplitterPaint
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
|
int32_t pos = w->as.splitter.dividerPos;
|
||||||
|
|
||||||
|
// Paint first child with clip rect
|
||||||
|
WidgetT *c1 = spFirstChild(w);
|
||||||
|
WidgetT *c2 = spSecondChild(w);
|
||||||
|
|
||||||
|
int32_t oldClipX = d->clipX;
|
||||||
|
int32_t oldClipY = d->clipY;
|
||||||
|
int32_t oldClipW = d->clipW;
|
||||||
|
int32_t oldClipH = d->clipH;
|
||||||
|
|
||||||
|
if (c1) {
|
||||||
|
if (w->as.splitter.vertical) {
|
||||||
|
setClipRect(d, w->x, w->y, pos, w->h);
|
||||||
|
} else {
|
||||||
|
setClipRect(d, w->x, w->y, w->w, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetPaintOne(c1, d, ops, font, colors);
|
||||||
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c2) {
|
||||||
|
if (w->as.splitter.vertical) {
|
||||||
|
setClipRect(d, w->x + pos + SPLITTER_BAR_W, w->y, w->w - pos - SPLITTER_BAR_W, w->h);
|
||||||
|
} else {
|
||||||
|
setClipRect(d, w->x, w->y + pos + SPLITTER_BAR_W, w->w, w->h - pos - SPLITTER_BAR_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetPaintOne(c2, d, ops, font, colors);
|
||||||
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw divider bar — raised bevel
|
||||||
|
BevelStyleT bevel = BEVEL_RAISED(colors, 1);
|
||||||
|
|
||||||
|
if (w->as.splitter.vertical) {
|
||||||
|
int32_t barX = w->x + pos;
|
||||||
|
drawBevel(d, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel);
|
||||||
|
|
||||||
|
// Gripper — row of embossed 2x2 bumps centered vertically
|
||||||
|
int32_t gx = barX + 1;
|
||||||
|
int32_t midY = w->y + w->h / 2;
|
||||||
|
int32_t count = 5;
|
||||||
|
|
||||||
|
for (int32_t i = -count; i <= count; i++) {
|
||||||
|
int32_t dy = midY + i * 3;
|
||||||
|
drawHLine(d, ops, gx, dy, 2, colors->windowHighlight);
|
||||||
|
drawHLine(d, ops, gx + 1, dy + 1, 2, colors->windowShadow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int32_t barY = w->y + pos;
|
||||||
|
drawBevel(d, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel);
|
||||||
|
|
||||||
|
// Gripper — row of embossed 2x2 bumps centered horizontally
|
||||||
|
int32_t gy = barY + 1;
|
||||||
|
int32_t midX = w->x + w->w / 2;
|
||||||
|
int32_t count = 5;
|
||||||
|
|
||||||
|
for (int32_t i = -count; i <= count; i++) {
|
||||||
|
int32_t dx = midX + i * 3;
|
||||||
|
drawHLine(d, ops, dx, gy, 1, colors->windowHighlight);
|
||||||
|
drawHLine(d, ops, dx + 1, gy, 1, colors->windowShadow);
|
||||||
|
drawHLine(d, ops, dx, gy + 1, 1, colors->windowHighlight);
|
||||||
|
drawHLine(d, ops, dx + 1, gy + 1, 1, colors->windowShadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtSplitter
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
WidgetT *wgtSplitter(WidgetT *parent, bool vertical) {
|
||||||
|
WidgetT *w = widgetAlloc(parent, WidgetSplitterE);
|
||||||
|
|
||||||
|
if (w) {
|
||||||
|
w->as.splitter.vertical = vertical;
|
||||||
|
w->as.splitter.dividerPos = 0;
|
||||||
|
w->weight = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtSplitterGetPos
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
int32_t wgtSplitterGetPos(const WidgetT *w) {
|
||||||
|
return w->as.splitter.dividerPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtSplitterSetPos
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtSplitterSetPos(WidgetT *w, int32_t pos) {
|
||||||
|
w->as.splitter.dividerPos = pos;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,118 @@
|
||||||
|
|
||||||
#include "widgetInternal.h"
|
#include "widgetInternal.h"
|
||||||
|
|
||||||
|
#define TAB_ARROW_W 16
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Prototypes
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void tabClosePopup(void);
|
||||||
|
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font);
|
||||||
|
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font);
|
||||||
|
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font);
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// tabClosePopup — close any open dropdown/combobox popup
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void tabClosePopup(void) {
|
||||||
|
if (sOpenPopup) {
|
||||||
|
if (sOpenPopup->type == WidgetDropdownE) {
|
||||||
|
sOpenPopup->as.dropdown.open = false;
|
||||||
|
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
||||||
|
sOpenPopup->as.comboBox.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sOpenPopup = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// tabEnsureVisible — scroll so active tab is visible
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
|
||||||
|
if (!tabNeedScroll(w, font)) {
|
||||||
|
w->as.tabControl.scrollOffset = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
|
||||||
|
|
||||||
|
if (headerW < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find start and end X of the active tab
|
||||||
|
int32_t tabX = 0;
|
||||||
|
int32_t tabIdx = 0;
|
||||||
|
|
||||||
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type != WidgetTabPageE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||||
|
|
||||||
|
if (tabIdx == w->as.tabControl.activeTab) {
|
||||||
|
int32_t tabLeft = tabX - w->as.tabControl.scrollOffset;
|
||||||
|
int32_t tabRight = tabLeft + tw;
|
||||||
|
|
||||||
|
if (tabLeft < 0) {
|
||||||
|
w->as.tabControl.scrollOffset += tabLeft;
|
||||||
|
} else if (tabRight > headerW) {
|
||||||
|
w->as.tabControl.scrollOffset += tabRight - headerW;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabX += tw;
|
||||||
|
tabIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
int32_t totalW = tabHeaderTotalW(w, font);
|
||||||
|
int32_t maxOff = totalW - headerW;
|
||||||
|
|
||||||
|
if (maxOff < 0) {
|
||||||
|
maxOff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// tabHeaderTotalW — total width of all tab headers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) {
|
||||||
|
int32_t total = 0;
|
||||||
|
|
||||||
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type == WidgetTabPageE) {
|
||||||
|
total += textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// tabNeedScroll — do tab headers overflow?
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font) {
|
||||||
|
int32_t totalW = tabHeaderTotalW(w, font);
|
||||||
|
return totalW > (w->w - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wgtTabControl
|
// wgtTabControl
|
||||||
|
|
@ -12,6 +124,7 @@ WidgetT *wgtTabControl(WidgetT *parent) {
|
||||||
|
|
||||||
if (w) {
|
if (w) {
|
||||||
w->as.tabControl.activeTab = 0;
|
w->as.tabControl.activeTab = 0;
|
||||||
|
w->as.tabControl.scrollOffset = 0;
|
||||||
w->weight = 100;
|
w->weight = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +182,6 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||||
int32_t maxPageW = 0;
|
int32_t maxPageW = 0;
|
||||||
int32_t maxPageH = 0;
|
int32_t maxPageH = 0;
|
||||||
int32_t tabHeaderW = 0;
|
|
||||||
|
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
if (c->type != WidgetTabPageE) {
|
if (c->type != WidgetTabPageE) {
|
||||||
|
|
@ -79,9 +191,6 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
widgetCalcMinSizeTree(c, font);
|
widgetCalcMinSizeTree(c, font);
|
||||||
maxPageW = DVX_MAX(maxPageW, c->calcMinW);
|
maxPageW = DVX_MAX(maxPageW, c->calcMinW);
|
||||||
maxPageH = DVX_MAX(maxPageH, c->calcMinH);
|
maxPageH = DVX_MAX(maxPageH, c->calcMinH);
|
||||||
|
|
||||||
int32_t labelW = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
|
||||||
tabHeaderW += labelW;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w->calcMinW = maxPageW + TAB_BORDER * 2;
|
w->calcMinW = maxPageW + TAB_BORDER * 2;
|
||||||
|
|
@ -103,6 +212,8 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
if (contentW < 0) { contentW = 0; }
|
if (contentW < 0) { contentW = 0; }
|
||||||
if (contentH < 0) { contentH = 0; }
|
if (contentH < 0) { contentH = 0; }
|
||||||
|
|
||||||
|
tabEnsureVisible(w, font);
|
||||||
|
|
||||||
int32_t idx = 0;
|
int32_t idx = 0;
|
||||||
|
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
|
@ -161,16 +272,7 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active != w->as.tabControl.activeTab) {
|
if (active != w->as.tabControl.activeTab) {
|
||||||
if (sOpenPopup) {
|
tabClosePopup();
|
||||||
if (sOpenPopup->type == WidgetDropdownE) {
|
|
||||||
sOpenPopup->as.dropdown.open = false;
|
|
||||||
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
|
||||||
sOpenPopup->as.comboBox.open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sOpenPopup = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
w->as.tabControl.activeTab = active;
|
w->as.tabControl.activeTab = active;
|
||||||
|
|
||||||
if (w->onChange) {
|
if (w->onChange) {
|
||||||
|
|
@ -193,8 +295,42 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||||
|
|
||||||
// Only handle clicks in the tab header area
|
// Only handle clicks in the tab header area
|
||||||
if (vy >= hit->y && vy < hit->y + tabH) {
|
if (vy < hit->y || vy >= hit->y + tabH) {
|
||||||
int32_t tabX = hit->x + 2;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool scroll = tabNeedScroll(hit, font);
|
||||||
|
|
||||||
|
// Check scroll arrow clicks
|
||||||
|
if (scroll) {
|
||||||
|
int32_t totalW = tabHeaderTotalW(hit, font);
|
||||||
|
int32_t headerW = hit->w - TAB_ARROW_W * 2 - 4;
|
||||||
|
int32_t maxOff = totalW - headerW;
|
||||||
|
|
||||||
|
if (maxOff < 0) {
|
||||||
|
maxOff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left arrow
|
||||||
|
if (vx >= hit->x && vx < hit->x + TAB_ARROW_W) {
|
||||||
|
hit->as.tabControl.scrollOffset -= font->charWidth * 4;
|
||||||
|
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
|
||||||
|
wgtInvalidatePaint(hit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right arrow
|
||||||
|
if (vx >= hit->x + hit->w - TAB_ARROW_W && vx < hit->x + hit->w) {
|
||||||
|
hit->as.tabControl.scrollOffset += font->charWidth * 4;
|
||||||
|
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
|
||||||
|
wgtInvalidatePaint(hit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on tab header
|
||||||
|
int32_t headerLeft = hit->x + 2 + (scroll ? TAB_ARROW_W : 0);
|
||||||
|
int32_t tabX = headerLeft - hit->as.tabControl.scrollOffset;
|
||||||
int32_t tabIdx = 0;
|
int32_t tabIdx = 0;
|
||||||
|
|
||||||
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
||||||
|
|
@ -204,19 +340,9 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
|
|
||||||
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
||||||
|
|
||||||
if (vx >= tabX && vx < tabX + tw) {
|
if (vx >= tabX && vx < tabX + tw && vx >= headerLeft) {
|
||||||
if (tabIdx != hit->as.tabControl.activeTab) {
|
if (tabIdx != hit->as.tabControl.activeTab) {
|
||||||
// Close any open dropdown/combobox popup
|
tabClosePopup();
|
||||||
if (sOpenPopup) {
|
|
||||||
if (sOpenPopup->type == WidgetDropdownE) {
|
|
||||||
sOpenPopup->as.dropdown.open = false;
|
|
||||||
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
|
||||||
sOpenPopup->as.comboBox.open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sOpenPopup = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
hit->as.tabControl.activeTab = tabIdx;
|
hit->as.tabControl.activeTab = tabIdx;
|
||||||
|
|
||||||
if (hit->onChange) {
|
if (hit->onChange) {
|
||||||
|
|
@ -230,7 +356,6 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
tabX += tw;
|
tabX += tw;
|
||||||
tabIdx++;
|
tabIdx++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -240,6 +365,7 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
||||||
|
|
||||||
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||||
|
bool scroll = tabNeedScroll(w, font);
|
||||||
|
|
||||||
// Content panel
|
// Content panel
|
||||||
BevelStyleT panelBevel;
|
BevelStyleT panelBevel;
|
||||||
|
|
@ -249,8 +375,56 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
||||||
panelBevel.width = 2;
|
panelBevel.width = 2;
|
||||||
drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel);
|
drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel);
|
||||||
|
|
||||||
// Tab headers
|
// Scroll arrows
|
||||||
int32_t tabX = w->x + 2;
|
if (scroll) {
|
||||||
|
int32_t totalW = tabHeaderTotalW(w, font);
|
||||||
|
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
|
||||||
|
int32_t maxOff = totalW - headerW;
|
||||||
|
|
||||||
|
if (maxOff < 0) {
|
||||||
|
maxOff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
|
||||||
|
|
||||||
|
uint32_t fg = colors->contentFg;
|
||||||
|
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
||||||
|
|
||||||
|
// Left arrow button
|
||||||
|
drawBevel(d, ops, w->x, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
||||||
|
{
|
||||||
|
int32_t cx = w->x + TAB_ARROW_W / 2;
|
||||||
|
int32_t cy = w->y + tabH / 2;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < 4; i++) {
|
||||||
|
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right arrow button
|
||||||
|
int32_t rx = w->x + w->w - TAB_ARROW_W;
|
||||||
|
drawBevel(d, ops, rx, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
||||||
|
{
|
||||||
|
int32_t cx = rx + TAB_ARROW_W / 2;
|
||||||
|
int32_t cy = w->y + tabH / 2;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < 4; i++) {
|
||||||
|
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab headers — clip to header area
|
||||||
|
int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0);
|
||||||
|
int32_t headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w);
|
||||||
|
|
||||||
|
int32_t oldClipX = d->clipX;
|
||||||
|
int32_t oldClipY = d->clipY;
|
||||||
|
int32_t oldClipW = d->clipW;
|
||||||
|
int32_t oldClipH = d->clipH;
|
||||||
|
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2);
|
||||||
|
|
||||||
|
int32_t tabX = headerLeft - w->as.tabControl.scrollOffset;
|
||||||
int32_t tabIdx = 0;
|
int32_t tabIdx = 0;
|
||||||
|
|
||||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||||
|
|
@ -264,6 +438,8 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
||||||
int32_t th = isActive ? tabH + 2 : tabH;
|
int32_t th = isActive ? tabH + 2 : tabH;
|
||||||
uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace;
|
uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace;
|
||||||
|
|
||||||
|
// Only draw tabs that are at least partially visible
|
||||||
|
if (tabX + tw > headerLeft && tabX < headerRight) {
|
||||||
// Fill tab background
|
// Fill tab background
|
||||||
rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace);
|
rectFill(d, ops, tabX + 2, ty + 2, tw - 4, th - 2, tabFace);
|
||||||
|
|
||||||
|
|
@ -295,17 +471,19 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
||||||
labelY++;
|
labelY++;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY,
|
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, c->as.tabPage.title, colors->contentFg, tabFace, true);
|
||||||
c->as.tabPage.title, colors->contentFg, tabFace, true);
|
|
||||||
|
|
||||||
if (isActive && w->focused) {
|
if (isActive && w->focused) {
|
||||||
drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg);
|
drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabX += tw;
|
tabX += tw;
|
||||||
tabIdx++;
|
tabIdx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||||
|
|
||||||
// Paint only active tab page's children
|
// Paint only active tab page's children
|
||||||
tabIdx = 0;
|
tabIdx = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2224,7 +2224,7 @@ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
||||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
|
||||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||||
|
|
||||||
// Sunken border
|
// Sunken border
|
||||||
|
|
@ -2294,7 +2294,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw cursor
|
// Draw cursor
|
||||||
if (w->focused) {
|
if (w->focused && w->enabled) {
|
||||||
int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth;
|
int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth;
|
||||||
|
|
||||||
if (cursorX >= w->x + TEXT_INPUT_PAD &&
|
if (cursorX >= w->x + TEXT_INPUT_PAD &&
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,16 @@
|
||||||
|
|
||||||
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font);
|
static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font);
|
||||||
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth);
|
static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth);
|
||||||
|
static void clearAllSelections(WidgetT *parent);
|
||||||
static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb);
|
static void drawTreeHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalW, int32_t innerW, bool hasVSb);
|
||||||
static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH);
|
static void drawTreeVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t totalH, int32_t innerH);
|
||||||
static WidgetT *firstVisibleItem(WidgetT *treeView);
|
static WidgetT *firstVisibleItem(WidgetT *treeView);
|
||||||
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth);
|
static void layoutTreeItems(WidgetT *parent, const BitmapFontT *font, int32_t x, int32_t *y, int32_t width, int32_t depth);
|
||||||
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView);
|
static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView);
|
||||||
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *selectedItem);
|
static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t innerW);
|
||||||
|
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView);
|
||||||
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView);
|
static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView);
|
||||||
|
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to);
|
||||||
static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb);
|
static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb);
|
||||||
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font);
|
static WidgetT *treeItemAtY(WidgetT *parent, int32_t targetY, int32_t *curY, const BitmapFontT *font);
|
||||||
static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font);
|
static int32_t treeItemYPos(WidgetT *treeView, WidgetT *target, const BitmapFontT *font);
|
||||||
|
|
@ -76,6 +79,25 @@ static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// clearAllSelections
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void clearAllSelections(WidgetT *parent) {
|
||||||
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
||||||
|
if (c->type != WidgetTreeItemE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
c->as.treeItem.selected = false;
|
||||||
|
|
||||||
|
if (c->firstChild) {
|
||||||
|
clearAllSelections(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// drawTreeHScrollbar
|
// drawTreeHScrollbar
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -278,11 +300,42 @@ static WidgetT *nextVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// paintReorderIndicator
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Draw a 2px insertion line at the drop target position.
|
||||||
|
|
||||||
|
static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t innerW) {
|
||||||
|
if (!w->as.treeView.reorderable || !w->as.treeView.dropTarget || !w->as.treeView.dragItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t dropY = treeItemYPos(w, w->as.treeView.dropTarget, font);
|
||||||
|
|
||||||
|
if (dropY < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t lineY = w->y + TREE_BORDER - w->as.treeView.scrollPos + dropY;
|
||||||
|
|
||||||
|
if (w->as.treeView.dropAfter) {
|
||||||
|
lineY += font->charHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t lineX = w->x + TREE_BORDER;
|
||||||
|
drawHLine(d, ops, lineX, lineY, innerW, colors->contentFg);
|
||||||
|
drawHLine(d, ops, lineX, lineY + 1, innerW, colors->contentFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// paintTreeItems
|
// paintTreeItems
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *selectedItem) {
|
static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t baseX, int32_t *itemY, int32_t depth, int32_t clipTop, int32_t clipBottom, WidgetT *treeView) {
|
||||||
|
bool multi = treeView->as.treeView.multiSelect;
|
||||||
|
|
||||||
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
||||||
if (c->type != WidgetTreeItemE || !c->visible) {
|
if (c->type != WidgetTreeItemE || !c->visible) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -295,14 +348,21 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
|
||||||
// Skip items outside visible area
|
// Skip items outside visible area
|
||||||
if (iy + font->charHeight <= clipTop || iy >= clipBottom) {
|
if (iy + font->charHeight <= clipTop || iy >= clipBottom) {
|
||||||
if (c->as.treeItem.expanded && c->firstChild) {
|
if (c->as.treeItem.expanded && c->firstChild) {
|
||||||
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, selectedItem);
|
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, treeView);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight selected item
|
// Highlight selected item(s)
|
||||||
bool isSelected = (c == selectedItem);
|
bool isSelected;
|
||||||
|
|
||||||
|
if (multi) {
|
||||||
|
isSelected = c->as.treeItem.selected;
|
||||||
|
} else {
|
||||||
|
isSelected = (c == treeView->as.treeView.selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t fg;
|
uint32_t fg;
|
||||||
uint32_t bg;
|
uint32_t bg;
|
||||||
|
|
||||||
|
|
@ -354,9 +414,15 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
|
||||||
// Draw text
|
// Draw text
|
||||||
drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected);
|
drawText(d, ops, font, textX, iy, c->as.treeItem.text, fg, bg, isSelected);
|
||||||
|
|
||||||
|
// Draw focus rectangle around cursor item in multi-select mode
|
||||||
|
if (multi && c == treeView->as.treeView.selectedItem && treeView->focused) {
|
||||||
|
uint32_t focusFg = isSelected ? colors->menuHighlightFg : colors->contentFg;
|
||||||
|
drawFocusRect(d, ops, baseX, iy, d->clipW, font->charHeight, focusFg);
|
||||||
|
}
|
||||||
|
|
||||||
// Recurse into expanded children
|
// Recurse into expanded children
|
||||||
if (c->as.treeItem.expanded && c->firstChild) {
|
if (c->as.treeItem.expanded && c->firstChild) {
|
||||||
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, selectedItem);
|
paintTreeItems(c, d, ops, font, colors, baseX, itemY, depth + 1, clipTop, clipBottom, treeView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -412,6 +478,53 @@ static WidgetT *prevVisibleItem(WidgetT *item, WidgetT *treeView) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// selectRange
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Select all visible items between 'from' and 'to' (inclusive).
|
||||||
|
// Direction is auto-detected.
|
||||||
|
|
||||||
|
static void selectRange(WidgetT *treeView, WidgetT *from, WidgetT *to) {
|
||||||
|
if (!from || !to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from == to) {
|
||||||
|
from->as.treeItem.selected = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk forward from 'from'. If we hit 'to', that's the direction.
|
||||||
|
// Otherwise, walk forward from 'to' to find 'from'.
|
||||||
|
WidgetT *start = from;
|
||||||
|
WidgetT *end = to;
|
||||||
|
|
||||||
|
// Check if 'to' comes after 'from'
|
||||||
|
bool forward = false;
|
||||||
|
|
||||||
|
for (WidgetT *v = nextVisibleItem(from, treeView); v; v = nextVisibleItem(v, treeView)) {
|
||||||
|
if (v == to) {
|
||||||
|
forward = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forward) {
|
||||||
|
start = to;
|
||||||
|
end = from;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (WidgetT *v = start; v; v = nextVisibleItem(v, treeView)) {
|
||||||
|
v->as.treeItem.selected = true;
|
||||||
|
|
||||||
|
if (v == end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// treeCalcScrollbarNeeds
|
// treeCalcScrollbarNeeds
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -630,6 +743,76 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
w->as.treeView.selectedItem = item;
|
w->as.treeView.selectedItem = item;
|
||||||
|
|
||||||
|
if (w->as.treeView.multiSelect && item) {
|
||||||
|
clearAllSelections(w);
|
||||||
|
item->as.treeItem.selected = true;
|
||||||
|
w->as.treeView.anchorItem = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtTreeViewSetMultiSelect
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi) {
|
||||||
|
if (!w || w->type != WidgetTreeViewE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.treeView.multiSelect = multi;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtTreeViewSetReorderable
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable) {
|
||||||
|
if (!w || w->type != WidgetTreeViewE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.treeView.reorderable = reorderable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// widgetTreeViewNextVisible
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Non-static wrapper around nextVisibleItem for use by
|
||||||
|
// widgetReorderUpdate() in widgetEvent.c.
|
||||||
|
|
||||||
|
WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView) {
|
||||||
|
return nextVisibleItem(item, treeView);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtTreeItemIsSelected
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
bool wgtTreeItemIsSelected(const WidgetT *w) {
|
||||||
|
if (!w || w->type != WidgetTreeItemE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return w->as.treeItem.selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wgtTreeItemSetSelected
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wgtTreeItemSetSelected(WidgetT *w, bool selected) {
|
||||||
|
if (!w || w->type != WidgetTreeItemE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
w->as.treeItem.selected = selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -650,12 +833,12 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
(void)mod;
|
|
||||||
|
|
||||||
if (!w || w->type != WidgetTreeViewE) {
|
if (!w || w->type != WidgetTreeViewE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool multi = w->as.treeView.multiSelect;
|
||||||
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
||||||
WidgetT *sel = w->as.treeView.selectedItem;
|
WidgetT *sel = w->as.treeView.selectedItem;
|
||||||
|
|
||||||
if (key == (0x50 | 0x100)) {
|
if (key == (0x50 | 0x100)) {
|
||||||
|
|
@ -745,10 +928,33 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (key == ' ' && multi) {
|
||||||
|
// Space — toggle selection of current item in multi-select
|
||||||
|
if (sel) {
|
||||||
|
sel->as.treeItem.selected = !sel->as.treeItem.selected;
|
||||||
|
w->as.treeView.anchorItem = sel;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update multi-select state after Up/Down navigation
|
||||||
|
WidgetT *newSel = w->as.treeView.selectedItem;
|
||||||
|
|
||||||
|
if (multi && newSel != sel && newSel) {
|
||||||
|
if (shift && w->as.treeView.anchorItem) {
|
||||||
|
// Shift+arrow: range from anchor to new cursor
|
||||||
|
clearAllSelections(w);
|
||||||
|
selectRange(w, w->as.treeView.anchorItem, newSel);
|
||||||
|
}
|
||||||
|
// Plain arrow: just move cursor, leave selections untouched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set anchor on first selection if not set
|
||||||
|
if (multi && !w->as.treeView.anchorItem && newSel) {
|
||||||
|
w->as.treeView.anchorItem = newSel;
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll to keep selected item visible
|
// Scroll to keep selected item visible
|
||||||
sel = w->as.treeView.selectedItem;
|
sel = w->as.treeView.selectedItem;
|
||||||
|
|
||||||
|
|
@ -786,7 +992,13 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
||||||
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
|
||||||
// Auto-select first item if nothing is selected
|
// Auto-select first item if nothing is selected
|
||||||
if (!w->as.treeView.selectedItem) {
|
if (!w->as.treeView.selectedItem) {
|
||||||
w->as.treeView.selectedItem = firstVisibleItem(w);
|
WidgetT *first = firstVisibleItem(w);
|
||||||
|
w->as.treeView.selectedItem = first;
|
||||||
|
|
||||||
|
if (w->as.treeView.multiSelect && first) {
|
||||||
|
first->as.treeItem.selected = true;
|
||||||
|
w->as.treeView.anchorItem = first;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t totalH;
|
int32_t totalH;
|
||||||
|
|
@ -942,11 +1154,33 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set selection
|
// Update selection
|
||||||
|
bool multi = hit->as.treeView.multiSelect;
|
||||||
|
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
|
||||||
|
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
|
||||||
|
|
||||||
hit->as.treeView.selectedItem = item;
|
hit->as.treeView.selectedItem = item;
|
||||||
|
|
||||||
|
if (multi) {
|
||||||
|
if (ctrl) {
|
||||||
|
// Ctrl+click: toggle item, update anchor
|
||||||
|
item->as.treeItem.selected = !item->as.treeItem.selected;
|
||||||
|
hit->as.treeView.anchorItem = item;
|
||||||
|
} else if (shift && hit->as.treeView.anchorItem) {
|
||||||
|
// Shift+click: range from anchor to clicked
|
||||||
|
clearAllSelections(hit);
|
||||||
|
selectRange(hit, hit->as.treeView.anchorItem, item);
|
||||||
|
} else {
|
||||||
|
// Plain click: select only this item, update anchor
|
||||||
|
clearAllSelections(hit);
|
||||||
|
item->as.treeItem.selected = true;
|
||||||
|
hit->as.treeView.anchorItem = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if click is on expand/collapse icon
|
// Check if click is on expand/collapse icon
|
||||||
bool hasChildren = false;
|
bool hasChildren = false;
|
||||||
|
bool clickedExpandIcon = false;
|
||||||
|
|
||||||
for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) {
|
for (WidgetT *gc = item->firstChild; gc; gc = gc->nextSibling) {
|
||||||
if (gc->type == WidgetTreeItemE) {
|
if (gc->type == WidgetTreeItemE) {
|
||||||
|
|
@ -968,6 +1202,7 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT - hit->as.treeView.scrollPosH;
|
int32_t iconX = hit->x + TREE_BORDER + depth * TREE_INDENT - hit->as.treeView.scrollPosH;
|
||||||
|
|
||||||
if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) {
|
if (vx >= iconX && vx < iconX + TREE_EXPAND_SIZE) {
|
||||||
|
clickedExpandIcon = true;
|
||||||
item->as.treeItem.expanded = !item->as.treeItem.expanded;
|
item->as.treeItem.expanded = !item->as.treeItem.expanded;
|
||||||
|
|
||||||
// Clamp scroll positions if collapsing reduced content size
|
// Clamp scroll positions if collapsing reduced content size
|
||||||
|
|
@ -1009,6 +1244,13 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
item->onClick(item);
|
item->onClick(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initiate drag-reorder if enabled (not from expand icon or modifier clicks)
|
||||||
|
if (hit->as.treeView.reorderable && !clickedExpandIcon && !shift && !ctrl) {
|
||||||
|
hit->as.treeView.dragItem = item;
|
||||||
|
hit->as.treeView.dropTarget = NULL;
|
||||||
|
sDragReorder = hit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1061,7 +1303,10 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
||||||
paintTreeItems(w, d, ops, font, colors,
|
paintTreeItems(w, d, ops, font, colors,
|
||||||
baseX, &itemY, 0,
|
baseX, &itemY, 0,
|
||||||
w->y + TREE_BORDER, w->y + TREE_BORDER + innerH,
|
w->y + TREE_BORDER, w->y + TREE_BORDER + innerH,
|
||||||
w->as.treeView.selectedItem);
|
w);
|
||||||
|
|
||||||
|
// Draw drag-reorder insertion indicator (while still clipped)
|
||||||
|
paintReorderIndicator(w, d, ops, font, colors, innerW);
|
||||||
|
|
||||||
// Restore clip rect
|
// Restore clip rect
|
||||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||||
|
|
|
||||||
195
dvxdemo/demo.c
195
dvxdemo/demo.c
|
|
@ -33,6 +33,12 @@
|
||||||
#define CMD_VIEW_SIZE_MED 308
|
#define CMD_VIEW_SIZE_MED 308
|
||||||
#define CMD_VIEW_SIZE_LARGE 309
|
#define CMD_VIEW_SIZE_LARGE 309
|
||||||
#define CMD_HELP_ABOUT 400
|
#define CMD_HELP_ABOUT 400
|
||||||
|
#define CMD_CTX_CUT 500
|
||||||
|
#define CMD_CTX_COPY 501
|
||||||
|
#define CMD_CTX_PASTE 502
|
||||||
|
#define CMD_CTX_DELETE 503
|
||||||
|
#define CMD_CTX_SELALL 504
|
||||||
|
#define CMD_CTX_PROPS 505
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Prototypes
|
// Prototypes
|
||||||
|
|
@ -205,6 +211,30 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
|
||||||
"A DESQview/X-style windowing system for DOS.",
|
"A DESQview/X-style windowing system for DOS.",
|
||||||
MB_OK | MB_ICONINFO);
|
MB_OK | MB_ICONINFO);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CMD_CTX_CUT:
|
||||||
|
dvxMessageBox(ctx, "Context Menu", "Cut selected.", MB_OK | MB_ICONINFO);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTX_COPY:
|
||||||
|
dvxMessageBox(ctx, "Context Menu", "Copied to clipboard.", MB_OK | MB_ICONINFO);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTX_PASTE:
|
||||||
|
dvxMessageBox(ctx, "Context Menu", "Pasted from clipboard.", MB_OK | MB_ICONINFO);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTX_DELETE:
|
||||||
|
dvxMessageBox(ctx, "Context Menu", "Deleted.", MB_OK | MB_ICONINFO);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTX_SELALL:
|
||||||
|
dvxMessageBox(ctx, "Context Menu", "Selected all.", MB_OK | MB_ICONINFO);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTX_PROPS:
|
||||||
|
dvxMessageBox(ctx, "Properties", "Window properties dialog would appear here.", MB_OK | MB_ICONINFO);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,21 +470,25 @@ static void setupControlsWindow(AppContextT *ctx) {
|
||||||
WidgetT *dd = wgtDropdown(ddRow);
|
WidgetT *dd = wgtDropdown(ddRow);
|
||||||
wgtDropdownSetItems(dd, colorItems, 6);
|
wgtDropdownSetItems(dd, colorItems, 6);
|
||||||
wgtDropdownSetSelected(dd, 0);
|
wgtDropdownSetSelected(dd, 0);
|
||||||
|
wgtSetTooltip(dd, "Choose a color");
|
||||||
|
|
||||||
WidgetT *cbRow = wgtHBox(page1);
|
WidgetT *cbRow = wgtHBox(page1);
|
||||||
wgtLabel(cbRow, "Si&ze:");
|
wgtLabel(cbRow, "Si&ze:");
|
||||||
WidgetT *cb = wgtComboBox(cbRow, 32);
|
WidgetT *cb = wgtComboBox(cbRow, 32);
|
||||||
wgtComboBoxSetItems(cb, sizeItems, 4);
|
wgtComboBoxSetItems(cb, sizeItems, 4);
|
||||||
wgtComboBoxSetSelected(cb, 1);
|
wgtComboBoxSetSelected(cb, 1);
|
||||||
|
wgtSetTooltip(cb, "Type or select a size");
|
||||||
|
|
||||||
wgtHSeparator(page1);
|
wgtHSeparator(page1);
|
||||||
|
|
||||||
wgtLabel(page1, "&Progress:");
|
wgtLabel(page1, "&Progress:");
|
||||||
WidgetT *pb = wgtProgressBar(page1);
|
WidgetT *pb = wgtProgressBar(page1);
|
||||||
wgtProgressBarSetValue(pb, 65);
|
wgtProgressBarSetValue(pb, 65);
|
||||||
|
wgtSetTooltip(pb, "Task progress: 65%");
|
||||||
|
|
||||||
wgtLabel(page1, "&Volume:");
|
wgtLabel(page1, "&Volume:");
|
||||||
wgtSlider(page1, 0, 100);
|
WidgetT *slider = wgtSlider(page1, 0, 100);
|
||||||
|
wgtSetTooltip(slider, "Adjust volume level");
|
||||||
|
|
||||||
WidgetT *spinRow = wgtHBox(page1);
|
WidgetT *spinRow = wgtHBox(page1);
|
||||||
spinRow->maxH = wgtPixels(30);
|
spinRow->maxH = wgtPixels(30);
|
||||||
|
|
@ -462,11 +496,13 @@ static void setupControlsWindow(AppContextT *ctx) {
|
||||||
WidgetT *spin = wgtSpinner(spinRow, 0, 999, 1);
|
WidgetT *spin = wgtSpinner(spinRow, 0, 999, 1);
|
||||||
wgtSpinnerSetValue(spin, 42);
|
wgtSpinnerSetValue(spin, 42);
|
||||||
spin->weight = 50;
|
spin->weight = 50;
|
||||||
|
wgtSetTooltip(spin, "Enter quantity (0-999)");
|
||||||
|
|
||||||
// --- Tab 2: Tree ---
|
// --- Tab 2: Tree ---
|
||||||
WidgetT *page2 = wgtTabPage(tabs, "&Tree");
|
WidgetT *page2 = wgtTabPage(tabs, "&Tree");
|
||||||
|
|
||||||
WidgetT *tree = wgtTreeView(page2);
|
WidgetT *tree = wgtTreeView(page2);
|
||||||
|
wgtTreeViewSetReorderable(tree, true);
|
||||||
WidgetT *docs = wgtTreeItem(tree, "Documents");
|
WidgetT *docs = wgtTreeItem(tree, "Documents");
|
||||||
wgtTreeItemSetExpanded(docs, true);
|
wgtTreeItemSetExpanded(docs, true);
|
||||||
wgtTreeItem(docs, "README.md");
|
wgtTreeItem(docs, "README.md");
|
||||||
|
|
@ -511,6 +547,7 @@ static void setupControlsWindow(AppContextT *ctx) {
|
||||||
wgtListViewSetData(lv, lvData, 10);
|
wgtListViewSetData(lv, lvData, 10);
|
||||||
wgtListViewSetSelected(lv, 0);
|
wgtListViewSetSelected(lv, 0);
|
||||||
wgtListViewSetMultiSelect(lv, true);
|
wgtListViewSetMultiSelect(lv, true);
|
||||||
|
wgtListViewSetReorderable(lv, true);
|
||||||
lv->weight = 100;
|
lv->weight = 100;
|
||||||
|
|
||||||
// --- Tab 4: ScrollPane ---
|
// --- Tab 4: ScrollPane ---
|
||||||
|
|
@ -623,6 +660,131 @@ static void setupControlsWindow(AppContextT *ctx) {
|
||||||
wgtCanvasDrawLine(cv, 70, 5, 130, 70);
|
wgtCanvasDrawLine(cv, 70, 5, 130, 70);
|
||||||
wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 0));
|
wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 0));
|
||||||
|
|
||||||
|
// --- Tab 8: Splitter ---
|
||||||
|
WidgetT *page8s = wgtTabPage(tabs, "S&plit");
|
||||||
|
|
||||||
|
// Outer horizontal splitter: explorer on top, detail on bottom
|
||||||
|
WidgetT *hSplit = wgtSplitter(page8s, false);
|
||||||
|
hSplit->weight = 100;
|
||||||
|
wgtSplitterSetPos(hSplit, 120);
|
||||||
|
|
||||||
|
// Top pane: vertical splitter (tree | list)
|
||||||
|
WidgetT *vSplit = wgtSplitter(hSplit, true);
|
||||||
|
wgtSplitterSetPos(vSplit, 120);
|
||||||
|
|
||||||
|
// Left pane: multi-select tree view
|
||||||
|
WidgetT *leftTree = wgtTreeView(vSplit);
|
||||||
|
wgtTreeViewSetMultiSelect(leftTree, true);
|
||||||
|
WidgetT *tiFolders = wgtTreeItem(leftTree, "Folders");
|
||||||
|
wgtTreeItemSetExpanded(tiFolders, true);
|
||||||
|
wgtTreeItem(tiFolders, "Documents");
|
||||||
|
wgtTreeItem(tiFolders, "Pictures");
|
||||||
|
wgtTreeItem(tiFolders, "Music");
|
||||||
|
WidgetT *tiSystem = wgtTreeItem(leftTree, "System");
|
||||||
|
wgtTreeItemSetExpanded(tiSystem, true);
|
||||||
|
wgtTreeItem(tiSystem, "Config");
|
||||||
|
wgtTreeItem(tiSystem, "Drivers");
|
||||||
|
|
||||||
|
// Right pane: list box
|
||||||
|
WidgetT *rightList = wgtListBox(vSplit);
|
||||||
|
|
||||||
|
static const char *explorerItems[] = {
|
||||||
|
"README.TXT", "SETUP.EXE", "CONFIG.SYS",
|
||||||
|
"AUTOEXEC.BAT", "COMMAND.COM", "HIMEM.SYS",
|
||||||
|
"EMM386.EXE", "MOUSE.COM"
|
||||||
|
};
|
||||||
|
|
||||||
|
wgtListBoxSetItems(rightList, explorerItems, 8);
|
||||||
|
|
||||||
|
// Bottom pane: detail/preview area
|
||||||
|
WidgetT *detailFrame = wgtFrame(hSplit, "Preview");
|
||||||
|
wgtLabel(detailFrame, "Select a file above to preview.");
|
||||||
|
|
||||||
|
// --- Tab 9: Disabled ---
|
||||||
|
WidgetT *page9d = wgtTabPage(tabs, "&Disabled");
|
||||||
|
|
||||||
|
// Left column: enabled widgets Right column: disabled widgets
|
||||||
|
WidgetT *disRow = wgtHBox(page9d);
|
||||||
|
disRow->weight = 100;
|
||||||
|
|
||||||
|
// Enabled column
|
||||||
|
WidgetT *enCol = wgtVBox(disRow);
|
||||||
|
enCol->weight = 100;
|
||||||
|
wgtLabel(enCol, "Enabled:");
|
||||||
|
wgtHSeparator(enCol);
|
||||||
|
wgtLabel(enCol, "A &label");
|
||||||
|
wgtButton(enCol, "A &button");
|
||||||
|
|
||||||
|
WidgetT *enChk = wgtCheckbox(enCol, "&Check me");
|
||||||
|
enChk->as.checkbox.checked = true;
|
||||||
|
|
||||||
|
WidgetT *enRg = wgtRadioGroup(enCol);
|
||||||
|
wgtRadio(enRg, "&Radio 1");
|
||||||
|
WidgetT *enR2 = wgtRadio(enRg, "R&adio 2");
|
||||||
|
enRg->as.radioGroup.selectedIdx = enR2->as.radio.index;
|
||||||
|
|
||||||
|
static const char *disItems[] = {"Alpha", "Beta", "Gamma"};
|
||||||
|
|
||||||
|
WidgetT *enDd = wgtDropdown(enCol);
|
||||||
|
wgtDropdownSetItems(enDd, disItems, 3);
|
||||||
|
wgtDropdownSetSelected(enDd, 0);
|
||||||
|
|
||||||
|
WidgetT *enSlider = wgtSlider(enCol, 0, 100);
|
||||||
|
wgtSliderSetValue(enSlider, 40);
|
||||||
|
|
||||||
|
WidgetT *enProg = wgtProgressBar(enCol);
|
||||||
|
wgtProgressBarSetValue(enProg, 60);
|
||||||
|
|
||||||
|
WidgetT *enSpin = wgtSpinner(enCol, 0, 100, 1);
|
||||||
|
wgtSpinnerSetValue(enSpin, 42);
|
||||||
|
|
||||||
|
WidgetT *enText = wgtTextInput(enCol, 64);
|
||||||
|
wgtSetText(enText, "Editable");
|
||||||
|
|
||||||
|
// Disabled column
|
||||||
|
WidgetT *disCol = wgtVBox(disRow);
|
||||||
|
disCol->weight = 100;
|
||||||
|
wgtLabel(disCol, "Disabled:");
|
||||||
|
wgtHSeparator(disCol);
|
||||||
|
|
||||||
|
WidgetT *dLbl = wgtLabel(disCol, "A &label");
|
||||||
|
wgtSetEnabled(dLbl, false);
|
||||||
|
|
||||||
|
WidgetT *dBtn = wgtButton(disCol, "A &button");
|
||||||
|
wgtSetEnabled(dBtn, false);
|
||||||
|
|
||||||
|
WidgetT *dChk = wgtCheckbox(disCol, "&Check me");
|
||||||
|
dChk->as.checkbox.checked = true;
|
||||||
|
wgtSetEnabled(dChk, false);
|
||||||
|
|
||||||
|
WidgetT *dRg = wgtRadioGroup(disCol);
|
||||||
|
WidgetT *dR1 = wgtRadio(dRg, "&Radio 1");
|
||||||
|
WidgetT *dR2 = wgtRadio(dRg, "R&adio 2");
|
||||||
|
dRg->as.radioGroup.selectedIdx = dR2->as.radio.index;
|
||||||
|
wgtSetEnabled(dR1, false);
|
||||||
|
wgtSetEnabled(dR2, false);
|
||||||
|
|
||||||
|
WidgetT *dDd = wgtDropdown(disCol);
|
||||||
|
wgtDropdownSetItems(dDd, disItems, 3);
|
||||||
|
wgtDropdownSetSelected(dDd, 0);
|
||||||
|
wgtSetEnabled(dDd, false);
|
||||||
|
|
||||||
|
WidgetT *dSlider = wgtSlider(disCol, 0, 100);
|
||||||
|
wgtSliderSetValue(dSlider, 40);
|
||||||
|
wgtSetEnabled(dSlider, false);
|
||||||
|
|
||||||
|
WidgetT *dProg = wgtProgressBar(disCol);
|
||||||
|
wgtProgressBarSetValue(dProg, 60);
|
||||||
|
wgtSetEnabled(dProg, false);
|
||||||
|
|
||||||
|
WidgetT *dSpin = wgtSpinner(disCol, 0, 100, 1);
|
||||||
|
wgtSpinnerSetValue(dSpin, 42);
|
||||||
|
wgtSetEnabled(dSpin, false);
|
||||||
|
|
||||||
|
WidgetT *dText = wgtTextInput(disCol, 64);
|
||||||
|
wgtSetText(dText, "Read-only");
|
||||||
|
wgtSetEnabled(dText, false);
|
||||||
|
|
||||||
// Status bar at bottom (outside tabs)
|
// Status bar at bottom (outside tabs)
|
||||||
WidgetT *sb = wgtStatusBar(root);
|
WidgetT *sb = wgtStatusBar(root);
|
||||||
WidgetT *sbLabel = wgtLabel(sb, "Ready");
|
WidgetT *sbLabel = wgtLabel(sb, "Ready");
|
||||||
|
|
@ -700,6 +862,24 @@ static void setupMainWindow(AppContextT *ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accelerator table (global hotkeys)
|
||||||
|
AccelTableT *accel = dvxCreateAccelTable();
|
||||||
|
dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW);
|
||||||
|
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN);
|
||||||
|
dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_FILE_SAVE);
|
||||||
|
dvxAddAccel(accel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT);
|
||||||
|
dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT);
|
||||||
|
win1->accelTable = accel;
|
||||||
|
|
||||||
|
// Window-level context menu
|
||||||
|
MenuT *winCtx = wmCreateMenu();
|
||||||
|
wmAddMenuItem(winCtx, "Cu&t", CMD_CTX_CUT);
|
||||||
|
wmAddMenuItem(winCtx, "&Copy", CMD_CTX_COPY);
|
||||||
|
wmAddMenuItem(winCtx, "&Paste", CMD_CTX_PASTE);
|
||||||
|
wmAddMenuSeparator(winCtx);
|
||||||
|
wmAddMenuItem(winCtx, "&Properties...", CMD_CTX_PROPS);
|
||||||
|
win1->contextMenu = winCtx;
|
||||||
|
|
||||||
wmUpdateContentRect(win1);
|
wmUpdateContentRect(win1);
|
||||||
wmReallocContentBuf(win1, &ctx->display);
|
wmReallocContentBuf(win1, &ctx->display);
|
||||||
|
|
||||||
|
|
@ -832,6 +1012,7 @@ static void setupWidgetDemo(AppContextT *ctx) {
|
||||||
|
|
||||||
win->userData = ctx;
|
win->userData = ctx;
|
||||||
win->onClose = onCloseCb;
|
win->onClose = onCloseCb;
|
||||||
|
win->onMenu = onMenuCb;
|
||||||
|
|
||||||
WidgetT *root = wgtInitWindow(ctx, win);
|
WidgetT *root = wgtInitWindow(ctx, win);
|
||||||
|
|
||||||
|
|
@ -878,7 +1059,19 @@ static void setupWidgetDemo(AppContextT *ctx) {
|
||||||
wgtLabel(listRow, "&Items:");
|
wgtLabel(listRow, "&Items:");
|
||||||
WidgetT *lb = wgtListBox(listRow);
|
WidgetT *lb = wgtListBox(listRow);
|
||||||
wgtListBoxSetItems(lb, listItems, 5);
|
wgtListBoxSetItems(lb, listItems, 5);
|
||||||
|
wgtListBoxSetReorderable(lb, true);
|
||||||
lb->weight = 100;
|
lb->weight = 100;
|
||||||
|
|
||||||
|
// Context menu on the list box
|
||||||
|
MenuT *lbCtx = wmCreateMenu();
|
||||||
|
wmAddMenuItem(lbCtx, "Cu&t", CMD_CTX_CUT);
|
||||||
|
wmAddMenuItem(lbCtx, "&Copy", CMD_CTX_COPY);
|
||||||
|
wmAddMenuItem(lbCtx, "&Paste", CMD_CTX_PASTE);
|
||||||
|
wmAddMenuSeparator(lbCtx);
|
||||||
|
wmAddMenuItem(lbCtx, "&Delete", CMD_CTX_DELETE);
|
||||||
|
wmAddMenuSeparator(lbCtx);
|
||||||
|
wmAddMenuItem(lbCtx, "Select &All", CMD_CTX_SELALL);
|
||||||
|
lb->contextMenu = lbCtx;
|
||||||
wgtLabel(listRow, "&Multi:");
|
wgtLabel(listRow, "&Multi:");
|
||||||
WidgetT *mlb = wgtListBox(listRow);
|
WidgetT *mlb = wgtListBox(listRow);
|
||||||
wgtListBoxSetMultiSelect(mlb, true);
|
wgtListBoxSetMultiSelect(mlb, true);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue