Glass windows remember where you drag them (BT_GLASS_LAYOUT)

The BT_GLASS_PANELS windows self-place in a pod-faithful ring and re-snap
once the main window is up.  They carry WS_CAPTION so a dev can drag them,
but the drag never survived the menu->mission->menu relaunch loop.

BT_GLASS_LAYOUT (L4GLASSWIN.cpp) adds opt-in persistence to a cwd-relative
glass_layout.cfg beside bindings.txt (gitignored):
  off/0/unset  computed ring only, no file I/O (default, pod-faithful)
  load         restore saved positions on startup (per-window fallback
               to computed); never writes
  save/adjust  restore first, then rewrite on each finished drag
               (WM_EXITSIZEMOVE) and on teardown -- the round trip

One "<title>=x,y,w,h" line per window; position restored, size ignored
(frame size is deterministic from content, so an old w,h can't distort a
later geometry change).  A restored window is flagged so ComputeLayout's
post-main-window re-snap leaves hand-placed windows alone.  Native
analogue of TeslaRel410 pod-launch's per-rig --bridge-pos/--layout args.

Verified [T2 runtime round-trip]: load restored 7 from a seeded cfg;
save wrote all 7 with a window moved to 777,333 (GetWindowRect read the
live window); relaunch in load put that window physically at 777,333
after the re-snap fired.  Release links clean (only the 40 tolerated
/FORCE externals).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-28 20:27:26 -05:00
co-authored by Claude Opus 4.8
parent 8284b8d9a7
commit 29c502df83
4 changed files with 219 additions and 0 deletions
+160
View File
@@ -113,6 +113,7 @@ struct GWin
int clientW, clientH;
int frameW, frameH;
int wantX, wantY;
int restored; // wantX/wantY came from glass_layout.cfg -> don't re-snap
};
static GWin gWins[8];
@@ -331,6 +332,8 @@ static void
for (int i = 0; i < gWinCount; ++i)
{
GWin &w = gWins[i];
if (w.restored)
continue; // sticky (BT_GLASS_LAYOUT) -- keep the saved spot
int fw = w.frameW, fh = w.frameH;
int x = m.left, y = m.top;
switch (i)
@@ -362,6 +365,152 @@ static void
0, 0, SWP_NOSIZE | SWP_NOACTIVATE);
}
//###########################################################################
// Sticky layout -- BT_GLASS_LAYOUT persists each window's on-screen position
// to glass_layout.cfg (cwd-relative, beside bindings.txt), so a developer can
// drag the ring into place once and have it survive the menu->mission->menu
// relaunch loop and future launches. Modes:
// off / 0 / unset -- computed pod-faithful ring only, no file I/O (default)
// load / restore -- restore saved positions (per-window fallback to
// computed); never write
// save / adjust / -- restore first, then re-save on finished-drag
// on / 1 (WM_EXITSIZEMOVE) and on teardown [the round trip]
// Position is restored; size stays deterministic from the window's content, so
// a later geometry change can't distort an old save. Windows carry WS_CAPTION
// in every mode, so they are always draggable -- the gate only controls whether
// a drag STICKS.
//###########################################################################
enum { LayoutOff = 0, LayoutLoad = 1, LayoutSave = 2 };
static const char *layoutFileName = "glass_layout.cfg";
static int
GlassLayoutMode()
{
static int m = -1;
if (m < 0)
{
const char *e = getenv("BT_GLASS_LAYOUT");
if (e == NULL || e[0] == '\0')
m = LayoutOff;
else if (_stricmp(e, "off") == 0 || _stricmp(e, "0") == 0)
m = LayoutOff;
else if (_stricmp(e, "load") == 0 || _stricmp(e, "restore") == 0)
m = LayoutLoad;
else // save / adjust / on / 1 / anything else truthy
m = LayoutSave;
if (m != LayoutOff)
DEBUG_STREAM << "[glasswin] BT_GLASS_LAYOUT="
<< (m == LayoutSave ? "save" : "load") << " (" << layoutFileName
<< ")\n" << std::flush;
}
return m;
}
// Find a window by its (stable) title. Titles carry no '=', so the cfg splits
// cleanly on the first '='.
static GWin *
FindGWinByTitle(const char *title)
{
for (int i = 0; i < gWinCount; ++i)
if (gWins[i].title != NULL && strcmp(gWins[i].title, title) == 0)
return &gWins[i];
return NULL;
}
// Restore saved positions over the just-computed pod-faithful defaults. Called
// from Create AFTER ComputeLayout, so any window not named in the file keeps its
// computed spot. Restored windows are flagged so the WM_TIMER re-snap (which
// re-runs ComputeLayout once the main window appears) leaves them alone.
static void
LoadLayout()
{
if (GlassLayoutMode() == LayoutOff)
return;
FILE *f = fopen(layoutFileName, "rt");
if (f == NULL)
{
DEBUG_STREAM << "[glasswin] no " << layoutFileName
<< " yet (using computed placement)\n" << std::flush;
return;
}
int restored = 0;
char line[256];
while (fgets(line, sizeof(line), f) != NULL)
{
char *s = line;
while (*s == ' ' || *s == '\t') ++s;
if (*s == '#' || *s == '\r' || *s == '\n' || *s == '\0')
continue;
char *eq = strchr(s, '=');
if (eq == NULL)
continue;
*eq = '\0';
// trim trailing space off the title
char *end = eq;
while (end > s && (end[-1] == ' ' || end[-1] == '\t')) --end;
*end = '\0';
int x = 0, y = 0, w = 0, h = 0;
if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
continue;
GWin *gw = FindGWinByTitle(s);
if (gw == NULL)
continue;
gw->wantX = x;
gw->wantY = y;
gw->restored = 1;
++restored;
}
fclose(f);
DEBUG_STREAM << "[glasswin] restored " << restored << " window position(s) from "
<< layoutFileName << "\n" << std::flush;
}
// Write every window's current on-screen frame rect. Whole-file rewrite (it is
// tiny), so partial/hard kills never leave a half-written file for long. Called
// on finished-drag and on teardown in save mode.
static void
SaveLayout()
{
if (GlassLayoutMode() != LayoutSave)
return;
FILE *f = fopen(layoutFileName, "wt");
if (f == NULL)
{
DEBUG_STREAM << "[glasswin] could not write " << layoutFileName
<< "\n" << std::flush;
return;
}
fputs("# BT411 glass cockpit window layout (BT_GLASS_LAYOUT=save writes this\n"
"# on finished-drag/exit; =load restores it). <title>=<x>,<y>,<w>,<h>\n",
f);
int wrote = 0;
for (int i = 0; i < gWinCount; ++i)
{
GWin &gw = gWins[i];
if (gw.hwnd == NULL || !IsWindow(gw.hwnd))
continue;
RECT r;
if (!GetWindowRect(gw.hwnd, &r))
continue;
fprintf(f, "%s=%ld,%ld,%ld,%ld\n", gw.title,
(long)r.left, (long)r.top,
(long)(r.right - r.left), (long)(r.bottom - r.top));
++wrote;
}
fclose(f);
DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to "
<< layoutFileName << "\n" << std::flush;
}
//###########################################################################
// Painting
//###########################################################################
@@ -715,6 +864,13 @@ static LRESULT CALLBACK
}
return 0;
case WM_EXITSIZEMOVE:
// The user finished dragging (or resizing) a window. In save/adjust
// mode, persist the whole ring so the layout survives a hard kill even
// if teardown never runs. No-op in load/off mode.
SaveLayout();
return 0;
case WM_CLOSE:
ShowWindow(window, SW_HIDE); // hide; the device owns the lifetime
return 0;
@@ -781,6 +937,7 @@ void
gWins[i].frameH = fr.bottom - fr.top;
}
ComputeLayout();
LoadLayout(); // BT_GLASS_LAYOUT=load/save: restore saved positions over the ring
for (int i = 0; i < gWinCount; ++i)
{
@@ -816,6 +973,9 @@ void
void
BTGlassPanels_Destroy()
{
SaveLayout(); // backstop for a clean teardown (WM_EXITSIZEMOVE already
// caught every finished drag); no-op unless mode==save
for (int i = 0; i < gWinCount; ++i)
{
if (gWins[i].hwnd != NULL)