Added message box, multi-column list/table, and submenus.

This commit is contained in:
Scott Duensing 2026-03-15 20:40:50 -05:00
parent 97530513b8
commit 6f8aeda7b2
18 changed files with 2370 additions and 301 deletions

View file

@ -11,7 +11,7 @@ OBJDIR = ../obj/dvx
WOBJDIR = ../obj/dvx/widgets
LIBDIR = ../lib
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c dvxDialog.c
WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetClass.c \
@ -29,6 +29,7 @@ WSRCS = widgets/widgetAnsiTerm.c \
widgets/widgetImageButton.c \
widgets/widgetLabel.c \
widgets/widgetListBox.c \
widgets/widgetListView.c \
widgets/widgetProgressBar.c \
widgets/widgetRadio.c \
widgets/widgetSeparator.c \
@ -75,6 +76,7 @@ $(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVid
$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h
$(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h
$(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h
$(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h dvxApp.h dvxWidget.h widgets/widgetInternal.h dvxTypes.h dvxDraw.h
# Widget file dependencies
WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
@ -94,6 +96,7 @@ $(WOBJDIR)/widgetImage.o: widgets/widgetImage.c $(WIDGET_DEPS) thirdparty/
$(WOBJDIR)/widgetImageButton.o: widgets/widgetImageButton.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetLabel.o: widgets/widgetLabel.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetListBox.o: widgets/widgetListBox.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetListView.o: widgets/widgetListView.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS)
$(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS)

View file

@ -15,22 +15,29 @@
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
#define ICON_REFRESH_INTERVAL 8
#define KB_MOVE_STEP 8
#define SUBMENU_ARROW_WIDTH 12
// ============================================================
// Prototypes
// ============================================================
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
static void closeAllPopups(AppContextT *ctx);
static void closePopupLevel(AppContextT *ctx);
static void closeSysMenu(AppContextT *ctx);
static void compositeAndFlush(AppContextT *ctx);
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
static bool dispatchAccelKey(AppContextT *ctx, char key);
static void dispatchEvents(AppContextT *ctx);
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo);
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void initColorScheme(AppContextT *ctx);
static void initMouse(AppContextT *ctx);
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
static void openSubMenu(AppContextT *ctx);
static void openSysMenu(AppContextT *ctx, WindowT *win);
static void pollAnsiTermWidgets(AppContextT *ctx);
static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win);
@ -60,6 +67,85 @@ static const char sAltScanToAscii[256] = {
};
// ============================================================
// calcPopupSize — compute popup width and height for a menu
// ============================================================
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
int32_t maxW = 0;
bool hasSub = false;
for (int32_t k = 0; k < menu->itemCount; k++) {
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label);
if (itemW > maxW) {
maxW = itemW;
}
if (menu->items[k].subMenu) {
hasSub = true;
}
}
*pw = maxW + CHROME_TITLE_PAD * 2 + 8 + (hasSub ? SUBMENU_ARROW_WIDTH : 0);
*ph = menu->itemCount * ctx->font.charHeight + 4;
}
// ============================================================
// closeAllPopups — dirty all popup levels and deactivate
// ============================================================
static void closeAllPopups(AppContextT *ctx) {
if (!ctx->popup.active) {
return;
}
// Dirty current (deepest) level
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
// Dirty all parent levels
for (int32_t i = 0; i < ctx->popup.depth; i++) {
PopupLevelT *pl = &ctx->popup.parentStack[i];
dirtyListAdd(&ctx->dirty, pl->popupX, pl->popupY, pl->popupW, pl->popupH);
}
ctx->popup.active = false;
ctx->popup.depth = 0;
}
// ============================================================
// closePopupLevel — close one submenu level (or deactivate if top)
// ============================================================
static void closePopupLevel(AppContextT *ctx) {
if (!ctx->popup.active) {
return;
}
// Dirty current level
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
if (ctx->popup.depth > 0) {
// Pop parent
ctx->popup.depth--;
PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth];
ctx->popup.menu = pl->menu;
ctx->popup.menuIdx = pl->menuIdx;
ctx->popup.popupX = pl->popupX;
ctx->popup.popupY = pl->popupY;
ctx->popup.popupW = pl->popupW;
ctx->popup.popupH = pl->popupH;
ctx->popup.hoverItem = pl->hoverItem;
} else {
ctx->popup.active = false;
}
}
// ============================================================
// closeSysMenu
// ============================================================
@ -127,71 +213,16 @@ static void compositeAndFlush(AppContextT *ctx) {
}
}
// 4. Draw popup menu if active
// 4. Draw popup menu if active (all levels)
if (ctx->popup.active) {
// Draw popup dropdown
RectT popRect = {
ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH
};
RectT popIsect;
if (rectIntersect(dr, &popRect, &popIsect)) {
// Find the window and menu
for (int32_t j = 0; j < ws->count; j++) {
if (ws->windows[j]->id == ctx->popup.windowId) {
WindowT *win = ws->windows[j];
MenuBarT *bar = win->menuBar;
if (bar && ctx->popup.menuIdx < bar->menuCount) {
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
// Draw popup background
BevelStyleT popBevel;
popBevel.highlight = ctx->colors.windowHighlight;
popBevel.shadow = ctx->colors.windowShadow;
popBevel.face = ctx->colors.menuBg;
popBevel.width = 2;
drawBevel(d, ops, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH, &popBevel);
// Draw menu items
int32_t itemY = ctx->popup.popupY + 2;
for (int32_t k = 0; k < menu->itemCount; k++) {
MenuItemT *item = &menu->items[k];
if (item->separator) {
drawHLine(d, ops, ctx->popup.popupX + 2,
itemY + ctx->font.charHeight / 2,
ctx->popup.popupW - 4,
ctx->colors.windowShadow);
itemY += ctx->font.charHeight;
continue;
// Draw parent levels first (bottom to top)
for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) {
PopupLevelT *pl = &ctx->popup.parentStack[lvl];
drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, dr);
}
uint32_t bg = ctx->colors.menuBg;
uint32_t fg = ctx->colors.menuFg;
if (k == ctx->popup.hoverItem) {
bg = ctx->colors.menuHighlightBg;
fg = ctx->colors.menuHighlightFg;
}
rectFill(d, ops, ctx->popup.popupX + 2, itemY,
ctx->popup.popupW - 4, ctx->font.charHeight, bg);
drawTextAccel(d, ops, &ctx->font,
ctx->popup.popupX + CHROME_TITLE_PAD + 2, itemY,
item->label, fg, bg, true);
itemY += ctx->font.charHeight;
}
}
break;
}
}
}
// Draw current (deepest) level
drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, dr);
}
// 4b. Draw system menu if active
@ -272,39 +303,8 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
// Check menu bar first
if (win->menuBar) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
MenuT *menu = &win->menuBar->menus[i];
if (menu->accelKey == key) {
// Close existing popup first
if (ctx->popup.active) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
}
// Open this menu's popup
ctx->popup.active = true;
ctx->popup.windowId = win->id;
ctx->popup.menuIdx = i;
ctx->popup.popupX = win->x + menu->barX;
ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT;
ctx->popup.hoverItem = -1;
// Calculate popup size
int32_t maxW = 0;
for (int32_t k = 0; k < menu->itemCount; k++) {
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label);
if (itemW > maxW) {
maxW = itemW;
}
}
ctx->popup.popupW = maxW + CHROME_TITLE_PAD * 2 + 8;
ctx->popup.popupH = menu->itemCount * ctx->font.charHeight + 4;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
if (win->menuBar->menus[i].accelKey == key) {
openPopupAtMenu(ctx, win, i);
return true;
}
}
@ -534,56 +534,150 @@ static void dispatchEvents(AppContextT *ctx) {
}
}
// Handle popup menu interaction
// Handle popup menu interaction (with cascading submenu support)
if (ctx->popup.active) {
// Check if mouse is inside popup
if (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH) {
// Check if mouse is inside current (deepest) popup level
bool inCurrent = (mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW &&
my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH);
// Find which item is hovered
if (inCurrent) {
// Hover tracking in current level
int32_t relY = my - ctx->popup.popupY - 2;
int32_t itemIdx = relY / ctx->font.charHeight;
if (itemIdx < 0) {
itemIdx = 0;
}
if (ctx->popup.menu && itemIdx >= ctx->popup.menu->itemCount) {
itemIdx = ctx->popup.menu->itemCount - 1;
}
if (itemIdx != ctx->popup.hoverItem) {
int32_t prevHover = ctx->popup.hoverItem;
ctx->popup.hoverItem = itemIdx;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
// If hovering a submenu item, open the submenu
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
MenuItemT *hItem = &ctx->popup.menu->items[itemIdx];
if (hItem->subMenu && hItem->enabled) {
openSubMenu(ctx);
}
}
// Click on item
// If we moved away from a submenu item to a non-submenu item,
// close any child submenus that were opened from this level
if (prevHover >= 0 && ctx->popup.menu && prevHover < ctx->popup.menu->itemCount) {
// Already handled: openSubMenu replaces the child, and if current
// item is not a submenu, no child opens. But we may still have
// a stale child — check if depth was increased by a previous hover.
// This case is handled below by the parent-level hit test popping levels.
}
}
// Click on item in current level
if ((buttons & 1) && !(prevBtn & 1)) {
// Find the window and menu
for (int32_t j = 0; j < ctx->stack.count; j++) {
if (ctx->stack.windows[j]->id == ctx->popup.windowId) {
WindowT *win = ctx->stack.windows[j];
MenuBarT *bar = win->menuBar;
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
MenuItemT *item = &ctx->popup.menu->items[itemIdx];
if (bar && ctx->popup.menuIdx < bar->menuCount) {
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
if (item->subMenu && item->enabled) {
// Clicking a submenu item opens it (already open from hover, but ensure)
openSubMenu(ctx);
} else if (item->enabled && !item->separator) {
// Close popup BEFORE calling onMenu (onMenu may run a nested event loop)
int32_t menuId = item->id;
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
closeAllPopups(ctx);
if (itemIdx >= 0 && itemIdx < menu->itemCount) {
MenuItemT *item = &menu->items[itemIdx];
if (item->enabled && !item->separator && win->onMenu) {
win->onMenu(win, item->id);
if (win && win->onMenu) {
win->onMenu(win, menuId);
}
}
}
}
} else {
// Mouse is not in current popup — check parent levels (deepest first)
bool inParent = false;
for (int32_t lvl = ctx->popup.depth - 1; lvl >= 0; lvl--) {
PopupLevelT *pl = &ctx->popup.parentStack[lvl];
if (mx >= pl->popupX && mx < pl->popupX + pl->popupW &&
my >= pl->popupY && my < pl->popupY + pl->popupH) {
// Close all levels deeper than this one
while (ctx->popup.depth > lvl + 1) {
closePopupLevel(ctx);
}
// Now close the level that was the "current" when we entered this parent
closePopupLevel(ctx);
// Now current level IS this parent — update hover
int32_t relY = my - ctx->popup.popupY - 2;
int32_t itemIdx = relY / ctx->font.charHeight;
if (itemIdx < 0) {
itemIdx = 0;
}
if (ctx->popup.menu && itemIdx >= ctx->popup.menu->itemCount) {
itemIdx = ctx->popup.menu->itemCount - 1;
}
ctx->popup.hoverItem = itemIdx;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
// If the newly hovered item has a submenu, open it
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
MenuItemT *hItem = &ctx->popup.menu->items[itemIdx];
if (hItem->subMenu && hItem->enabled) {
openSubMenu(ctx);
}
}
inParent = true;
break;
}
}
if (!inParent) {
// Check if mouse is on the menu bar (for switching top-level menus)
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar) {
int32_t barY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT;
if (my >= barY && my < barY + CHROME_MENU_HEIGHT &&
mx >= win->x + CHROME_TOTAL_SIDE &&
mx < win->x + win->w - CHROME_TOTAL_SIDE) {
int32_t relX = mx - win->x;
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
MenuT *menu = &win->menuBar->menus[i];
if (relX >= menu->barX && relX < menu->barX + menu->barW) {
if (i != ctx->popup.menuIdx || ctx->popup.depth > 0) {
openPopupAtMenu(ctx, win, i);
}
break;
}
}
// Close popup
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
} else if ((buttons & 1) && !(prevBtn & 1)) {
// Click outside all popups and menu bar — close everything
closeAllPopups(ctx);
}
} else if ((buttons & 1) && !(prevBtn & 1)) {
// Click outside popup — close it
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
closeAllPopups(ctx);
}
}
}
return;
@ -632,6 +726,68 @@ static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y) {
}
// ============================================================
// drawPopupLevel — draw one popup menu (bevel + items)
// ============================================================
static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo) {
RectT popRect = { px, py, pw, ph };
RectT popIsect;
if (!rectIntersect(clipTo, &popRect, &popIsect)) {
return;
}
// Draw popup background
BevelStyleT popBevel;
popBevel.highlight = ctx->colors.windowHighlight;
popBevel.shadow = ctx->colors.windowShadow;
popBevel.face = ctx->colors.menuBg;
popBevel.width = 2;
drawBevel(d, ops, px, py, pw, ph, &popBevel);
// Draw menu items
int32_t itemY = py + 2;
for (int32_t k = 0; k < menu->itemCount; k++) {
const MenuItemT *item = &menu->items[k];
if (item->separator) {
drawHLine(d, ops, px + 2, itemY + ctx->font.charHeight / 2, pw - 4, ctx->colors.windowShadow);
itemY += ctx->font.charHeight;
continue;
}
uint32_t bg = ctx->colors.menuBg;
uint32_t fg = ctx->colors.menuFg;
if (k == hoverItem) {
bg = ctx->colors.menuHighlightBg;
fg = ctx->colors.menuHighlightFg;
}
rectFill(d, ops, px + 2, itemY, pw - 4, ctx->font.charHeight, bg);
drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2, itemY, item->label, fg, bg, true);
// Draw submenu arrow indicator
if (item->subMenu) {
int32_t arrowX = px + pw - SUBMENU_ARROW_WIDTH - 2;
int32_t arrowY = itemY + ctx->font.charHeight / 2;
for (int32_t row = -3; row <= 3; row++) {
int32_t len = 4 - (row < 0 ? -row : row);
if (len > 0) {
drawHLine(d, ops, arrowX + (row < 0 ? 3 + row : 3 - row), arrowY + row, len, fg);
}
}
}
itemY += ctx->font.charHeight;
}
}
// ============================================================
// dvxCreateWindow
// ============================================================
@ -957,8 +1113,10 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
break;
case SysMenuMinimizeE:
if (ctx->modalWindow != win) {
wmMinimize(&ctx->stack, &ctx->dirty, win);
dirtyListAdd(&ctx->dirty, 0, ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING, ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING);
}
break;
case SysMenuMaximizeE:
@ -998,6 +1156,16 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) {
// ============================================================
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons) {
// Modal window gating: only the modal window receives clicks
if (ctx->modalWindow) {
int32_t hitPart;
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
if (hitIdx >= 0 && ctx->stack.windows[hitIdx] != ctx->modalWindow) {
return;
}
}
// Check for click on minimized icon first
int32_t iconIdx = wmMinimizedIconHit(&ctx->stack, &ctx->display, mx, my);
@ -1084,7 +1252,6 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
case 4: // menu bar
{
// Determine which menu was clicked
if (!win->menuBar) {
break;
}
@ -1095,30 +1262,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
MenuT *menu = &win->menuBar->menus[i];
if (relX >= menu->barX && relX < menu->barX + menu->barW) {
// Open popup
ctx->popup.active = true;
ctx->popup.windowId = win->id;
ctx->popup.menuIdx = i;
ctx->popup.popupX = win->x + menu->barX;
ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT;
ctx->popup.hoverItem = -1;
// Calculate popup size
int32_t maxW = 0;
for (int32_t k = 0; k < menu->itemCount; k++) {
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label);
if (itemW > maxW) {
maxW = itemW;
}
}
ctx->popup.popupW = maxW + CHROME_TITLE_PAD * 2 + 8;
ctx->popup.popupH = menu->itemCount * ctx->font.charHeight + 4;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
openPopupAtMenu(ctx, win, i);
break;
}
}
@ -1133,12 +1277,14 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, 1, mx, my);
break;
case 7: // minimize
case 7: // minimize (not allowed for modal windows)
if (ctx->modalWindow != win) {
wmMinimize(&ctx->stack, &ctx->dirty, win);
// Dirty the icon strip area so the new icon gets drawn
dirtyListAdd(&ctx->dirty, 0,
ctx->display.height - ICON_TOTAL_SIZE - ICON_SPACING,
ctx->display.width, ICON_TOTAL_SIZE + ICON_SPACING);
}
break;
case 8: // maximize / restore
@ -1222,16 +1368,92 @@ static void initMouse(AppContextT *ctx) {
}
// ============================================================
// openPopupAtMenu — open top-level popup for a menu bar menu
// ============================================================
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx) {
if (!win->menuBar || menuIdx < 0 || menuIdx >= win->menuBar->menuCount) {
return;
}
// Close any existing popup chain first
closeAllPopups(ctx);
MenuT *menu = &win->menuBar->menus[menuIdx];
ctx->popup.active = true;
ctx->popup.windowId = win->id;
ctx->popup.menuIdx = menuIdx;
ctx->popup.menu = menu;
ctx->popup.popupX = win->x + menu->barX;
ctx->popup.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT + CHROME_MENU_HEIGHT;
ctx->popup.hoverItem = -1;
ctx->popup.depth = 0;
calcPopupSize(ctx, menu, &ctx->popup.popupW, &ctx->popup.popupH);
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
}
// ============================================================
// openSubMenu — open submenu for the currently hovered item
// ============================================================
static void openSubMenu(AppContextT *ctx) {
if (!ctx->popup.active || !ctx->popup.menu) {
return;
}
int32_t idx = ctx->popup.hoverItem;
if (idx < 0 || idx >= ctx->popup.menu->itemCount) {
return;
}
MenuItemT *item = &ctx->popup.menu->items[idx];
if (!item->subMenu) {
return;
}
if (ctx->popup.depth >= MAX_SUBMENU_DEPTH) {
return;
}
// Push current state to parent stack
PopupLevelT *pl = &ctx->popup.parentStack[ctx->popup.depth];
pl->menu = ctx->popup.menu;
pl->menuIdx = ctx->popup.menuIdx;
pl->popupX = ctx->popup.popupX;
pl->popupY = ctx->popup.popupY;
pl->popupW = ctx->popup.popupW;
pl->popupH = ctx->popup.popupH;
pl->hoverItem = ctx->popup.hoverItem;
ctx->popup.depth++;
// Open submenu at right edge of current popup, aligned with hovered item
ctx->popup.menu = item->subMenu;
ctx->popup.popupX = pl->popupX + pl->popupW - 2;
ctx->popup.popupY = pl->popupY + 2 + idx * ctx->font.charHeight;
ctx->popup.hoverItem = -1;
calcPopupSize(ctx, ctx->popup.menu, &ctx->popup.popupW, &ctx->popup.popupH);
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
}
// ============================================================
// openSysMenu
// ============================================================
static void openSysMenu(AppContextT *ctx, WindowT *win) {
// Close any existing popup menus first
if (ctx->popup.active) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
}
closeAllPopups(ctx);
if (ctx->sysMenu.active) {
closeSysMenu(ctx);
@ -1265,12 +1487,12 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) {
item->enabled = win->resizable && !win->maximized;
item->accelKey = accelParse(item->label);
// Minimize
// Minimize — not available on modal windows
item = &ctx->sysMenu.items[ctx->sysMenu.itemCount++];
strncpy(item->label, "Mi&nimize", MAX_MENU_LABEL - 1);
item->cmd = SysMenuMinimizeE;
item->separator = false;
item->enabled = true;
item->enabled = !win->modal;
item->accelKey = accelParse(item->label);
// Maximize — only if resizable and not maximized
@ -1461,10 +1683,7 @@ static void pollKeyboard(AppContextT *ctx) {
if (win->menuBar && win->menuBar->menuCount > 0) {
if (ctx->popup.active) {
// F10 again closes the menu
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
closeAllPopups(ctx);
} else {
dispatchAccelKey(ctx, win->menuBar->menus[0].accelKey);
}
@ -1699,22 +1918,21 @@ static void pollKeyboard(AppContextT *ctx) {
// Popup menu keyboard navigation (arrows, enter, esc)
if (ctx->popup.active && ascii == 0) {
MenuT *curMenu = ctx->popup.menu;
// Up arrow
if (scancode == 0x48) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
if (curMenu && curMenu->itemCount > 0) {
int32_t idx = ctx->popup.hoverItem;
for (int32_t tries = 0; tries < menu->itemCount; tries++) {
for (int32_t tries = 0; tries < curMenu->itemCount; tries++) {
idx--;
if (idx < 0) {
idx = menu->itemCount - 1;
idx = curMenu->itemCount - 1;
}
if (!menu->items[idx].separator) {
if (!curMenu->items[idx].separator) {
break;
}
}
@ -1722,25 +1940,23 @@ static void pollKeyboard(AppContextT *ctx) {
ctx->popup.hoverItem = idx;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
}
continue;
}
// Down arrow
if (scancode == 0x50) {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
if (curMenu && curMenu->itemCount > 0) {
int32_t idx = ctx->popup.hoverItem;
for (int32_t tries = 0; tries < menu->itemCount; tries++) {
for (int32_t tries = 0; tries < curMenu->itemCount; tries++) {
idx++;
if (idx >= menu->itemCount) {
if (idx >= curMenu->itemCount) {
idx = 0;
}
if (!menu->items[idx].separator) {
if (!curMenu->items[idx].separator) {
break;
}
}
@ -1748,11 +1964,15 @@ static void pollKeyboard(AppContextT *ctx) {
ctx->popup.hoverItem = idx;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
}
continue;
}
// Left arrow — switch to previous menu
// Left arrow — close submenu, or switch to previous top-level menu
if (scancode == 0x4B) {
if (ctx->popup.depth > 0) {
closePopupLevel(ctx);
} else {
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && win->menuBar->menuCount > 1) {
@ -1762,13 +1982,26 @@ static void pollKeyboard(AppContextT *ctx) {
newIdx = win->menuBar->menuCount - 1;
}
dispatchAccelKey(ctx, win->menuBar->menus[newIdx].accelKey);
openPopupAtMenu(ctx, win, newIdx);
}
}
continue;
}
// Right arrow — switch to next menu
// Right arrow — open submenu, or switch to next top-level menu
if (scancode == 0x4D) {
// If hovered item has a submenu, open it
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
MenuItemT *hItem = &curMenu->items[ctx->popup.hoverItem];
if (hItem->subMenu && hItem->enabled) {
openSubMenu(ctx);
continue;
}
}
// Otherwise switch to next top-level menu
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar && win->menuBar->menuCount > 1) {
@ -1778,74 +2011,77 @@ static void pollKeyboard(AppContextT *ctx) {
newIdx = 0;
}
dispatchAccelKey(ctx, win->menuBar->menus[newIdx].accelKey);
openPopupAtMenu(ctx, win, newIdx);
}
continue;
}
}
// Enter executes highlighted popup menu item
// Enter executes highlighted popup menu item (or opens submenu)
if (ctx->popup.active && ascii == 0x0D) {
if (ctx->popup.hoverItem >= 0) {
MenuT *curMenu = ctx->popup.menu;
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
MenuItemT *item = &curMenu->items[ctx->popup.hoverItem];
if (item->subMenu && item->enabled) {
openSubMenu(ctx);
} else if (item->enabled && !item->separator) {
int32_t menuId = item->id;
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
closeAllPopups(ctx);
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
if (ctx->popup.hoverItem < menu->itemCount) {
MenuItemT *item = &menu->items[ctx->popup.hoverItem];
if (item->enabled && !item->separator && win->onMenu) {
win->onMenu(win, item->id);
}
if (win && win->onMenu) {
win->onMenu(win, menuId);
}
}
} else {
closeAllPopups(ctx);
}
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
continue;
}
// Check for plain key accelerator in open popup menu
if (ctx->popup.active && ascii != 0) {
char lc = (ascii >= 'A' && ascii <= 'Z') ? (char)(ascii + 32) : (char)ascii;
for (int32_t j = 0; j < ctx->stack.count; j++) {
if (ctx->stack.windows[j]->id == ctx->popup.windowId) {
WindowT *win = ctx->stack.windows[j];
MenuBarT *bar = win->menuBar;
if (bar && ctx->popup.menuIdx < bar->menuCount) {
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
MenuT *curMenu = ctx->popup.menu;
// Try matching an item in the current popup
for (int32_t k = 0; k < menu->itemCount; k++) {
MenuItemT *item = &menu->items[k];
if (curMenu) {
for (int32_t k = 0; k < curMenu->itemCount; k++) {
MenuItemT *item = &curMenu->items[k];
if (item->accelKey == lc && item->enabled && !item->separator) {
if (win->onMenu) {
win->onMenu(win, item->id);
}
if (item->subMenu) {
ctx->popup.hoverItem = k;
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
openSubMenu(ctx);
} else {
int32_t menuId = item->id;
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
closeAllPopups(ctx);
// Close popup
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
goto nextKey;
if (win && win->onMenu) {
win->onMenu(win, menuId);
}
}
// No match in current popup — try switching to another menu
for (int32_t i = 0; i < bar->menuCount; i++) {
if (bar->menus[i].accelKey == lc && i != ctx->popup.menuIdx) {
dispatchAccelKey(ctx, lc);
goto nextKey;
}
}
}
break;
// No match in current popup — try switching to another top-level menu
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
if (win && win->menuBar) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
if (win->menuBar->menus[i].accelKey == lc && i != ctx->popup.menuIdx) {
openPopupAtMenu(ctx, win, i);
goto nextKey;
}
}
}
}
@ -1874,11 +2110,9 @@ static void pollKeyboard(AppContextT *ctx) {
continue;
}
// ESC closes popup menu
// ESC closes one popup level (or all if at top level)
if (ctx->popup.active && ascii == 0x1B) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
ctx->popup.popupW, ctx->popup.popupH);
ctx->popup.active = false;
closePopupLevel(ctx);
continue;
}
@ -2101,6 +2335,10 @@ static void updateCursorShape(AppContextT *ctx) {
newCursor = CURSOR_RESIZE_V;
}
}
// Active ListView column resize drag
else if (sResizeListView) {
newCursor = CURSOR_RESIZE_H;
}
// Not in an active drag/resize — check what we're hovering
else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) {
int32_t hitPart;
@ -2125,6 +2363,24 @@ static void updateCursorShape(AppContextT *ctx) {
} else if (vert) {
newCursor = CURSOR_RESIZE_V;
}
} else if (hitIdx >= 0 && hitPart == 0) {
// Hovering over content area — check for ListView column border
WindowT *win = ctx->stack.windows[hitIdx];
if (win->widgetRoot) {
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);
if (hit && hit->type == WidgetListViewE && widgetListViewColBorderHit(hit, vx, vy)) {
newCursor = CURSOR_RESIZE_H;
}
}
}
}

View file

@ -43,6 +43,7 @@ typedef struct AppContextT {
int32_t frameCount; // frame counter for periodic tasks
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
void *idleCtx;
WindowT *modalWindow; // if non-NULL, only this window receives input
} AppContextT;
// Initialize the application (VESA mode, input, etc.)

486
dvx/dvxDialog.c Normal file
View file

@ -0,0 +1,486 @@
// dvxDialog.c — Modal dialogs for DV/X GUI
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "widgets/widgetInternal.h"
#include <string.h>
// ============================================================
// Constants
// ============================================================
#define MSG_MAX_WIDTH 320
#define MSG_PADDING 8
#define ICON_AREA_WIDTH 40
#define BUTTON_WIDTH 80
#define BUTTON_HEIGHT 24
#define BUTTON_GAP 8
// ============================================================
// Prototypes
// ============================================================
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color);
static void onButtonClick(WidgetT *w);
static void onMsgBoxClose(WindowT *win);
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW);
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg);
// ============================================================
// Message box state (one active at a time)
// ============================================================
typedef struct {
AppContextT *ctx;
int32_t result;
bool done;
const char *message;
int32_t iconType;
int32_t textX;
int32_t textY;
int32_t textMaxW;
int32_t msgAreaH;
} MsgBoxStateT;
static MsgBoxStateT sMsgBox;
// ============================================================
// drawIconGlyph — draw a simple icon shape
// ============================================================
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color) {
if (iconType == MB_ICONINFO) {
// Circle outline with 'i'
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
rectFill(d, ops, x + 11, y + 6, 2, 2, color);
rectFill(d, ops, x + 11, y + 10, 2, 8, color);
} else if (iconType == MB_ICONWARNING) {
// Triangle outline with '!'
for (int32_t row = 0; row < 24; row++) {
int32_t halfW = row / 2;
int32_t lx = 12 - halfW;
int32_t rx = 12 + halfW;
rectFill(d, ops, x + lx, y + row, 1, 1, color);
rectFill(d, ops, x + rx, y + row, 1, 1, color);
if (row == 23) {
drawHLine(d, ops, x + lx, y + row, rx - lx + 1, color);
}
}
rectFill(d, ops, x + 11, y + 8, 2, 9, color);
rectFill(d, ops, x + 11, y + 19, 2, 2, color);
} else if (iconType == MB_ICONERROR) {
// Circle outline with X
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
for (int32_t i = 0; i < 12; i++) {
rectFill(d, ops, x + 6 + i, y + 6 + i, 2, 2, color);
rectFill(d, ops, x + 18 - i, y + 6 + i, 2, 2, color);
}
} else if (iconType == MB_ICONQUESTION) {
// Circle outline with '?'
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
rectFill(d, ops, x + 9, y + 6, 6, 2, color);
rectFill(d, ops, x + 13, y + 8, 2, 4, color);
rectFill(d, ops, x + 11, y + 12, 2, 3, color);
rectFill(d, ops, x + 11, y + 17, 2, 2, color);
}
}
// ============================================================
// dvxMessageBox
// ============================================================
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
int32_t btnFlags = flags & 0x000F;
int32_t iconFlags = flags & 0x00F0;
// Determine button labels and IDs
const char *btnLabels[3];
int32_t btnIds[3];
int32_t btnCount = 0;
switch (btnFlags) {
case MB_OK:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnCount = 1;
break;
case MB_OKCANCEL:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnLabels[1] = "&Cancel";
btnIds[1] = ID_CANCEL;
btnCount = 2;
break;
case MB_YESNO:
btnLabels[0] = "&Yes";
btnIds[0] = ID_YES;
btnLabels[1] = "&No";
btnIds[1] = ID_NO;
btnCount = 2;
break;
case MB_YESNOCANCEL:
btnLabels[0] = "&Yes";
btnIds[0] = ID_YES;
btnLabels[1] = "&No";
btnIds[1] = ID_NO;
btnLabels[2] = "&Cancel";
btnIds[2] = ID_CANCEL;
btnCount = 3;
break;
case MB_RETRYCANCEL:
btnLabels[0] = "&Retry";
btnIds[0] = ID_RETRY;
btnLabels[1] = "&Cancel";
btnIds[1] = ID_CANCEL;
btnCount = 2;
break;
default:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnCount = 1;
break;
}
// Calculate message text dimensions
bool hasIcon = (iconFlags != 0);
int32_t textMaxW = MSG_MAX_WIDTH - MSG_PADDING * 2 - (hasIcon ? ICON_AREA_WIDTH : 0);
int32_t textH = wordWrapHeight(&ctx->font, message, textMaxW);
// Calculate content area sizes
int32_t msgAreaH = textH + MSG_PADDING * 2;
int32_t iconAreaH = hasIcon ? (24 + MSG_PADDING * 2) : 0;
if (msgAreaH < iconAreaH) {
msgAreaH = iconAreaH;
}
int32_t buttonsW = btnCount * BUTTON_WIDTH + (btnCount - 1) * BUTTON_GAP;
int32_t contentW = MSG_MAX_WIDTH;
if (buttonsW + MSG_PADDING * 2 > contentW) {
contentW = buttonsW + MSG_PADDING * 2;
}
int32_t contentH = msgAreaH + BUTTON_HEIGHT + MSG_PADDING * 3;
// Create the dialog window (non-resizable)
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE;
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title, winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) {
return ID_CANCEL;
}
win->modal = true;
// Set up state
sMsgBox.ctx = ctx;
sMsgBox.result = ID_CANCEL;
sMsgBox.done = false;
sMsgBox.message = message;
sMsgBox.iconType = iconFlags;
sMsgBox.textX = MSG_PADDING + (hasIcon ? ICON_AREA_WIDTH : 0);
sMsgBox.textY = MSG_PADDING;
sMsgBox.textMaxW = textMaxW;
sMsgBox.msgAreaH = msgAreaH;
// Create button widgets using wgtInitWindow for proper root setup
// (sets onPaint, onMouse, onKey, onResize, userData on root)
WidgetT *root = wgtInitWindow(ctx, win);
// Override onPaint with our custom handler, set window-level state
win->userData = &sMsgBox;
win->onPaint = onMsgBoxPaint;
win->onClose = onMsgBoxClose;
win->maxW = win->w;
win->maxH = win->h;
if (root) {
// Spacer for message area (text/icon drawn by onPaint)
WidgetT *msgSpacer = wgtSpacer(root);
if (msgSpacer) {
msgSpacer->minH = wgtPixels(msgAreaH);
}
// Button row centered
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignCenterE;
for (int32_t i = 0; i < btnCount; i++) {
WidgetT *btn = wgtButton(btnRow, btnLabels[i]);
if (btn) {
btn->minW = wgtPixels(BUTTON_WIDTH);
btn->minH = wgtPixels(BUTTON_HEIGHT);
btn->userData = (void *)(intptr_t)btnIds[i];
btn->onClick = onButtonClick;
}
}
}
// Bottom padding
WidgetT *bottomSpacer = wgtSpacer(root);
if (bottomSpacer) {
bottomSpacer->minH = wgtPixels(MSG_PADDING);
}
}
// Initial paint (window is already correctly sized, don't call dvxFitWindow)
RectT fullRect = { 0, 0, win->contentW, win->contentH };
win->onPaint(win, &fullRect);
// Set as modal
ctx->modalWindow = win;
// Nested event loop
while (!sMsgBox.done && ctx->running) {
dvxUpdate(ctx);
}
// Clean up
ctx->modalWindow = NULL;
dvxDestroyWindow(ctx, win);
return sMsgBox.result;
}
// ============================================================
// onButtonClick
// ============================================================
static void onButtonClick(WidgetT *w) {
sMsgBox.result = (int32_t)(intptr_t)w->userData;
sMsgBox.done = true;
}
// ============================================================
// onMsgBoxClose
// ============================================================
static void onMsgBoxClose(WindowT *win) {
(void)win;
sMsgBox.result = ID_CANCEL;
sMsgBox.done = true;
}
// ============================================================
// onMsgBoxPaint — custom paint: background + text/icon + widgets
// ============================================================
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
(void)dirtyArea;
MsgBoxStateT *state = (MsgBoxStateT *)win->userData;
AppContextT *ctx = state->ctx;
// Set up display context pointing at content buffer
DisplayT cd = ctx->display;
cd.lfb = win->contentBuf;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Fill background with window face color (not content bg — dialog style)
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.windowFace);
// Draw word-wrapped message text
wordWrapDraw(&cd, &ctx->blitOps, &ctx->font, state->textX, state->textY, state->message, state->textMaxW, ctx->colors.contentFg, ctx->colors.windowFace);
// Draw icon
if (state->iconType != 0) {
drawIconGlyph(&cd, &ctx->blitOps, MSG_PADDING, MSG_PADDING, state->iconType, ctx->colors.contentFg);
}
// Draw separator line above buttons
drawHLine(&cd, &ctx->blitOps, 0, state->msgAreaH, win->contentW, ctx->colors.windowShadow);
drawHLine(&cd, &ctx->blitOps, 0, state->msgAreaH + 1, win->contentW, ctx->colors.windowHighlight);
// Paint widget tree (buttons) on top
if (win->widgetRoot) {
WidgetT *root = win->widgetRoot;
// Layout widgets
widgetCalcMinSizeTree(root, &ctx->font);
root->x = 0;
root->y = 0;
root->w = win->contentW;
root->h = win->contentH;
widgetLayoutChildren(root, &ctx->font);
// Paint widgets
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
}
}
// ============================================================
// wordWrapDraw — draw word-wrapped text
// ============================================================
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t curY = y;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
// Skip leading spaces
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
// Handle explicit newlines
if (*text == '\n') {
curY += lineH;
text++;
continue;
}
// Find how many characters fit on this line
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
// If we didn't reach end and didn't hit newline, wrap at word boundary
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
drawTextN(d, ops, font, x, curY, text, lineLen, fg, bg, true);
curY += lineH;
text += lineLen;
}
}
// ============================================================
// wordWrapHeight — compute height of word-wrapped text
// ============================================================
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t lines = 0;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
if (*text == '\n') {
lines++;
text++;
continue;
}
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
lines++;
text += lineLen;
}
if (lines == 0) {
lines = 1;
}
return lines * lineH;
}

40
dvx/dvxDialog.h Normal file
View file

@ -0,0 +1,40 @@
// dvxDialog.h — Modal dialogs for DV/X GUI
#ifndef DVX_DIALOG_H
#define DVX_DIALOG_H
#include "dvxApp.h"
// ============================================================
// Message box button flags
// ============================================================
#define MB_OK 0x0000
#define MB_OKCANCEL 0x0001
#define MB_YESNO 0x0002
#define MB_YESNOCANCEL 0x0003
#define MB_RETRYCANCEL 0x0004
// ============================================================
// Message box icon flags
// ============================================================
#define MB_ICONINFO 0x0010
#define MB_ICONWARNING 0x0020
#define MB_ICONERROR 0x0030
#define MB_ICONQUESTION 0x0040
// ============================================================
// Message box return values
// ============================================================
#define ID_OK 1
#define ID_CANCEL 2
#define ID_YES 3
#define ID_NO 4
#define ID_RETRY 5
// Show a modal message box and return which button was pressed.
// flags = MB_xxx button flag | MB_ICONxxx icon flag
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags);
#endif // DVX_DIALOG_H

View file

@ -152,6 +152,9 @@ typedef struct {
#define MAX_MENUS 8
#define MAX_MENU_LABEL 32
// Forward declaration for submenu pointers
typedef struct MenuT MenuT;
typedef struct {
char label[MAX_MENU_LABEL];
int32_t id; // application-defined command ID
@ -159,16 +162,17 @@ typedef struct {
bool enabled;
bool checked;
char accelKey; // lowercase accelerator character, 0 if none
MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item)
} MenuItemT;
typedef struct {
struct MenuT {
char label[MAX_MENU_LABEL]; // menu bar label (e.g. "File")
MenuItemT items[MAX_MENU_ITEMS];
int32_t itemCount;
int32_t barX; // computed position on menu bar
int32_t barW; // computed width on menu bar
char accelKey; // lowercase accelerator character, 0 if none
} MenuT;
};
typedef struct {
MenuT menus[MAX_MENUS];
@ -234,6 +238,7 @@ typedef struct WindowT {
bool minimized;
bool maximized;
bool resizable;
bool modal;
bool contentDirty; // true when contentBuf has changed since last icon refresh
int32_t maxW; // maximum width (-1 = screen width)
int32_t maxH; // maximum height (-1 = screen height)
@ -305,18 +310,34 @@ typedef struct {
} CursorT;
// ============================================================
// Popup state for dropdown menus
// Popup state for dropdown menus (with cascading submenu support)
// ============================================================
#define MAX_SUBMENU_DEPTH 4
// Saved parent popup state when a submenu is open
typedef struct {
bool active;
int32_t windowId; // which window owns this popup
int32_t menuIdx; // which menu is open
int32_t popupX; // screen position of popup
MenuT *menu;
int32_t menuIdx;
int32_t popupX;
int32_t popupY;
int32_t popupW;
int32_t popupH;
int32_t hoverItem; // which item is highlighted (-1 = none)
int32_t hoverItem;
} PopupLevelT;
typedef struct {
bool active;
int32_t windowId; // which window owns this popup chain
int32_t menuIdx; // which menu bar menu is open (top level)
int32_t popupX; // screen position of current (deepest) popup
int32_t popupY;
int32_t popupW;
int32_t popupH;
int32_t hoverItem; // highlighted item in current popup (-1 = none)
MenuT *menu; // direct pointer to current menu (avoids lookup for submenus)
int32_t depth; // 0 = top-level only, 1+ = submenu depth
PopupLevelT parentStack[MAX_SUBMENU_DEPTH];
} PopupStateT;
// ============================================================

View file

@ -292,10 +292,18 @@ void resetClipRect(DisplayT *d) {
// ============================================================
void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) {
int32_t x2 = x + w;
int32_t y2 = y + h;
if (x < 0) { x = 0; }
if (y < 0) { y = 0; }
if (x2 > d->width) { x2 = d->width; }
if (y2 > d->height) { y2 = d->height; }
d->clipX = x;
d->clipY = y;
d->clipW = w;
d->clipH = h;
d->clipW = (x2 > x) ? x2 - x : 0;
d->clipH = (y2 > y) ? y2 - y : 0;
}

View file

@ -67,9 +67,34 @@ typedef enum {
WidgetImageE,
WidgetImageButtonE,
WidgetCanvasE,
WidgetAnsiTermE
WidgetAnsiTermE,
WidgetListViewE
} WidgetTypeE;
// ============================================================
// ListView types
// ============================================================
#define LISTVIEW_MAX_COLS 16
typedef enum {
ListViewAlignLeftE,
ListViewAlignCenterE,
ListViewAlignRightE
} ListViewAlignE;
typedef enum {
ListViewSortNoneE,
ListViewSortAscE,
ListViewSortDescE
} ListViewSortE;
typedef struct {
const char *title;
int32_t width; // tagged size (wgtPixels/wgtChars/wgtPercent, 0 = auto)
ListViewAlignE align;
} ListViewColT;
// ============================================================
// Alignment enum
// ============================================================
@ -376,6 +401,22 @@ typedef struct WidgetT {
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);
int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len);
} ansiTerm;
struct {
const ListViewColT *cols;
int32_t colCount;
const char **cellData;
int32_t rowCount;
int32_t selectedIdx;
int32_t scrollPos;
int32_t scrollPosH;
int32_t sortCol;
ListViewSortE sortDir;
int32_t resolvedColW[LISTVIEW_MAX_COLS];
int32_t totalColW;
int32_t *sortIndex;
void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
} listView;
} as;
} WidgetT;
@ -485,6 +526,18 @@ WidgetT *wgtTreeItem(WidgetT *parent, const char *text);
void wgtTreeItemSetExpanded(WidgetT *w, bool expanded);
bool wgtTreeItemIsExpanded(const WidgetT *w);
// ============================================================
// ListView (multi-column list)
// ============================================================
WidgetT *wgtListView(WidgetT *parent);
void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count);
void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount);
int32_t wgtListViewGetSelected(const WidgetT *w);
void wgtListViewSetSelected(WidgetT *w, int32_t idx);
void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir);
void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir));
// ============================================================
// ImageButton
// ============================================================

View file

@ -41,6 +41,7 @@ typedef struct {
static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font);
static void computeTitleGeom(const WindowT *win, TitleGeomT *g);
static void freeMenuRecursive(MenuT *menu);
static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h);
static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win);
static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win);
@ -104,11 +105,16 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) {
g->maxX = -1;
}
if (win->modal) {
g->minX = -1;
} else {
g->minX = rightX;
rightX -= g->gadgetS + GADGET_PAD;
}
// Text area between close gadget and minimize gadget
// Text area between close gadget and first right-side gadget
g->textLeftEdge = g->closeX + g->gadgetS + GADGET_PAD;
g->textRightEdge = g->minX - GADGET_PAD;
g->textRightEdge = rightX + g->gadgetS;
}
@ -468,12 +474,14 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
}
}
// Minimize gadget (always present)
// Minimize gadget (not on modal windows)
if (g.minX >= 0) {
drawTitleGadget(d, ops, colors, g.minX, g.gadgetY, g.gadgetS);
// Small square centered in minimize gadget
int32_t miniSize = 4;
rectFill(d, ops, g.minX + (g.gadgetS - miniSize) / 2, g.gadgetY + (g.gadgetS - miniSize) / 2,
miniSize, miniSize, colors->contentFg);
}
// Title text — centered between close gadget and minimize, truncated to fit
int32_t availW = g.textRightEdge - g.textLeftEdge;
@ -502,6 +510,21 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
}
// ============================================================
// freeMenuRecursive
// ============================================================
static void freeMenuRecursive(MenuT *menu) {
for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].subMenu) {
freeMenuRecursive(menu->items[i].subMenu);
free(menu->items[i].subMenu);
menu->items[i].subMenu = NULL;
}
}
}
// ============================================================
// minimizedIconPos
// ============================================================
@ -639,6 +662,36 @@ void wmAddMenuSeparator(MenuT *menu) {
}
// ============================================================
// wmAddSubMenu
// ============================================================
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label) {
if (parentMenu->itemCount >= MAX_MENU_ITEMS) {
return NULL;
}
MenuT *child = (MenuT *)calloc(1, sizeof(MenuT));
if (!child) {
return NULL;
}
MenuItemT *item = &parentMenu->items[parentMenu->itemCount];
memset(item, 0, sizeof(*item));
strncpy(item->label, label, MAX_MENU_LABEL - 1);
item->label[MAX_MENU_LABEL - 1] = '\0';
item->id = -1;
item->separator = false;
item->enabled = true;
item->accelKey = accelParse(label);
item->subMenu = child;
parentMenu->itemCount++;
return child;
}
// ============================================================
// wmAddVScrollbar
// ============================================================
@ -756,6 +809,10 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
}
if (win->menuBar) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
freeMenuRecursive(&win->menuBar->menus[i]);
}
free(win->menuBar);
}
@ -1026,8 +1083,9 @@ int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hi
return i;
}
// Minimize gadget (always present)
if (mx >= g.minX && mx < g.minX + g.gadgetS &&
// Minimize gadget (not on modal windows)
if (g.minX >= 0 &&
mx >= g.minX && mx < g.minX + g.gadgetS &&
my >= g.gadgetY && my < g.gadgetY + g.gadgetS) {
*hitPart = 7;
return i;

View file

@ -37,6 +37,9 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id);
// Add a separator to a menu
void wmAddMenuSeparator(MenuT *menu);
// Add a submenu item (returns the child MenuT to populate, or NULL on failure)
MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label);
// Add a vertical scrollbar to a window
ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize);

View file

@ -344,6 +344,19 @@ static const WidgetClassT sClassCanvas = {
.setText = NULL
};
static const WidgetClassT sClassListView = {
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE,
.paint = widgetListViewPaint,
.paintOverlay = NULL,
.calcMinSize = widgetListViewCalcMinSize,
.layout = NULL,
.onMouse = widgetListViewOnMouse,
.onKey = widgetListViewOnKey,
.destroy = widgetListViewDestroy,
.getText = NULL,
.setText = NULL
};
static const WidgetClassT sClassAnsiTerm = {
.flags = WCLASS_FOCUSABLE,
.paint = widgetAnsiTermPaint,
@ -388,5 +401,6 @@ const WidgetClassT *widgetClassTable[] = {
[WidgetImageE] = &sClassImage,
[WidgetImageButtonE] = &sClassImageButton,
[WidgetCanvasE] = &sClassCanvas,
[WidgetAnsiTermE] = &sClassAnsiTerm
[WidgetAnsiTermE] = &sClassAnsiTerm,
[WidgetListViewE] = &sClassListView
};

View file

@ -14,6 +14,10 @@ WidgetT *sDragSlider = NULL;
WidgetT *sDrawingCanvas = NULL;
WidgetT *sDragTextSelect = NULL;
int32_t sDragOffset = 0;
WidgetT *sResizeListView = NULL;
int32_t sResizeCol = -1;
int32_t sResizeStartX = 0;
int32_t sResizeOrigW = 0;
// ============================================================
@ -130,6 +134,11 @@ void widgetDestroyChildren(WidgetT *w) {
sDrawingCanvas = NULL;
}
if (sResizeListView == child) {
sResizeListView = NULL;
sResizeCol = -1;
}
free(child);
child = next;
}

View file

@ -249,6 +249,40 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
return;
}
// Handle ListView column resize release
if (sResizeListView && !(buttons & 1)) {
sResizeListView = NULL;
sResizeCol = -1;
return;
}
// Handle ListView column resize drag
if (sResizeListView && (buttons & 1)) {
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t delta = (x + scrollX) - sResizeStartX;
int32_t newW = sResizeOrigW + delta;
if (newW < 20) {
newW = 20;
}
if (newW != sResizeListView->as.listView.resolvedColW[sResizeCol]) {
sResizeListView->as.listView.resolvedColW[sResizeCol] = newW;
// Recalculate totalColW
int32_t total = 0;
for (int32_t c = 0; c < sResizeListView->as.listView.colCount; c++) {
total += sResizeListView->as.listView.resolvedColW[c];
}
sResizeListView->as.listView.totalColW = total;
wgtInvalidatePaint(root);
}
return;
}
// Handle button press release
if (sPressedButton && !(buttons & 1)) {
if (sPressedButton->type == WidgetImageButtonE) {

View file

@ -95,6 +95,10 @@ extern WidgetT *sDragSlider;
extern WidgetT *sDrawingCanvas;
extern WidgetT *sDragTextSelect;
extern int32_t sDragOffset;
extern WidgetT *sResizeListView;
extern int32_t sResizeCol;
extern int32_t sResizeStartX;
extern int32_t sResizeOrigW;
// ============================================================
// Core functions (widgetCore.c)
@ -161,6 +165,8 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
void widgetImagePaint(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);
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy);
void widgetProgressBarPaint(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);
@ -186,6 +192,7 @@ void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font);
@ -234,6 +241,7 @@ void widgetCanvasDestroy(WidgetT *w);
void widgetComboBoxDestroy(WidgetT *w);
void widgetImageButtonDestroy(WidgetT *w);
void widgetImageDestroy(WidgetT *w);
void widgetListViewDestroy(WidgetT *w);
void widgetTextAreaDestroy(WidgetT *w);
void widgetTextInputDestroy(WidgetT *w);
@ -262,6 +270,8 @@ void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy)
void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetListBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetListViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod);

1009
dvx/widgets/widgetListView.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -238,15 +238,19 @@ void wgtInvalidate(WidgetT *w) {
}
// Manage scrollbars (measures, adds/removes scrollbars, relayouts)
// Skip if window has a custom paint handler (e.g. dialog) that manages its own layout
if (w->window->onPaint == widgetOnPaint) {
widgetManageScrollbars(w->window, ctx);
}
// Repaint
RectT fullRect = {0, 0, w->window->contentW, w->window->contentH};
widgetOnPaint(w->window, &fullRect);
w->window->contentDirty = true;
// Repaint (use win->onPaint so custom paint handlers like dialogs work)
WindowT *win = w->window;
RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect);
win->contentDirty = true;
// Dirty the window on screen
dvxInvalidateWindow(ctx, w->window);
dvxInvalidateWindow(ctx, win);
}
@ -275,12 +279,13 @@ void wgtInvalidatePaint(WidgetT *w) {
return;
}
// Repaint without measure/layout
RectT fullRect = {0, 0, w->window->contentW, w->window->contentH};
widgetOnPaint(w->window, &fullRect);
w->window->contentDirty = true;
// Repaint without measure/layout (use win->onPaint so custom handlers work)
WindowT *win = w->window;
RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect);
win->contentDirty = true;
dvxInvalidateWindow(ctx, w->window);
dvxInvalidateWindow(ctx, win);
}

View file

@ -84,7 +84,7 @@ void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
tabHeaderW += labelW;
}
w->calcMinW = DVX_MAX(maxPageW + TAB_BORDER * 2, tabHeaderW);
w->calcMinW = maxPageW + TAB_BORDER * 2;
w->calcMinH = tabH + maxPageH + TAB_BORDER * 2;
}

View file

@ -1,6 +1,7 @@
// demo.c — DV/X GUI demonstration application
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxWidget.h"
#include <stdio.h>
@ -23,6 +24,9 @@
#define CMD_EDIT_PASTE 202
#define CMD_VIEW_TERM 300
#define CMD_VIEW_CTRL 301
#define CMD_VIEW_ZOOM_IN 302
#define CMD_VIEW_ZOOM_OUT 303
#define CMD_VIEW_ZOOM_FIT 304
#define CMD_HELP_ABOUT 400
// ============================================================
@ -119,9 +123,15 @@ static void onCloseMainCb(WindowT *win) {
AppContextT *ctx = (AppContextT *)win->userData;
if (ctx) {
int32_t result = dvxMessageBox(ctx, "Exit",
"Are you sure you want to exit?",
MB_YESNO | MB_ICONQUESTION);
if (result == ID_YES) {
dvxQuit(ctx);
}
}
}
// ============================================================
@ -134,8 +144,14 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
switch (menuId) {
case CMD_FILE_EXIT:
if (ctx) {
int32_t result = dvxMessageBox(ctx, "Exit",
"Are you sure you want to exit?",
MB_YESNO | MB_ICONQUESTION);
if (result == ID_YES) {
dvxQuit(ctx);
}
}
break;
case CMD_VIEW_TERM:
@ -147,6 +163,10 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
break;
case CMD_HELP_ABOUT:
dvxMessageBox(sCtx, "About DV/X Demo",
"DV/X GUI Demonstration\n\n"
"A DESQview/X-style windowing system for DOS.",
MB_OK | MB_ICONINFO);
break;
}
}
@ -419,10 +439,39 @@ static void setupControlsWindow(AppContextT *ctx) {
wgtTreeItem(config, "settings.ini");
wgtTreeItem(config, "palette.dat");
// --- Tab 3: Toolbar (ImageButtons + VSeparator) ---
WidgetT *page3 = wgtTabPage(tabs, "Tool&bar");
// --- Tab 3: ListView (multi-column list) ---
WidgetT *page3 = wgtTabPage(tabs, "&List");
WidgetT *tb = wgtToolbar(page3);
static const ListViewColT lvCols[] = {
{"Name", (int32_t)(WGT_SIZE_CHARS | 16), ListViewAlignLeftE},
{"Size", (int32_t)(WGT_SIZE_CHARS | 8), ListViewAlignRightE},
{"Type", (int32_t)(WGT_SIZE_CHARS | 12), ListViewAlignLeftE},
{"Modified", (int32_t)(WGT_SIZE_CHARS | 12), ListViewAlignLeftE}
};
static const char *lvData[] = {
"AUTOEXEC.BAT", "412", "Batch File", "03/15/1994",
"CONFIG.SYS", "256", "System File", "03/15/1994",
"COMMAND.COM", "54,645", "Application", "09/30/1993",
"HIMEM.SYS", "29,136", "System Driver", "09/30/1993",
"EMM386.EXE", "120,926", "Application", "09/30/1993",
"MOUSE.COM", "56,408", "Application", "06/01/1994",
"DOSKEY.COM", "5,883", "Application", "09/30/1993",
"EDIT.COM", "413", "Application", "09/30/1993",
"README.TXT", "8,192", "Text File", "01/10/1994",
"DVXDEMO.EXE", "98,304", "Application", "03/15/2026"
};
WidgetT *lv = wgtListView(page3);
wgtListViewSetColumns(lv, lvCols, 4);
wgtListViewSetData(lv, lvData, 10);
wgtListViewSetSelected(lv, 0);
lv->weight = 100;
// --- Tab 4: Toolbar (ImageButtons + VSeparator) ---
WidgetT *page4 = wgtTabPage(tabs, "Tool&bar");
WidgetT *tb = wgtToolbar(page4);
int32_t imgW;
int32_t imgH;
@ -453,18 +502,18 @@ static void setupControlsWindow(AppContextT *ctx) {
WidgetT *btnHelp = wgtButton(tb, "&Help");
btnHelp->onClick = onToolbarClick;
wgtLabel(page3, "ImageButtons with VSeparator.");
wgtLabel(page4, "ImageButtons with VSeparator.");
// --- Tab 4: Media (Image, ImageFromFile) ---
WidgetT *page4 = wgtTabPage(tabs, "&Media");
// --- Tab 5: Media (Image, ImageFromFile) ---
WidgetT *page5 = wgtTabPage(tabs, "&Media");
wgtLabel(page4, "ImageFromFile (sample.bmp):");
wgtImageFromFile(page4, "sample.bmp");
wgtLabel(page5, "ImageFromFile (sample.bmp):");
wgtImageFromFile(page5, "sample.bmp");
wgtHSeparator(page4);
wgtHSeparator(page5);
wgtLabel(page4, "Image (logo.bmp):");
WidgetT *imgRow = wgtHBox(page4);
wgtLabel(page5, "Image (logo.bmp):");
WidgetT *imgRow = wgtHBox(page5);
uint8_t *logoData = loadBmpPixels(ctx, "logo.bmp", &imgW, &imgH, &imgPitch);
if (logoData) {
wgtImage(imgRow, logoData, imgW, imgH, imgPitch);
@ -472,19 +521,19 @@ static void setupControlsWindow(AppContextT *ctx) {
wgtVSeparator(imgRow);
wgtLabel(imgRow, "32x32 DV/X logo");
// --- Tab 5: Editor (TextArea, Canvas) ---
WidgetT *page5 = wgtTabPage(tabs, "&Editor");
// --- Tab 6: Editor (TextArea, Canvas) ---
WidgetT *page6 = wgtTabPage(tabs, "&Editor");
wgtLabel(page5, "TextArea:");
WidgetT *ta = wgtTextArea(page5, 512);
wgtLabel(page6, "TextArea:");
WidgetT *ta = wgtTextArea(page6, 512);
ta->weight = 100;
wgtSetText(ta, "Multi-line text editor.\n\nFeatures:\n- Word wrap\n- Selection\n- Copy/Paste\n- Undo (Ctrl+Z)");
wgtHSeparator(page5);
wgtHSeparator(page6);
wgtLabel(page5, "Canvas (draw with mouse):");
wgtLabel(page6, "Canvas (draw with mouse):");
const DisplayT *d = dvxGetDisplay(ctx);
WidgetT *cv = wgtCanvas(page5, 280, 80);
WidgetT *cv = wgtCanvas(page6, 280, 80);
wgtCanvasSetPenColor(cv, packColor(d, 200, 0, 0));
wgtCanvasDrawRect(cv, 5, 5, 50, 35);
wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 200));
@ -544,6 +593,16 @@ static void setupMainWindow(AppContextT *ctx) {
if (viewMenu) {
wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM);
wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL);
wmAddMenuSeparator(viewMenu);
MenuT *zoomMenu = wmAddSubMenu(viewMenu, "&Zoom");
if (zoomMenu) {
wmAddMenuItem(zoomMenu, "Zoom &In", CMD_VIEW_ZOOM_IN);
wmAddMenuItem(zoomMenu, "Zoom &Out", CMD_VIEW_ZOOM_OUT);
wmAddMenuSeparator(zoomMenu);
wmAddMenuItem(zoomMenu, "&Fit to Window", CMD_VIEW_ZOOM_FIT);
}
}
MenuT *helpMenu = wmAddMenu(bar, "&Help");