From 7476367ace338a1976792d258a433d1f78f3f24e Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 15 Mar 2026 21:04:40 -0500 Subject: [PATCH] Checkboxes and radio buttons in menus. --- dvx/dvxApp.c | 102 ++++++++++++++++++++++++++++++++++++++++++++++++- dvx/dvxApp.h | 6 +++ dvx/dvxTypes.h | 21 ++++++---- dvx/dvxWm.c | 44 +++++++++++++++++++++ dvx/dvxWm.h | 6 +++ dvxdemo/demo.c | 23 ++++++++--- 6 files changed, 188 insertions(+), 14 deletions(-) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 55e0c9b..4be1a3c 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -15,6 +15,7 @@ #define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2) #define ICON_REFRESH_INTERVAL 8 #define KB_MOVE_STEP 8 +#define MENU_CHECK_WIDTH 14 #define SUBMENU_ARROW_WIDTH 12 // ============================================================ @@ -22,6 +23,7 @@ // ============================================================ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph); +static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx); static void closeAllPopups(AppContextT *ctx); static void closePopupLevel(AppContextT *ctx); static void closeSysMenu(AppContextT *ctx); @@ -74,6 +76,7 @@ static const char sAltScanToAscii[256] = { static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) { int32_t maxW = 0; bool hasSub = false; + bool hasCheck = false; for (int32_t k = 0; k < menu->itemCount; k++) { int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label); @@ -85,13 +88,50 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw if (menu->items[k].subMenu) { hasSub = true; } + + if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) { + hasCheck = true; + } } - *pw = maxW + CHROME_TITLE_PAD * 2 + 8 + (hasSub ? SUBMENU_ARROW_WIDTH : 0); + *pw = maxW + CHROME_TITLE_PAD * 2 + 8 + (hasSub ? SUBMENU_ARROW_WIDTH : 0) + (hasCheck ? MENU_CHECK_WIDTH : 0); *ph = menu->itemCount * ctx->font.charHeight + 4; } +// ============================================================ +// clickMenuCheckRadio — toggle check or select radio on click +// ============================================================ + +static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) { + MenuItemT *item = &menu->items[itemIdx]; + + if (item->type == MenuItemCheckE) { + item->checked = !item->checked; + } else if (item->type == MenuItemRadioE) { + // Uncheck all radio items in the same group (consecutive radio items) + // Search backward to find group start + int32_t groupStart = itemIdx; + + while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) { + groupStart--; + } + + // Search forward to find group end + int32_t groupEnd = itemIdx; + + while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) { + groupEnd++; + } + + // Uncheck all in group, check the clicked one + for (int32_t i = groupStart; i <= groupEnd; i++) { + menu->items[i].checked = (i == itemIdx); + } + } +} + + // ============================================================ // closeAllPopups — dirty all popup levels and deactivate // ============================================================ @@ -587,6 +627,11 @@ static void dispatchEvents(AppContextT *ctx) { // Clicking a submenu item opens it (already open from hover, but ensure) openSubMenu(ctx); } else if (item->enabled && !item->separator) { + // Toggle check/radio state before closing + if (item->type == MenuItemCheckE || item->type == MenuItemRadioE) { + clickMenuCheckRadio(ctx->popup.menu, itemIdx); + } + // Close popup BEFORE calling onMenu (onMenu may run a nested event loop) int32_t menuId = item->id; WindowT *win = findWindowById(ctx, ctx->popup.windowId); @@ -738,6 +783,18 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c return; } + // Detect if menu has check/radio items (for left margin) + bool hasCheck = false; + + for (int32_t k = 0; k < menu->itemCount; k++) { + if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) { + hasCheck = true; + break; + } + } + + int32_t checkMargin = hasCheck ? MENU_CHECK_WIDTH : 0; + // Draw popup background BevelStyleT popBevel; popBevel.highlight = ctx->colors.windowHighlight; @@ -767,7 +824,30 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c } 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); + drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2 + checkMargin, itemY, item->label, fg, bg, true); + + // Draw check/radio indicator + if (item->checked) { + int32_t cy = itemY + ctx->font.charHeight / 2; + int32_t cx = px + 2 + MENU_CHECK_WIDTH / 2; + + if (item->type == MenuItemCheckE) { + // Checkmark: small tick shape + drawVLine(d, ops, cx - 2, cy - 1, 2, fg); + drawVLine(d, ops, cx - 1, cy, 2, fg); + drawVLine(d, ops, cx, cy + 1, 2, fg); + drawVLine(d, ops, cx + 1, cy, 2, fg); + drawVLine(d, ops, cx + 2, cy - 1, 2, fg); + drawVLine(d, ops, cx + 3, cy - 2, 2, fg); + } else if (item->type == MenuItemRadioE) { + // Filled circle bullet (5x5) + drawHLine(d, ops, cx - 1, cy - 2, 3, fg); + drawHLine(d, ops, cx - 2, cy - 1, 5, fg); + drawHLine(d, ops, cx - 2, cy, 5, fg); + drawHLine(d, ops, cx - 2, cy + 1, 5, fg); + drawHLine(d, ops, cx - 1, cy + 2, 3, fg); + } + } // Draw submenu arrow indicator if (item->subMenu) { @@ -971,6 +1051,24 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) { } +// ============================================================ +// dvxMaximizeWindow +// ============================================================ + +void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) { + wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win); +} + + +// ============================================================ +// dvxMinimizeWindow +// ============================================================ + +void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) { + wmMinimize(&ctx->stack, &ctx->dirty, win); +} + + // ============================================================ // dvxQuit // ============================================================ diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 9eb5ef7..664e622 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -74,6 +74,12 @@ void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int // Invalidate entire window content void dvxInvalidateWindow(AppContextT *ctx, WindowT *win); +// Minimize a window (show as icon at bottom of screen) +void dvxMinimizeWindow(AppContextT *ctx, WindowT *win); + +// Maximize a window (expand to fill screen or maxW/maxH) +void dvxMaximizeWindow(AppContextT *ctx, WindowT *win); + // Request exit from main loop void dvxQuit(AppContextT *ctx); diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 81b41e5..5e41c45 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -155,14 +155,21 @@ typedef struct { // Forward declaration for submenu pointers typedef struct MenuT MenuT; +typedef enum { + MenuItemNormalE, + MenuItemCheckE, + MenuItemRadioE, +} MenuItemTypeE; + typedef struct { - char label[MAX_MENU_LABEL]; - int32_t id; // application-defined command ID - bool separator; // true = this is a separator line, not a clickable item - bool enabled; - bool checked; - char accelKey; // lowercase accelerator character, 0 if none - MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item) + char label[MAX_MENU_LABEL]; + int32_t id; // application-defined command ID + MenuItemTypeE type; // normal, checkbox, or radio + bool separator; // true = this is a separator line, not a clickable item + bool enabled; + bool checked; + char accelKey; // lowercase accelerator character, 0 if none + MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item) } MenuItemT; struct MenuT { diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index 4921856..c053a93 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -645,6 +645,50 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) { } +// ============================================================ +// wmAddMenuCheckItem +// ============================================================ + +void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) { + if (menu->itemCount >= MAX_MENU_ITEMS) { + return; + } + + MenuItemT *item = &menu->items[menu->itemCount]; + memset(item, 0, sizeof(*item)); + strncpy(item->label, label, MAX_MENU_LABEL - 1); + item->label[MAX_MENU_LABEL - 1] = '\0'; + item->id = id; + item->type = MenuItemCheckE; + item->enabled = true; + item->checked = checked; + item->accelKey = accelParse(label); + menu->itemCount++; +} + + +// ============================================================ +// wmAddMenuRadioItem +// ============================================================ + +void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) { + if (menu->itemCount >= MAX_MENU_ITEMS) { + return; + } + + MenuItemT *item = &menu->items[menu->itemCount]; + memset(item, 0, sizeof(*item)); + strncpy(item->label, label, MAX_MENU_LABEL - 1); + item->label[MAX_MENU_LABEL - 1] = '\0'; + item->id = id; + item->type = MenuItemRadioE; + item->enabled = true; + item->checked = checked; + item->accelKey = accelParse(label); + menu->itemCount++; +} + + // ============================================================ // wmAddMenuSeparator // ============================================================ diff --git a/dvx/dvxWm.h b/dvx/dvxWm.h index 8e9065d..92c5569 100644 --- a/dvx/dvxWm.h +++ b/dvx/dvxWm.h @@ -34,6 +34,12 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label); // Add an item to a menu void wmAddMenuItem(MenuT *menu, const char *label, int32_t id); +// Add a checkbox item to a menu +void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked); + +// Add a radio item to a menu (radio group = consecutive radio items) +void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked); + // Add a separator to a menu void wmAddMenuSeparator(MenuT *menu); diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index 3e0794d..7f7c2f4 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -22,11 +22,16 @@ #define CMD_EDIT_CUT 200 #define CMD_EDIT_COPY 201 #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_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_VIEW_TOOLBAR 305 +#define CMD_VIEW_STATUSBAR 306 +#define CMD_VIEW_SIZE_SMALL 307 +#define CMD_VIEW_SIZE_MED 308 +#define CMD_VIEW_SIZE_LARGE 309 #define CMD_HELP_ABOUT 400 // ============================================================ @@ -594,6 +599,13 @@ static void setupMainWindow(AppContextT *ctx) { wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM); wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL); wmAddMenuSeparator(viewMenu); + wmAddMenuCheckItem(viewMenu, "Tool&bar", CMD_VIEW_TOOLBAR, true); + wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUSBAR, true); + wmAddMenuSeparator(viewMenu); + wmAddMenuRadioItem(viewMenu, "S&mall", CMD_VIEW_SIZE_SMALL, false); + wmAddMenuRadioItem(viewMenu, "Me&dium", CMD_VIEW_SIZE_MED, true); + wmAddMenuRadioItem(viewMenu, "&Large", CMD_VIEW_SIZE_LARGE, false); + wmAddMenuSeparator(viewMenu); MenuT *zoomMenu = wmAddSubMenu(viewMenu, "&Zoom"); @@ -727,6 +739,7 @@ static void setupTerminalWindow(AppContextT *ctx) { dvxFitWindow(ctx, win); wgtInvalidate(root); + dvxMinimizeWindow(ctx, win); }