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
+2
View File
@@ -30,6 +30,8 @@ content/environ.ini
content/frontend.egg
content/steam_appid.txt
content/fe_last.ini
content/glass_layout.cfg
glass_layout.cfg
# local root copies of the player bats (players/ is the source of truth)
/play_solo.bat
/joyconfig.bat
+25
View File
@@ -114,6 +114,31 @@ each, so they cover the foot band too, and the hit test takes the first match
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
first-hit reachability, not mere presence.
## Sticky window placement — BT_GLASS_LAYOUT (2026-07-28) [T2 round-trip-verified]
The `BT_GLASS_PANELS` windows self-place in a pod-faithful ring around the main window
(`ComputeLayout`, then a one-shot re-snap once the real main window is up). They carry
`WS_CAPTION` in every mode, so a dev can always drag them — but by default a drag does **not**
survive relaunch (and the menu→mission→menu loop relaunches the process each hop). **`BT_GLASS_LAYOUT`**
(`L4GLASSWIN.cpp`) adds opt-in persistence to a cwd-relative `glass_layout.cfg` (beside
`bindings.txt`; gitignored):
| `BT_GLASS_LAYOUT` | Behaviour |
|---|---|
| unset / `off` / `0` | computed ring only, no file I/O (**default** — pod-faithful, unchanged) |
| `load` / `restore` | restore saved positions on startup (per-window fallback to computed for any not in the file); never writes |
| `save` / `adjust` / `on` / `1` | restore first, **then** re-write the whole file on each finished drag (`WM_EXITSIZEMOVE`) and on teardown — the round-trip "adjust" mode |
Mechanics: `glass_layout.cfg` is `# comment`-tolerant, one `«window title»=x,y,w,h` line per
window (title is the stable key — titles carry no `=`). **Position is restored; size is not**
each window's frame size is deterministic from its content (`AdjustWindowRect`), so honouring an
old saved `w,h` could distort a later geometry change; the `w,h` are written for reference only.
A restored window sets a `restored` flag that makes `ComputeLayout` skip it, so the post-main-window
re-snap leaves hand-placed windows alone (round-trip verified: a window saved at 777,333 came back
at exactly 777,333 after the re-snap fired). Save fires on `WM_EXITSIZEMOVE` (survives a hard kill,
since it writes as soon as a drag finishes) with a teardown write as backstop. This is the native
analogue of TeslaRel410 `emulator/pod-launch`'s per-rig `--bridge-pos`/`--layout` placement args.
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
+32
View File
@@ -732,3 +732,35 @@ mode). mkdist wraps the fragment in a doctype/charset shell and ships it beside
- 72/72 panel addresses still dispatch on click.
- surround / exploded / dock / pod / dev: boot + simulate, zero faults.
- Dist carries CONTROLS.txt + a 46 KB CONTROLS.html with doctype and charset.
## 2026-07-28 — Sticky per-display window placement (BT_GLASS_LAYOUT)
The exploded (`BT_GLASS_PANELS`) windows self-place in the pod-faithful ring and re-snap once the
main window appears; a dev could always drag them (they carry `WS_CAPTION`) but the drag never
survived the menu→mission→menu relaunch loop. New **`BT_GLASS_LAYOUT`** gate (all in
`L4GLASSWIN.cpp`, ~160 lines, no shared-file churn) persists positions to a cwd-relative
`glass_layout.cfg` beside `bindings.txt` (gitignored):
- **`off`/`0`/unset (default):** computed ring only, no file I/O — pod behaviour unchanged.
- **`load`/`restore`:** on `Create`, after `ComputeLayout`, `LoadLayout` overrides `wantX/wantY`
for each window named in the file (per-window fallback to computed) and sets a `restored` flag;
never writes.
- **`save`/`adjust`/`on`/`1`:** loads first, then `SaveLayout` rewrites the whole file on each
finished drag (`WM_EXITSIZEMOVE` in the WndProc) and once more on teardown (`Destroy`) as a
backstop. Writing on drag-finish means a hard kill still keeps the layout.
Format: `# comment`-tolerant, one `«title»=x,y,w,h` per window; title is the stable key (no `=` in
any title). **Position restored, size ignored** — frame size is deterministic from content
(`AdjustWindowRect`), so an old `w,h` can't distort a later geometry change; `w,h` are written for
reference only. The `restored` flag makes `ComputeLayout` skip that window, so the post-main-window
re-snap leaves hand-placed windows put. Native analogue of TeslaRel410 `emulator/pod-launch`'s
per-rig `--bridge-pos`/`--layout` args.
### Verified [T2, runtime round-trip]
- Build: Release links clean, only the 40 tolerated `/FORCE` externals, no compile errors.
- `load`: seeded a distinctive cfg → log `restored 7 window position(s)`, cockpit up, mission ran.
- `save`: moved *Flight Controls* to (777,333) + drove `WM_EXITSIZEMOVE` → cfg written with all 7
rects, the moved one at `777,333` (proves `GetWindowRect` read the live window), others at ring.
- round-trip: relaunched `load` against the game-written cfg → after 12 s (past the re-snap) the
*Flight Controls* window was physically at (777,333), all 7 at their saved coords — the re-snap
guard held.
+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)