The setup menu picks from drop-downs
Every list group is a combo box now: track, time, weather, length, vehicle, and colour/badge or team/position. Scenario stays as visible buttons because it decides what the other lists contain, so it should not be hidden behind one of them. This ends a problem I had been solving the wrong way. The menu was flat lists of everything, which was fine when the content was short enough to see at once - the quality that made it feel like the pod panel. The promoted resource file roughly doubled it, and I answered with two columns, then better margins, then a general column flow, each time keeping an idiom whose justification had already gone. Eight controls replace ninety-odd rows. At 800x600 the columns go from 131px to 323px, so nothing ellipsizes any more - the longest name wants 158. The whole menu now needs 310px of the 492 above the buttons there, and 529 of 900 at 1080p, so adding vehicles or maps cannot crowd it again. The boxes are owner-drawn - green on black, highlight inverted rather than tinted - so they read as part of the panel instead of arriving in system colours. They are rebuilt rather than moved when the scenario changes, since it swaps two of them outright and reshuffles the track list. Built and run at 640x480, 800x600, 1280x720 and 1920x1080. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+238
-129
@@ -242,6 +242,13 @@ namespace
|
||||
|
||||
HWND menuWindow;
|
||||
HWND nameEdit;
|
||||
|
||||
// Every list group is a drop-down. They are owner-drawn so they
|
||||
// keep the green-on-black panel look instead of arriving in
|
||||
// system colours, and their labels are painted by PaintMenu.
|
||||
HWND combo[GroupCount];
|
||||
RECT comboLabel[GroupCount];
|
||||
|
||||
HFONT textFont;
|
||||
HFONT titleFont;
|
||||
HBRUSH editBrush;
|
||||
@@ -251,6 +258,9 @@ namespace
|
||||
};
|
||||
|
||||
FEState *gFE = NULL;
|
||||
|
||||
// ItemName() reads this to answer with the right track list.
|
||||
const int *gItemNameSelection = NULL;
|
||||
int gLastMissionSeconds = 0;
|
||||
|
||||
// carried across races so cycling back to the menu keeps the
|
||||
@@ -613,9 +623,54 @@ namespace
|
||||
// Layout: three columns of lists + the launch button
|
||||
//---------------------------------------------------------------
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The groups that became drop-downs, in the order they are laid
|
||||
// out. Scenario is deliberately NOT one of them: it decides what
|
||||
// the others contain, so it stays visible as a pair of buttons.
|
||||
//---------------------------------------------------------------
|
||||
int ComboGroups(const int *selection, int *groups)
|
||||
{
|
||||
int n = 0;
|
||||
groups[n++] = GroupMap;
|
||||
groups[n++] = GroupTime;
|
||||
groups[n++] = GroupWeather;
|
||||
groups[n++] = GroupLength;
|
||||
groups[n++] = GroupVehicle;
|
||||
if (IsFootball(selection))
|
||||
{
|
||||
groups[n++] = GroupTeam;
|
||||
groups[n++] = GroupPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
groups[n++] = GroupColor;
|
||||
groups[n++] = GroupBadge;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// The combo id is its group, so CBN_SELCHANGE says which list moved.
|
||||
const int kComboIdBase = 100;
|
||||
|
||||
void DestroyCombos(FEState *fe)
|
||||
{
|
||||
for (int g = 0; g < GroupCount; ++g)
|
||||
{
|
||||
if (fe->combo[g] != NULL)
|
||||
{
|
||||
DestroyWindow(fe->combo[g]);
|
||||
fe->combo[g] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// defined below, next to the catalogs it reads
|
||||
const char *ItemName(int group, int index);
|
||||
|
||||
void LayoutMenu(FEState *fe, int client_w, int client_h)
|
||||
{
|
||||
fe->itemCount = 0;
|
||||
gItemNameSelection = fe->selection;
|
||||
|
||||
int row_h = client_h / 36;
|
||||
if (row_h < 18) row_h = 18;
|
||||
@@ -624,111 +679,71 @@ namespace
|
||||
int top = client_h / 7;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Fixed columns cannot hold this menu any more. The promoted
|
||||
// resource file brought eleven vehicles and seven maps, and the
|
||||
// settings column - scenario, map, time, weather, length - was
|
||||
// already within 11px of the bottom at 800x600 before any of
|
||||
// them arrived. So the groups FLOW: they fill a column, then
|
||||
// start the next, and the layout takes as many columns as the
|
||||
// content needs. Nothing here knows how many that will be, which
|
||||
// is the point - adding a map or a vehicle can no longer push
|
||||
// anything off the screen.
|
||||
// Every list is a drop-down, so the menu no longer grows with the
|
||||
// content: eight controls instead of ninety-odd rows. Two columns
|
||||
// of label-over-box, settings on the left and loadout on the right,
|
||||
// with the scenario left as visible buttons because it decides what
|
||||
// the other lists contain.
|
||||
//---------------------------------------------------------------
|
||||
int map_count;
|
||||
ActiveMaps(fe->selection, &map_count);
|
||||
|
||||
struct FlowGroup { int group; int count; };
|
||||
FlowGroup flow[8];
|
||||
int flow_count = 0;
|
||||
flow[flow_count].group = GroupScenario;
|
||||
flow[flow_count++].count = FE_COUNT(kScenarios);
|
||||
flow[flow_count].group = GroupMap;
|
||||
flow[flow_count++].count = map_count;
|
||||
flow[flow_count].group = GroupTime;
|
||||
flow[flow_count++].count = FE_COUNT(kTimes);
|
||||
flow[flow_count].group = GroupWeather;
|
||||
flow[flow_count++].count = FE_COUNT(kWeather);
|
||||
flow[flow_count].group = GroupLength;
|
||||
flow[flow_count++].count = FE_COUNT(kLengths);
|
||||
flow[flow_count].group = GroupVehicle;
|
||||
flow[flow_count++].count = FE_COUNT(kVehicles);
|
||||
if (IsFootball(fe->selection))
|
||||
{
|
||||
flow[flow_count].group = GroupTeam;
|
||||
flow[flow_count++].count = FE_COUNT(kTeams);
|
||||
flow[flow_count].group = GroupPosition;
|
||||
flow[flow_count++].count = FE_COUNT(kPositions);
|
||||
}
|
||||
else
|
||||
{
|
||||
flow[flow_count].group = GroupColor;
|
||||
flow[flow_count++].count = FE_COUNT(kColors);
|
||||
flow[flow_count].group = GroupBadge;
|
||||
flow[flow_count++].count = FE_COUNT(kBadges);
|
||||
}
|
||||
|
||||
// Every column starts two rows down so the pilot name box has the
|
||||
// same home on whichever column ends up last, and they all line
|
||||
// up. The bottom is reserved for LAUNCH and the lobby buttons.
|
||||
int content_top = top + 2 * row_h;
|
||||
int bottom = client_h - (RPL4Lobby_Configured() ? 7 : 4) * row_h;
|
||||
int rows_per_col = (bottom - content_top) / row_h;
|
||||
if (rows_per_col < 1) rows_per_col = 1;
|
||||
|
||||
// Pass one: how many columns does this content want? A group is
|
||||
// preceded by a blank row unless it lands at the top of a column,
|
||||
// and a group longer than a column simply continues into the next.
|
||||
int columns = 1;
|
||||
{
|
||||
int row = 0;
|
||||
for (int g = 0; g < flow_count; ++g)
|
||||
{
|
||||
if (row > 0) ++row;
|
||||
for (int i = 0; i < flow[g].count; ++i)
|
||||
{
|
||||
if (row >= rows_per_col) { ++columns; row = 0; }
|
||||
++row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int margin = client_w / 20;
|
||||
int gap = client_w / 50;
|
||||
int col_w = (client_w - 2 * margin - (columns - 1) * gap) / columns;
|
||||
int margin = client_w / 14;
|
||||
int gap = client_w / 20;
|
||||
int col_w = (client_w - 2 * margin - gap) / 2;
|
||||
if (col_w < 1) col_w = 1;
|
||||
int col2 = margin + col_w + gap;
|
||||
|
||||
// Pass two: the same walk, placing items this time.
|
||||
int last_col = 0;
|
||||
int block_h = row_h * 5 / 2; // label, box, breathing room
|
||||
|
||||
// scenario: buttons, top of the left column
|
||||
int y = top;
|
||||
fe->comboLabel[GroupScenario].left = margin;
|
||||
fe->comboLabel[GroupScenario].right = margin + col_w;
|
||||
fe->comboLabel[GroupScenario].top = y;
|
||||
fe->comboLabel[GroupScenario].bottom = y + row_h;
|
||||
y += row_h;
|
||||
for (int i = 0; i < FE_COUNT(kScenarios); ++i)
|
||||
{
|
||||
int column = 0, row = 0;
|
||||
for (int g = 0; g < flow_count; ++g)
|
||||
{
|
||||
if (row > 0) ++row;
|
||||
for (int i = 0; i < flow[g].count; ++i)
|
||||
{
|
||||
if (row >= rows_per_col) { ++column; row = 0; }
|
||||
FEItem *item = &fe->items[fe->itemCount++];
|
||||
item->group = flow[g].group;
|
||||
item->index = i;
|
||||
item->rect.left = margin + column * (col_w + gap);
|
||||
item->rect.top = content_top + row * row_h;
|
||||
item->rect.right = item->rect.left + col_w;
|
||||
item->rect.bottom = item->rect.top + row_h;
|
||||
++row;
|
||||
}
|
||||
last_col = column;
|
||||
}
|
||||
FEItem *item = &fe->items[fe->itemCount++];
|
||||
item->group = GroupScenario;
|
||||
item->index = i;
|
||||
item->rect.left = margin + i * (col_w / 2);
|
||||
item->rect.top = y;
|
||||
item->rect.right = item->rect.left + col_w / 2 - row_h / 3;
|
||||
item->rect.bottom = y + row_h;
|
||||
}
|
||||
y += row_h + row_h / 2;
|
||||
|
||||
int groups[GroupCount];
|
||||
int group_count = ComboGroups(fe->selection, groups);
|
||||
|
||||
// left column takes the mission settings, right the loadout; the
|
||||
// split is where the vehicle list starts.
|
||||
int left_y = y;
|
||||
int right_y = top;
|
||||
for (int g = 0; g < group_count; ++g)
|
||||
{
|
||||
int group = groups[g];
|
||||
Logical left = (group == GroupMap || group == GroupTime ||
|
||||
group == GroupWeather || group == GroupLength);
|
||||
int x = left ? margin : col2;
|
||||
int *slot = left ? &left_y : &right_y;
|
||||
|
||||
fe->comboLabel[group].left = x;
|
||||
fe->comboLabel[group].right = x + col_w;
|
||||
fe->comboLabel[group].top = *slot;
|
||||
fe->comboLabel[group].bottom = *slot + row_h;
|
||||
*slot += block_h;
|
||||
}
|
||||
|
||||
int loadout_col = margin + last_col * (col_w + gap);
|
||||
// pilot name goes under the loadout column
|
||||
int name_y = right_y;
|
||||
|
||||
// launch button
|
||||
FEItem *launch = &fe->items[fe->itemCount++];
|
||||
launch->group = GroupLaunch;
|
||||
launch->index = 0;
|
||||
launch->rect.left = loadout_col;
|
||||
launch->rect.left = col2;
|
||||
launch->rect.top = client_h - 3 * row_h;
|
||||
launch->rect.right = loadout_col + col_w;
|
||||
launch->rect.right = col2 + col_w;
|
||||
launch->rect.bottom = client_h - row_h;
|
||||
|
||||
// Steam lobby buttons, offered whenever environ.ini asked for Steam.
|
||||
@@ -738,17 +753,17 @@ namespace
|
||||
FEItem *host = &fe->items[fe->itemCount++];
|
||||
host->group = GroupSteamHost;
|
||||
host->index = 0;
|
||||
host->rect.left = loadout_col;
|
||||
host->rect.left = col2;
|
||||
host->rect.top = client_h - 6 * row_h;
|
||||
host->rect.right = loadout_col + col_w;
|
||||
host->rect.right = col2 + col_w;
|
||||
host->rect.bottom = client_h - 5 * row_h;
|
||||
|
||||
FEItem *join = &fe->items[fe->itemCount++];
|
||||
join->group = GroupSteamJoin;
|
||||
join->index = 0;
|
||||
join->rect.left = loadout_col;
|
||||
join->rect.left = col2;
|
||||
join->rect.top = client_h - (9 * row_h) / 2;
|
||||
join->rect.right = loadout_col + col_w;
|
||||
join->rect.right = col2 + col_w;
|
||||
join->rect.bottom = client_h - (7 * row_h) / 2;
|
||||
}
|
||||
|
||||
@@ -769,11 +784,54 @@ namespace
|
||||
quit->rect.right = margin + col_w / 2;
|
||||
quit->rect.bottom = client_h - row_h;
|
||||
|
||||
// pilot name edit sits at the top of column 3
|
||||
// pilot name, under the loadout column
|
||||
fe->comboLabel[GroupLaunch].left = col2; // reused: name label
|
||||
fe->comboLabel[GroupLaunch].right = col2 + col_w;
|
||||
fe->comboLabel[GroupLaunch].top = name_y;
|
||||
fe->comboLabel[GroupLaunch].bottom = name_y + row_h;
|
||||
if (fe->nameEdit != NULL)
|
||||
{
|
||||
MoveWindow(fe->nameEdit,
|
||||
loadout_col, top - row_h / 4, col_w, row_h, TRUE);
|
||||
MoveWindow(fe->nameEdit, col2, name_y + row_h, col_w, row_h, TRUE);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The drop-downs themselves. Rebuilt rather than moved, because
|
||||
// the scenario swaps two of them outright (team/position for
|
||||
// colour/badge) and reshuffles the track list.
|
||||
//---------------------------------------------------------------
|
||||
DestroyCombos(fe);
|
||||
for (int g = 0; g < group_count; ++g)
|
||||
{
|
||||
int group = groups[g];
|
||||
const RECT &label = fe->comboLabel[group];
|
||||
int count = GroupSize(group, fe->selection);
|
||||
|
||||
// The height given at creation is the DROPPED height - what the
|
||||
// closed box shows is the item height - so ask for enough to
|
||||
// show a dozen rows without a scrollbar where the list is short.
|
||||
int drop_rows = (count < 12) ? count : 12;
|
||||
HWND box = CreateWindowExA(
|
||||
0, "COMBOBOX", "",
|
||||
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
|
||||
CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS,
|
||||
label.left, label.top + row_h,
|
||||
label.right - label.left, row_h + drop_rows * row_h,
|
||||
fe->menuWindow, (HMENU)(INT_PTR)(kComboIdBase + group),
|
||||
(HINSTANCE) GetWindowLongPtr(fe->menuWindow, GWLP_HINSTANCE), NULL);
|
||||
if (box == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SendMessageA(box, WM_SETFONT, (WPARAM) fe->textFont, TRUE);
|
||||
SendMessageA(box, CB_SETITEMHEIGHT, (WPARAM) -1, row_h);
|
||||
SendMessageA(box, CB_SETITEMHEIGHT, 0, row_h);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
SendMessageA(box, CB_ADDSTRING, 0, (LPARAM) ItemName(group, i));
|
||||
}
|
||||
if (fe->selection[group] >= count) fe->selection[group] = 0;
|
||||
SendMessageA(box, CB_SETCURSEL, fe->selection[group], 0);
|
||||
fe->combo[group] = box;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,7 +854,6 @@ namespace
|
||||
}
|
||||
|
||||
// the map rows need the active scenario's list
|
||||
const int *gItemNameSelection = NULL;
|
||||
|
||||
const char *ItemName(int group, int index)
|
||||
{
|
||||
@@ -858,21 +915,31 @@ namespace
|
||||
|
||||
SelectObject(mem, fe->textFont);
|
||||
|
||||
// group headers (drawn above each group's first item)
|
||||
int previous_group = -1;
|
||||
// Labels: one above each drop-down, one above the scenario
|
||||
// buttons, one above the pilot name box. The drop-downs draw
|
||||
// themselves (WM_DRAWITEM), so all that is left here is their
|
||||
// captions.
|
||||
SetTextColor(mem, kGreenBright);
|
||||
for (int g = 0; g < GroupCount; ++g)
|
||||
{
|
||||
Logical labelled = (g == GroupScenario) || (fe->combo[g] != NULL);
|
||||
if (!labelled && g != GroupLaunch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
RECT label = fe->comboLabel[g];
|
||||
if (label.right <= label.left)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const char *caption = (g == GroupLaunch) ? "PILOT NAME" : GroupTitle(g);
|
||||
DrawTextA(mem, caption, -1, &label,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
for (int i = 0; i < fe->itemCount; ++i)
|
||||
{
|
||||
const FEItem *item = &fe->items[i];
|
||||
if (item->group != previous_group && item->group < GroupLaunch)
|
||||
{
|
||||
previous_group = item->group;
|
||||
RECT header = item->rect;
|
||||
header.top -= (item->rect.bottom - item->rect.top);
|
||||
header.bottom = item->rect.top;
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, GroupTitle(item->group), -1, &header,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
Logical selected =
|
||||
(item->group >= GroupLaunch) ||
|
||||
@@ -937,22 +1004,7 @@ namespace
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
}
|
||||
|
||||
// pilot-name header
|
||||
if (fe->nameEdit != NULL)
|
||||
{
|
||||
RECT edit_rect;
|
||||
GetWindowRect(fe->nameEdit, &edit_rect);
|
||||
POINT corner = { edit_rect.left, edit_rect.top };
|
||||
ScreenToClient(fe->menuWindow, &corner);
|
||||
RECT header;
|
||||
header.left = corner.x;
|
||||
header.right = corner.x + 300;
|
||||
header.bottom = corner.y;
|
||||
header.top = corner.y - 26;
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, "PILOT NAME", -1, &header,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
// (the pilot-name caption is drawn with the other labels above)
|
||||
|
||||
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
||||
|
||||
@@ -1039,6 +1091,63 @@ namespace
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_COMMAND:
|
||||
if (fe != NULL && HIWORD(wParam) == CBN_SELCHANGE)
|
||||
{
|
||||
int group = LOWORD(wParam) - kComboIdBase;
|
||||
if (group >= 0 && group < GroupCount && fe->combo[group] != NULL)
|
||||
{
|
||||
int pick = (int) SendMessageA(fe->combo[group], CB_GETCURSEL, 0, 0);
|
||||
if (pick >= 0)
|
||||
{
|
||||
fe->selection[group] = pick;
|
||||
}
|
||||
InvalidateRect(fe->menuWindow, NULL, FALSE);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
//
|
||||
// The drop-downs are owner-drawn so they read as part of the
|
||||
// panel rather than as system widgets: green on black, and the
|
||||
// highlight inverted rather than tinted.
|
||||
//
|
||||
case WM_DRAWITEM:
|
||||
if (fe != NULL)
|
||||
{
|
||||
DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT *) lParam;
|
||||
if (di->CtlType == ODT_COMBOBOX && (int) di->itemID >= 0)
|
||||
{
|
||||
Logical hot =
|
||||
((di->itemState & (ODS_SELECTED | ODS_COMBOBOXEDIT)) == ODS_SELECTED);
|
||||
HBRUSH back = CreateSolidBrush(hot ? kGreenDim : kBlack);
|
||||
FillRect(di->hDC, &di->rcItem, back);
|
||||
DeleteObject(back);
|
||||
|
||||
char text[128];
|
||||
text[0] = '\0';
|
||||
SendMessageA(di->hwndItem, CB_GETLBTEXT, di->itemID, (LPARAM) text);
|
||||
RECT label = di->rcItem;
|
||||
label.left += 6;
|
||||
SetBkMode(di->hDC, TRANSPARENT);
|
||||
SetTextColor(di->hDC, hot ? kBlack : kGreenBright);
|
||||
DrawTextA(di->hDC, text, -1, &label,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CTLCOLORLISTBOX:
|
||||
if (fe != NULL)
|
||||
{
|
||||
SetTextColor((HDC) wParam, kGreenBright);
|
||||
SetBkColor((HDC) wParam, kBlack);
|
||||
return (LRESULT) fe->editBrush;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CTLCOLOREDIT:
|
||||
if (fe != NULL)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user