Files
RP412/MUNGA_L4/L4MFDVIEW.cpp
T
CydandClaude Fable 5 0bec0f9640 Glass cockpit: MFD buttons reach under the display
The red banks were thin strips sitting outside the glass, so the click
target was only as tall as the strip. They are now 100px buttons that
extend BEHIND the MFD picture with just a 10px indicator clearing the
edge: the lamp still reads as a slim strip along the bezel, but the
region of the display above or below it is what you press. Paint order
flipped to match - buttons first, glass over them.

The banks are clamped so they can never meet in the middle on a
scaled-down cockpit, and the panes lost 40px of height each (260 vs
300 for a 240-tall glass), which hands that space back to the
viewscreen.

Verified live: clicking 50px inside the picture presses the button
behind it and the press shows in the indicator strip (770 pixels
changed, all of them within the strip).

Amber map columns are untouched for now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:25:23 -05:00

477 lines
12 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4mfdview.h"
#include "l4padrio.h"
namespace
{
const char mfdViewClass[] = "RPMFDView";
const int buttonGap = 4;
//---------------------------------------------------------------
// MFD button banks run UNDER the glass: the button is this tall
// in total, but only the indicator strip shows past the display
// edge - the rest sits behind the MFD picture, which is what the
// player actually clicks.
//---------------------------------------------------------------
const int mfdButtonHeight = 100;
const int mfdIndicatorStrip = 10;
//---------------------------------------------------------------
// Button fill colors by lamp brightness (0 off / 1-2 dim / 3
// bright), red family for MFD strips, amber for the side columns.
//---------------------------------------------------------------
COLORREF ButtonFill(int amber, int level)
{
if (amber)
{
if (level >= 3) return RGB(255, 210, 48);
if (level >= 1) return RGB(150, 124, 26);
return RGB(64, 52, 10);
}
if (level >= 3) return RGB(255, 72, 44);
if (level >= 1) return RGB(150, 44, 28);
return RGB(64, 18, 12);
}
//---------------------------------------------------------------
// Lamp byte -> current brightness level, animating flash modes.
// Low 2 bits: solid/slow/med/fast; bits 2-3: state 1 brightness;
// bits 4-5: state 2 brightness (the flash alternate).
//---------------------------------------------------------------
int LampLevel(int lamp_state)
{
int mode = lamp_state & 0x03;
int level1 = (lamp_state >> 2) & 0x03;
int level2 = (lamp_state >> 4) & 0x03;
if (mode == 0)
{
return level1;
}
static const int half_period[4] = { 0, 500, 250, 125 };
return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
}
LRESULT CALLBACK
MFDViewWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
MFDSplitView *view =
(MFDSplitView *) GetWindowLongPtrA(hwnd, GWLP_USERDATA);
switch (message)
{
case WM_PAINT:
if (view != NULL)
{
view->Paint();
return 0;
}
// no instance (e.g. the plain viewscreen child): default paint
break;
case WM_LBUTTONDOWN:
if (view != NULL)
{
view->MouseDown((int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam));
return 0;
}
break;
case WM_LBUTTONUP:
case WM_CAPTURECHANGED:
if (view != NULL)
{
view->MouseUp();
return 0;
}
break;
case WM_CLOSE:
// Part of the cockpit; just hide it.
ShowWindow(hwnd, SW_HIDE);
return 0;
case WM_ERASEBKGND:
if (view != NULL)
{
return 1;
}
break; // viewscreen child: erase with the class black brush
}
return DefWindowProcA(hwnd, message, wParam, lParam);
}
}
//########################################################################
//############################ MFDSplitView ##############################
//########################################################################
MFDSplitView::MFDSplitView(
const char *title,
int source_width,
int source_height,
int display_width,
int display_height,
int x,
int y,
ButtonStyle button_style,
int anchorA,
int anchorB,
void *parent
)
{
Check_Pointer(this);
sourceWidth = source_width;
sourceHeight = source_height;
buttonCount = 0;
pressedIndex = -1;
pixels = new unsigned long[source_width * source_height];
memset(pixels, 0, source_width * source_height * sizeof(unsigned long));
BITMAPINFOHEADER *header = new BITMAPINFOHEADER;
memset(header, 0, sizeof(BITMAPINFOHEADER));
header->biSize = sizeof(BITMAPINFOHEADER);
header->biWidth = source_width;
header->biHeight = -source_height; // top-down
header->biPlanes = 1;
header->biBitCount = 32;
header->biCompression = BI_RGB;
blitHeader = header;
int client_width = display_width;
int client_height = display_height;
LayoutButtons(button_style, anchorA, anchorB,
display_width, display_height, &client_width, &client_height);
clientWidth = client_width;
clientHeight = client_height;
HINSTANCE instance = GetModuleHandleA(NULL);
static Logical class_registered = False;
if (!class_registered)
{
class_registered = True;
WNDCLASSA window_class;
memset(&window_class, 0, sizeof(window_class));
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.lpfnWndProc = MFDViewWndProc;
window_class.hInstance = instance;
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
window_class.lpszClassName = mfdViewClass;
RegisterClassA(&window_class);
}
DWORD style;
int window_w, window_h;
if (parent != NULL)
{
// chrome-less pane inside the cockpit window; clips siblings so
// overlapping panes and the viewscreen never paint over it
style = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS;
window_w = client_width;
window_h = client_height;
}
else
{
style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
RECT bounds;
bounds.left = 0;
bounds.top = 0;
bounds.right = client_width;
bounds.bottom = client_height;
AdjustWindowRect(&bounds, style, FALSE);
window_w = bounds.right - bounds.left;
window_h = bounds.bottom - bounds.top;
}
window = CreateWindowExA(
0,
mfdViewClass,
title,
style,
x, y,
window_w,
window_h,
(HWND) parent, NULL, instance, NULL
);
if (window != NULL)
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
}
else
{
DEBUG_STREAM << "MFDSplitView: window creation failed (" << title << ")\n" << std::flush;
}
}
MFDSplitView::~MFDSplitView()
{
Check_Pointer(this);
if (window != NULL)
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, 0);
DestroyWindow((HWND) window);
window = NULL;
}
delete (BITMAPINFOHEADER *) blitHeader;
blitHeader = NULL;
delete [] pixels;
pixels = NULL;
}
Logical
MFDSplitView::TestInstance() const
{
return pixels != NULL;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Compute the display rectangle and the button rectangles, growing the
// client area to make room for the strips.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
MFDSplitView::LayoutButtons(
ButtonStyle button_style,
int anchorA,
int anchorB,
int display_width,
int display_height,
int *client_width,
int *client_height
)
{
displayX = 0;
displayY = 0;
displayW = display_width;
displayH = display_height;
if (button_style == MFDStrips)
{
//-----------------------------------------------------------
// 4 buttons above the glass, 4 below, flush to the pane
// edges (no border against the viewscreen behind); RIO
// addresses descend from the anchor, row-major (vRIO
// CockpitLayout::Mfd).
//
// The banks reach BEHIND the display: each button is
// mfdButtonHeight tall but only mfdIndicatorStrip of it
// clears the glass edge, so the lamp reads as a thin strip
// and the picture above/below it is the click target. Paint
// draws the buttons before the MFD image for that reason.
//-----------------------------------------------------------
int strip_h = mfdIndicatorStrip;
int button_h = mfdButtonHeight;
// on a scaled-down cockpit, keep the two banks from meeting
int limit = (display_height + 2 * strip_h) / 2;
if (button_h > limit) button_h = limit;
if (button_h < strip_h) button_h = strip_h;
int button_w = (display_width - 3 * buttonGap) / 4;
displayY = strip_h;
*client_height = display_height + 2 * strip_h;
for (int i = 0; i < 8; ++i)
{
int row = i / 4; // 0 = top bank, 1 = bottom bank
int col = i % 4;
ScreenButton *button = &buttons[buttonCount++];
button->unit = anchorA - i;
button->amber = 0;
button->x = col * (button_w + buttonGap);
button->y = row
? (displayY + displayH + strip_h - button_h)
: 0;
button->w = (col == 3)
? (display_width - 3 * (button_w + buttonGap))
: button_w;
button->h = button_h;
}
}
else if (button_style == SideColumns)
{
//-----------------------------------------------------------
// 6 buttons down each side of the glass, stacked contiguous
// and flush to the pane edges, like the pod's strips (the
// pod wires only 6 of each column's 8 addresses to buttons;
// the rest are Tesla relays). Left = anchorA ascending,
// right = anchorB.
//-----------------------------------------------------------
int col_w = display_width / 8;
if (col_w < 20) col_w = 20;
if (col_w > 40) col_w = 40;
int button_h = display_height / 6;
displayX = col_w;
*client_width = display_width + 2 * col_w;
for (int i = 0; i < 6; ++i)
{
int h = (i == 5) ? (display_height - 5 * button_h) : button_h;
ScreenButton *left = &buttons[buttonCount++];
left->unit = anchorA + i;
left->amber = 1;
left->x = 0;
left->y = i * button_h;
left->w = col_w;
left->h = h;
ScreenButton *right = &buttons[buttonCount++];
right->unit = anchorB + i;
right->amber = 1;
right->x = displayX + displayW;
right->y = left->y;
right->w = col_w;
right->h = h;
}
}
}
void
MFDSplitView::Paint()
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint((HWND) window, &ps);
RECT client;
GetClientRect((HWND) window, &client);
//---------------------------------------------------------------
// Double-buffered: compose the whole pane off screen and blit it
// in one operation, otherwise the clear-then-draw sequence
// flickers at the repaint cadence.
//---------------------------------------------------------------
HDC mem = CreateCompatibleDC(hdc);
HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom);
HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface);
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
//---------------------------------------------------------------
// Buttons first, glass second: the MFD banks extend under the
// display so the picture is the click target, and the image
// painted over them leaves only their indicator strips showing.
//---------------------------------------------------------------
for (int i = 0; i < buttonCount; ++i)
{
const ScreenButton *button = &buttons[i];
int level = PadRIO::IsActive()
? LampLevel(PadRIO::GetLampState(button->unit))
: 0;
RECT rect;
rect.left = button->x;
rect.top = button->y;
rect.right = button->x + button->w;
rect.bottom = button->y + button->h;
HBRUSH fill = CreateSolidBrush(ButtonFill(button->amber, level));
FillRect(mem, &rect, fill);
DeleteObject(fill);
HBRUSH frame = CreateSolidBrush(
(i == pressedIndex) ? RGB(255, 255, 255) : RGB(48, 48, 48));
FrameRect(mem, &rect, frame);
DeleteObject(frame);
}
// HALFTONE area-averages the downscale (the compact glass shows the
// full 640x480 canvas at ~half size); COLORONCOLOR dropped every other
// row/column, which shredded the 1-bit vector strokes and small text.
// HALFTONE requires the brush origin be set (MSDN).
SetStretchBltMode(mem, HALFTONE);
SetBrushOrgEx(mem, 0, 0, NULL);
StretchDIBits(
mem,
displayX, displayY, displayW, displayH,
0, 0, sourceWidth, sourceHeight,
pixels, (const BITMAPINFO *) blitHeader,
DIB_RGB_COLORS, SRCCOPY
);
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
SelectObject(mem, old_surface);
DeleteObject(surface);
DeleteDC(mem);
EndPaint((HWND) window, &ps);
}
void
MFDSplitView::MouseDown(int x, int y)
{
for (int i = 0; i < buttonCount; ++i)
{
const ScreenButton *button = &buttons[i];
if (x >= button->x && x < button->x + button->w &&
y >= button->y && y < button->y + button->h)
{
pressedIndex = i;
SetCapture((HWND) window);
PadRIO::SetScreenButton(button->unit, True);
Repaint();
return;
}
}
}
void
MFDSplitView::MouseUp()
{
if (pressedIndex >= 0)
{
PadRIO::SetScreenButton(buttons[pressedIndex].unit, False);
pressedIndex = -1;
if (GetCapture() == (HWND) window)
{
ReleaseCapture();
}
Repaint();
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Paint NOW rather than queueing an invalidation: the game loop pumps
// one message per frame, and queued WM_PAINTs (lowest priority) starve
// behind it - panes would freeze on their first frame.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void
MFDSplitView::SetPosition(int x, int y)
{
Check_Pointer(this);
if (window != NULL)
{
SetWindowPos((HWND) window, NULL, x, y, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
void
MFDSplitView::Repaint()
{
Check_Pointer(this);
if (window != NULL)
{
RedrawWindow((HWND) window, NULL, NULL,
RDW_INVALIDATE | RDW_UPDATENOW);
}
}