Files
RP412/MUNGA_L4/L4PLASMASCREEN.cpp
T
CydandClaude Opus 5 dce8818273 The plasma glass is placeable too
It is a draggable top-level window whenever L4PLASMA=SCREEN, which makes
it the last one still being placed fresh every launch. It joins
mfd_layout.cfg under "Plasma Display", the caption it already carries -
the same key-is-the-title rule the display panes follow.

Position only, like the panes: the glass is 128x32 at L4PLASMASCALE, so
its size is a setting rather than something to drag.

It registers and loads at the point it creates its window rather than
leaving that to SVGA16. The glass comes from the gauge renderer and the
panes from the video mode, and nothing guarantees which is built first;
loading in both places means whichever runs second simply re-applies a
placement the first already has. Its window procedure picks up the same
WM_EXITSIZEMOVE save the panes have, so a drag writes the file straight
away, and the destructor forgets the window before destroying it.

Verified by round trip in the exploded view: dragged to 640,880, the
file took "Plasma Display=640,880,528,167" alongside the panes and the
game window, and a fresh launch in load mode put it physically back at
640,880. The three load lines in the log - one window, then two, then
eight - are the ordering doing its job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 08:05:50 -05:00

348 lines
8.6 KiB
C++

#include "mungal4.h"
#pragma hdrstop
#include "l4plasmascreen.h"
#include "l4app.h"
#include "l4mfdview.h" // RPWindowLayout_*
namespace
{
const char plasmaWindowClass[] = "RPPlasmaScreen";
LRESULT CALLBACK
PlasmaWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PlasmaScreen *screen =
(PlasmaScreen *) GetWindowLongPtrA(hwnd, GWLP_USERDATA);
switch (message)
{
case WM_PAINT:
if (screen != NULL)
{
// handled below through the friend-free public surface:
// the window procedure only asks the screen to repaint.
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// PlasmaScreen stores what StretchDIBits needs in
// window properties to keep the class decoupled.
const BITMAPINFO *bmi =
(const BITMAPINFO *) GetPropA(hwnd, "PlasmaBitmapInfo");
const void *pixels = GetPropA(hwnd, "PlasmaPixels");
RECT client;
GetClientRect(hwnd, &client);
if (bmi != NULL && pixels != NULL)
{
SetStretchBltMode(hdc, COLORONCOLOR);
StretchDIBits(
hdc,
0, 0, client.right, client.bottom,
0, 0, PlasmaScreen::plasmaWidth, PlasmaScreen::plasmaHeight,
pixels, bmi, DIB_RGB_COLORS, SRCCOPY
);
}
EndPaint(hwnd, &ps);
return 0;
}
break;
case WM_EXITSIZEMOVE:
//
// Dragged to a new spot. Write the whole arrangement now rather
// than trusting teardown, so a hard kill still leaves what was
// on screen. No-op unless RP412MFDLAYOUT is save.
//
RPWindowLayout_Save();
return 0;
case WM_CLOSE:
// The glass is part of the cockpit; just hide it.
ShowWindow(hwnd, SW_HIDE);
return 0;
case WM_ERASEBKGND:
return 1;
}
return DefWindowProcA(hwnd, message, wParam, lParam);
}
}
//########################################################################
//############################ PlasmaScreen ##############################
//########################################################################
PlasmaScreen *PlasmaScreen::activeInstance = NULL;
void
PlasmaScreen::Position(void *parent, int x, int y, int client_width, int client_height)
{
if (activeInstance == NULL || activeInstance->window == NULL)
{
return;
}
HWND glass = (HWND) activeInstance->window;
int window_w = client_width;
int window_h = client_height;
if (parent != NULL)
{
// fold the glass into the cockpit window as a chrome-less pane
SetWindowLongPtrA(glass, GWL_STYLE, WS_CHILD | WS_VISIBLE);
SetParent(glass, (HWND) parent);
}
else
{
DWORD 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;
}
SetWindowPos(
glass, NULL,
x, y,
window_w,
window_h,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW
);
}
void
PlasmaScreen::Hide()
{
if (activeInstance != NULL && activeInstance->window != NULL)
{
ShowWindow((HWND) activeInstance->window, SW_HIDE);
}
}
PlasmaScreen::PlasmaScreen():
Video8BitBuffered(plasmaWidth, plasmaHeight)
{
Check_Pointer(this);
window = NULL;
bitmapInfo = NULL;
scale = 4;
const char *scale_string = getenv("L4PLASMASCALE");
if (scale_string != NULL)
{
int requested = atoi(scale_string);
if (requested >= 1 && requested <= 16)
{
scale = requested;
}
}
//---------------------------------------------------------------
// Build the 8bpp DIB header with the plasma-orange palette:
// index 0 is the dark glass, everything else lights up.
//---------------------------------------------------------------
BITMAPINFOHEADER *header = (BITMAPINFOHEADER *) new char[
sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)];
memset(header, 0, sizeof(BITMAPINFOHEADER));
header->biSize = sizeof(BITMAPINFOHEADER);
header->biWidth = plasmaWidth;
header->biHeight = -plasmaHeight; // top-down, matching PixelMap8
header->biPlanes = 1;
header->biBitCount = 8;
header->biCompression = BI_RGB;
header->biClrUsed = 256;
RGBQUAD *palette = (RGBQUAD *)((char *) header + sizeof(BITMAPINFOHEADER));
for (int i = 0; i < 256; ++i)
{
if (i == 0)
{
palette[i].rgbRed = 24; palette[i].rgbGreen = 10; palette[i].rgbBlue = 4;
}
else
{
palette[i].rgbRed = 255; palette[i].rgbGreen = 144; palette[i].rgbBlue = 32;
}
palette[i].rgbReserved = 0;
}
bitmapInfo = header;
CreateGlassWindow();
Tell("PlasmaScreen: on-screen plasma display, scale x" << scale << "\n");
}
PlasmaScreen::~PlasmaScreen()
{
Check_Pointer(this);
if (activeInstance == this)
{
activeInstance = NULL;
}
if (window != NULL)
{
RPWindowLayout_Forget(window);
RemovePropA((HWND) window, "PlasmaBitmapInfo");
RemovePropA((HWND) window, "PlasmaPixels");
DestroyWindow((HWND) window);
window = NULL;
}
delete [] (char *) bitmapInfo;
bitmapInfo = NULL;
}
Logical
PlasmaScreen::TestInstance() const
{
return valid;
}
void
PlasmaScreen::CreateGlassWindow()
{
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 = PlasmaWndProc;
window_class.hInstance = instance;
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
window_class.lpszClassName = plasmaWindowClass;
RegisterClassA(&window_class);
}
RECT bounds;
bounds.left = 0;
bounds.top = 0;
bounds.right = plasmaWidth * scale;
bounds.bottom = plasmaHeight * scale;
AdjustWindowRect(&bounds, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE);
int window_w = bounds.right - bounds.left;
int window_h = bounds.bottom - bounds.top;
//---------------------------------------------------------------
// Default position: directly below the main view (the pod mounts
// the glass adjacent to the displays). The main window sits at
// (0,0) with the -res size; clamp to the work area in case the
// main view fills the screen. L4PLASMAPOS=x,y overrides.
//---------------------------------------------------------------
int x = 0;
int y = (int) L4Application::GetScreenHeight();
const char *pos_string = getenv("L4PLASMAPOS");
if (pos_string != NULL)
{
int px = 0, py = 0;
if (sscanf(pos_string, "%d,%d", &px, &py) == 2)
{
x = px;
y = py;
}
}
else
{
RECT work;
if (SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0))
{
if (y + window_h > work.bottom)
{
y = work.bottom - window_h;
}
if (y < 0)
{
y = 0;
}
}
}
window = CreateWindowExA(
0,
plasmaWindowClass,
"Plasma Display",
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
x, y,
window_w,
window_h,
NULL, NULL, instance, NULL
);
if (window != NULL)
{
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
SetPropA((HWND) window, "PlasmaBitmapInfo", bitmapInfo);
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
activeInstance = this;
//
//-----------------------------------------------------------
// Join the saved layout. Position only, like the display
// panes: the glass is 128x32 at L4PLASMASCALE, so its size
// is a setting rather than something to drag.
//
// Loading here rather than leaving it to SVGA16 keeps this
// independent of which is built first - the glass comes from
// the gauge renderer, the panes from the video mode, and
// whichever runs second re-applies a placement the first
// already has.
//-----------------------------------------------------------
//
RPWindowLayout_Register(window, "Plasma Display", False);
RPWindowLayout_Load();
}
else
{
DEBUG_STREAM << "PlasmaScreen: window creation failed\n" << std::flush;
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Called from the gauge renderer's background task, same slot the serial
// plasma used for streaming. Repaint when any line changed.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Logical
PlasmaScreen::Update(Logical forceall)
{
Check(this);
Logical dirty = forceall;
for (int y = 0; y < plasmaHeight; ++y)
{
if (changedLine[y])
{
changedLine[y] = 0;
dirty = True;
}
}
if (dirty && window != NULL)
{
// paint now - queued WM_PAINTs starve behind the game loop's
// one-message-per-frame pump
RedrawWindow((HWND) window, NULL, NULL,
RDW_INVALIDATE | RDW_UPDATENOW);
}
return False; // 'done' - nothing left to stream
}
void
PlasmaScreen::WaitForUpdate()
{
Check(this);
}