Checkboxes and radio buttons in menus.

This commit is contained in:
Scott Duensing 2026-03-15 21:04:40 -05:00
parent 6f8aeda7b2
commit 7476367ace
6 changed files with 188 additions and 14 deletions

View file

@ -15,6 +15,7 @@
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2) #define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
#define ICON_REFRESH_INTERVAL 8 #define ICON_REFRESH_INTERVAL 8
#define KB_MOVE_STEP 8 #define KB_MOVE_STEP 8
#define MENU_CHECK_WIDTH 14
#define SUBMENU_ARROW_WIDTH 12 #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 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 closeAllPopups(AppContextT *ctx);
static void closePopupLevel(AppContextT *ctx); static void closePopupLevel(AppContextT *ctx);
static void closeSysMenu(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) { static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
int32_t maxW = 0; int32_t maxW = 0;
bool hasSub = false; bool hasSub = false;
bool hasCheck = false;
for (int32_t k = 0; k < menu->itemCount; k++) { for (int32_t k = 0; k < menu->itemCount; k++) {
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label); 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) { if (menu->items[k].subMenu) {
hasSub = true; 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; *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 // 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) // Clicking a submenu item opens it (already open from hover, but ensure)
openSubMenu(ctx); openSubMenu(ctx);
} else if (item->enabled && !item->separator) { } 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) // Close popup BEFORE calling onMenu (onMenu may run a nested event loop)
int32_t menuId = item->id; int32_t menuId = item->id;
WindowT *win = findWindowById(ctx, ctx->popup.windowId); WindowT *win = findWindowById(ctx, ctx->popup.windowId);
@ -738,6 +783,18 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
return; 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 // Draw popup background
BevelStyleT popBevel; BevelStyleT popBevel;
popBevel.highlight = ctx->colors.windowHighlight; 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); 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 // Draw submenu arrow indicator
if (item->subMenu) { 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 // dvxQuit
// ============================================================ // ============================================================

View file

@ -74,6 +74,12 @@ void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int
// Invalidate entire window content // Invalidate entire window content
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win); 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 // Request exit from main loop
void dvxQuit(AppContextT *ctx); void dvxQuit(AppContextT *ctx);

View file

@ -155,9 +155,16 @@ typedef struct {
// Forward declaration for submenu pointers // Forward declaration for submenu pointers
typedef struct MenuT MenuT; typedef struct MenuT MenuT;
typedef enum {
MenuItemNormalE,
MenuItemCheckE,
MenuItemRadioE,
} MenuItemTypeE;
typedef struct { typedef struct {
char label[MAX_MENU_LABEL]; char label[MAX_MENU_LABEL];
int32_t id; // application-defined command ID 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 separator; // true = this is a separator line, not a clickable item
bool enabled; bool enabled;
bool checked; bool checked;

View file

@ -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 // wmAddMenuSeparator
// ============================================================ // ============================================================

View file

@ -34,6 +34,12 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label);
// Add an item to a menu // Add an item to a menu
void wmAddMenuItem(MenuT *menu, const char *label, int32_t id); 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 // Add a separator to a menu
void wmAddMenuSeparator(MenuT *menu); void wmAddMenuSeparator(MenuT *menu);

View file

@ -27,6 +27,11 @@
#define CMD_VIEW_ZOOM_IN 302 #define CMD_VIEW_ZOOM_IN 302
#define CMD_VIEW_ZOOM_OUT 303 #define CMD_VIEW_ZOOM_OUT 303
#define CMD_VIEW_ZOOM_FIT 304 #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 #define CMD_HELP_ABOUT 400
// ============================================================ // ============================================================
@ -594,6 +599,13 @@ static void setupMainWindow(AppContextT *ctx) {
wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM); wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM);
wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL); wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL);
wmAddMenuSeparator(viewMenu); 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"); MenuT *zoomMenu = wmAddSubMenu(viewMenu, "&Zoom");
@ -727,6 +739,7 @@ static void setupTerminalWindow(AppContextT *ctx) {
dvxFitWindow(ctx, win); dvxFitWindow(ctx, win);
wgtInvalidate(root); wgtInvalidate(root);
dvxMinimizeWindow(ctx, win);
} }