diff --git a/docs/superpowers/plans/2026-07-15-auto-hide-caption.md b/docs/superpowers/plans/2026-07-15-auto-hide-caption.md new file mode 100644 index 0000000..19b1aae --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-auto-hide-caption.md @@ -0,0 +1,239 @@ +# Auto-Hide Caption Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show the calculator window's title bar and close button only while the window is active; when it loses activation it stays on top as a chromeless display, with the number display never moving on screen. + +**Architecture:** `CCalculatorWindow` handles `WM_ACTIVATE` and toggles `WS_CAPTION | WS_SYSMENU` via a new `ApplyFrameStyle(bool)` helper that keeps the client rectangle fixed in screen coordinates (caption grows upward / retracts). Window position is persisted in canonical form (client origin) so the saved point no longer depends on the caption state. + +**Tech Stack:** C++17, ATL `CWindowImpl`, raw Win32 (`SetWindowLongPtr`, `AdjustWindowRectEx`, `SetWindowPos`), VS 2022 `v143`, Windows SDK 10.0.26100.0. + +**Spec:** `docs/superpowers/specs/2026-07-15-auto-hide-caption-design.md` + +## Global Constraints + +- Build the **`.vcxproj` directly**, never the `.sln` (the solution references a setup project not in this repo): `msbuild TaskbarCalculator.vcxproj /p:Configuration=Debug /p:Platform=x64`. +- Commands assume an **x64 Native Tools Command Prompt for VS 2022** (`msbuild` and `cl` on PATH). +- Match existing style: TCHAR/`_T()`, ATL `CString`, raw Win32; `CalculatorWindow.cpp` uses the `stdafx.h` PCH. +- Only `CalculatorWindow.h` / `CalculatorWindow.cpp` may change. Engine (`calc`), `Settings` storage format, `App.cpp`, tray, packaging: untouched. +- **Testing note:** this feature is pure UI; the repo's unit-test harness covers only the `calc` engine. Each task's test cycle is therefore: (a) build succeeds, (b) engine regression `tests\build-and-run.cmd` still reports **16/16 passed**, (c) a targeted manual check listed in the task. There is no failing-test-first step because no UI test harness exists and building one is out of scope (YAGNI). + +--- + +### Task 1: Frame toggle on activation (`ApplyFrameStyle` + `WM_ACTIVATE`) + +**Files:** +- Modify: `CalculatorWindow.h` (message map ~line 34-45, private section ~line 52-61, members ~line 76-85) +- Modify: `CalculatorWindow.cpp` (new handler + helper, after `OnExitSizeMove` ~line 362) + +**Interfaces:** +- Consumes: existing `CWindowImpl` plumbing; window is created in `CreateStandalone` with `WS_POPUP | WS_CAPTION | WS_SYSMENU`, ex-style `WS_EX_TOPMOST | WS_EX_TOOLWINDOW`. +- Produces: `void CCalculatorWindow::ApplyFrameStyle(bool visible)` (private), `bool m_captionVisible` (private, starts `true`), `LRESULT OnActivate(UINT, WPARAM, LPARAM, BOOL&)`. Task 2 does not call these but relies on the invariant they establish: the client origin is the style-independent (canonical) position. + +- [ ] **Step 1: Declare handler, helper, and state in `CalculatorWindow.h`** + +In the message map, after `MESSAGE_HANDLER(WM_EXITSIZEMOVE, OnExitSizeMove)` add: + +```cpp + MESSAGE_HANDLER(WM_ACTIVATE, OnActivate) +``` + +In the private handler-prototype block, after the `OnExitSizeMove` declaration add: + +```cpp + LRESULT OnActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); +``` + +In the private section holding `m_fHasFocus` / `m_shown`, add the helper declaration and the state member: + +```cpp + void ApplyFrameStyle(bool visible); + bool m_captionVisible = true; // window is created with WS_CAPTION | WS_SYSMENU +``` + +- [ ] **Step 2: Implement `OnActivate` and `ApplyFrameStyle` in `CalculatorWindow.cpp`** + +Insert after `OnExitSizeMove` (line ~362), before `Paint`: + +```cpp +LRESULT CCalculatorWindow::OnActivate(UINT, WPARAM wParam, LPARAM, BOOL& bHandled) +{ + // Caption + close button only while the window is active. + ApplyFrameStyle(LOWORD(wParam) != WA_INACTIVE); + bHandled = FALSE; // let default activation processing run too + return 0; +} + +void CCalculatorWindow::ApplyFrameStyle(bool visible) +{ + if (visible == m_captionVisible) + return; + m_captionVisible = visible; + + // The client rect must not move on screen: capture it before the style change. + RECT rc; GetClientRect(&rc); + ::MapWindowPoints(m_hWnd, NULL, (LPPOINT)&rc, 2); + + LONG_PTR style = GetWindowLongPtr(GWL_STYLE); + if (visible) + style |= (WS_CAPTION | WS_SYSMENU); + else + style &= ~(WS_CAPTION | WS_SYSMENU); + SetWindowLongPtr(GWL_STYLE, style); + + RECT r = rc; + AdjustWindowRectEx(&r, (DWORD)style, FALSE, + (DWORD)GetWindowLongPtr(GWL_EXSTYLE)); + + // Guard: never push the top edge above the work area. + RECT work; SystemParametersInfo(SPI_GETWORKAREA, 0, &work, 0); + if (r.top < work.top) + ::OffsetRect(&r, 0, work.top - r.top); + + SetWindowPos(NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, + SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE); +} +``` + +Notes for the implementer: +- `GetWindowLongPtr`/`SetWindowLongPtr`/`GetClientRect`/`SetWindowPos` are `CWindow` members operating on `m_hWnd`; the `::`-prefixed calls are deliberate raw Win32. +- The early-return makes the toggle idempotent and prevents any recursion via nested `WM_ACTIVATE`. +- `AdjustWindowRectEx` with `WS_EX_TOOLWINDOW` accounts for the small tool-window caption height automatically. + +- [ ] **Step 3: Build** + +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Debug /p:Platform=x64` +Expected: `Build succeeded.` with `0 Error(s)`. + +- [ ] **Step 4: Engine regression** + +Run: `tests\build-and-run.cmd` +Expected: `16/16 passed`. + +- [ ] **Step 5: Targeted manual check** + +Launch `x64\Debug\TaskbarCalculator.exe`, left-click the tray icon: +- Window appears **with** caption + X (it is foreground). +- Click any other window: caption and X disappear; the number display does not move. +- Click the calculator body: caption and X reappear; display does not move. +- Click X: window hides to tray (existing behavior). +Exit via tray menu → Exit when done. + +- [ ] **Step 6: Commit** + +```bash +git add CalculatorWindow.h CalculatorWindow.cpp +git commit -m "feat: show caption and close button only while calculator window is active" +``` + +--- + +### Task 2: Canonical (client-origin) position persistence + +**Files:** +- Modify: `CalculatorWindow.cpp` — `OnExitSizeMove` (~line 357-362) and `PlaceInitial` (~line 159-178) + +**Interfaces:** +- Consumes: `Settings::SaveWindowPos(POINT)` / `Settings::LoadWindowPos(POINT&)` (unchanged, `Settings.h`); the canonical-position invariant from Task 1. +- Produces: no new symbols — changes the *meaning* of the persisted point from "window origin" to "client origin". Registry value names/types unchanged. + +- [ ] **Step 1: Save the client origin in `OnExitSizeMove`** + +Replace the body: + +```cpp +LRESULT CCalculatorWindow::OnExitSizeMove(UINT, WPARAM, LPARAM, BOOL&) +{ + // Persist the canonical position (client origin), independent of caption state. + RECT rc; GetClientRect(&rc); + ::MapWindowPoints(m_hWnd, NULL, (LPPOINT)&rc, 2); + Settings::SaveWindowPos(POINT{ rc.left, rc.top }); + return 0; +} +``` + +- [ ] **Step 2: Restore by converting the canonical point to a window origin in `PlaceInitial`** + +Replace only the `LoadWindowPos` branch (the first-run bottom-right anchor stays as is): + +```cpp + POINT pt; + if (Settings::LoadWindowPos(pt)) { + // Saved point is canonical (client origin). Convert to the window + // origin for the style currently in effect. + RECT r = { pt.x, pt.y, pt.x + 1, pt.y + 1 }; + AdjustWindowRectEx(&r, + (DWORD)GetWindowLongPtr(GWL_STYLE), FALSE, + (DWORD)GetWindowLongPtr(GWL_EXSTYLE)); + SetWindowPos(NULL, r.left, r.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER); + return; + } +``` + +Note: positions saved by older builds were window origins; they are reinterpreted as client origins exactly once (the window appears one caption height lower), then the position is stable forever after the first drag. + +- [ ] **Step 3: Build** + +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Debug /p:Platform=x64` +Expected: `Build succeeded.` with `0 Error(s)`. + +- [ ] **Step 4: Engine regression** + +Run: `tests\build-and-run.cmd` +Expected: `16/16 passed`. + +- [ ] **Step 5: Targeted manual check** + +Launch `x64\Debug\TaskbarCalculator.exe`, show the window, drag it somewhere distinctive (note where the *display* sits), exit via tray menu, relaunch, show the window: +- The display is exactly where it was left — no vertical drift. +- Repeat once more (exit while the window is inactive/chromeless): still no drift. + +- [ ] **Step 6: Commit** + +```bash +git add CalculatorWindow.cpp +git commit -m "fix: persist window position as client origin so caption toggle causes no drift" +``` + +--- + +### Task 3: Full manual verification + docs + +**Files:** +- Modify: `docs/superpowers/VERIFICATION.md` (append a section) + +**Interfaces:** +- Consumes: the finished behavior from Tasks 1-2. +- Produces: recorded verification evidence; no code. + +- [ ] **Step 1: Run the full checklist from the spec** + +Launch `x64\Debug\TaskbarCalculator.exe` and verify each item: +1. Show from tray → window appears with caption + X. +2. Click another app → caption disappears; display does not move. +3. Click the calculator → caption reappears; display does not move. +4. X → hides to tray. +5. Drag by caption and drag by body both work; position persists after restart. +6. Type `12+34` Enter → `46` (engine still wired through the edit control). +7. Open tray menu / About: calculator loses the caption while they are open (accepted side effect, spec §Accepted side effects). + +- [ ] **Step 2: Record results in `docs/superpowers/VERIFICATION.md`** + +Append: + +```markdown +## Auto-hide caption (2026-07-15) + +- ✅ Caption + X visible only while the window is active; chromeless when inactive. +- ✅ Display never moves when the frame toggles (client rect pinned). +- ✅ Position persists across restarts with no vertical drift (canonical client origin). +- ✅ X hides to tray; drag by caption and by body both work. +- ✅ Engine regression: 16/16 (`tests\build-and-run.cmd`). +``` + +- [ ] **Step 3: Commit** + +```bash +git add docs/superpowers/VERIFICATION.md +git commit -m "docs: verification results for auto-hide caption" +```