Added message box, multi-column list/table, and submenus.
This commit is contained in:
parent
97530513b8
commit
6f8aeda7b2
18 changed files with 2370 additions and 301 deletions
|
|
@ -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)
|
||||
|
|
|
|||
746
dvx/dvxApp.c
746
dvx/dvxApp.c
|
|
@ -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;
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Click on item
|
||||
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 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 (bar && ctx->popup.menuIdx < bar->menuCount) {
|
||||
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
|
||||
|
||||
if (itemIdx >= 0 && itemIdx < menu->itemCount) {
|
||||
MenuItemT *item = &menu->items[itemIdx];
|
||||
|
||||
if (item->enabled && !item->separator && win->onMenu) {
|
||||
win->onMenu(win, item->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
if (hItem->subMenu && hItem->enabled) {
|
||||
openSubMenu(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Close popup
|
||||
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY,
|
||||
ctx->popup.popupW, ctx->popup.popupH);
|
||||
ctx->popup.active = false;
|
||||
// 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)) {
|
||||
if (ctx->popup.menu && itemIdx >= 0 && itemIdx < ctx->popup.menu->itemCount) {
|
||||
MenuItemT *item = &ctx->popup.menu->items[itemIdx];
|
||||
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
||||
// Click outside all popups and menu bar — close everything
|
||||
closeAllPopups(ctx);
|
||||
}
|
||||
} else if ((buttons & 1) && !(prevBtn & 1)) {
|
||||
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;
|
||||
}
|
||||
|
||||
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:
|
||||
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);
|
||||
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
|
||||
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);
|
||||
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 (curMenu && curMenu->itemCount > 0) {
|
||||
int32_t idx = ctx->popup.hoverItem;
|
||||
|
||||
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
|
||||
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
|
||||
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 (curMenu && curMenu->itemCount > 0) {
|
||||
int32_t idx = ctx->popup.hoverItem;
|
||||
|
||||
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
|
||||
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
|
||||
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,27 +1964,44 @@ 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) {
|
||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||
if (ctx->popup.depth > 0) {
|
||||
closePopupLevel(ctx);
|
||||
} else {
|
||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||
|
||||
if (win && win->menuBar && win->menuBar->menuCount > 1) {
|
||||
int32_t newIdx = ctx->popup.menuIdx - 1;
|
||||
if (win && win->menuBar && win->menuBar->menuCount > 1) {
|
||||
int32_t newIdx = ctx->popup.menuIdx - 1;
|
||||
|
||||
if (newIdx < 0) {
|
||||
newIdx = win->menuBar->menuCount - 1;
|
||||
if (newIdx < 0) {
|
||||
newIdx = win->menuBar->menuCount - 1;
|
||||
}
|
||||
|
||||
openPopupAtMenu(ctx, win, newIdx);
|
||||
}
|
||||
|
||||
dispatchAccelKey(ctx, win->menuBar->menus[newIdx].accelKey);
|
||||
}
|
||||
|
||||
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) {
|
||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||
MenuT *curMenu = ctx->popup.menu;
|
||||
|
||||
if (win && win->menuBar && ctx->popup.menuIdx < win->menuBar->menuCount) {
|
||||
MenuT *menu = &win->menuBar->menus[ctx->popup.menuIdx];
|
||||
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
|
||||
MenuItemT *item = &curMenu->items[ctx->popup.hoverItem];
|
||||
|
||||
if (ctx->popup.hoverItem < menu->itemCount) {
|
||||
MenuItemT *item = &menu->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 (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;
|
||||
char lc = (ascii >= 'A' && ascii <= 'Z') ? (char)(ascii + 32) : (char)ascii;
|
||||
MenuT *curMenu = ctx->popup.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;
|
||||
// Try matching an item in the current popup
|
||||
if (curMenu) {
|
||||
for (int32_t k = 0; k < curMenu->itemCount; k++) {
|
||||
MenuItemT *item = &curMenu->items[k];
|
||||
|
||||
if (bar && ctx->popup.menuIdx < bar->menuCount) {
|
||||
MenuT *menu = &bar->menus[ctx->popup.menuIdx];
|
||||
if (item->accelKey == lc && item->enabled && !item->separator) {
|
||||
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);
|
||||
|
||||
// Try matching an item in the current popup
|
||||
for (int32_t k = 0; k < menu->itemCount; k++) {
|
||||
MenuItemT *item = &menu->items[k];
|
||||
|
||||
if (item->accelKey == lc && item->enabled && !item->separator) {
|
||||
if (win->onMenu) {
|
||||
win->onMenu(win, item->id);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
486
dvx/dvxDialog.c
Normal 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
40
dvx/dvxDialog.h
Normal 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
|
||||
|
|
@ -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;
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
80
dvx/dvxWm.c
80
dvx/dvxWm.c
|
|
@ -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;
|
||||
}
|
||||
|
||||
g->minX = rightX;
|
||||
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)
|
||||
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);
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
1009
dvx/widgets/widgetListView.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -238,15 +238,19 @@ void wgtInvalidate(WidgetT *w) {
|
|||
}
|
||||
|
||||
// Manage scrollbars (measures, adds/removes scrollbars, relayouts)
|
||||
widgetManageScrollbars(w->window, ctx);
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,7 +123,13 @@ static void onCloseMainCb(WindowT *win) {
|
|||
AppContextT *ctx = (AppContextT *)win->userData;
|
||||
|
||||
if (ctx) {
|
||||
dvxQuit(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,7 +144,13 @@ static void onMenuCb(WindowT *win, int32_t menuId) {
|
|||
switch (menuId) {
|
||||
case CMD_FILE_EXIT:
|
||||
if (ctx) {
|
||||
dvxQuit(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;
|
||||
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue