diff --git a/CalculatorWindow.cpp b/CalculatorWindow.cpp index 6109ef1..dcc59db 100644 --- a/CalculatorWindow.cpp +++ b/CalculatorWindow.cpp @@ -131,8 +131,12 @@ BOOL CCalculatorWindow::CreateStandalone() { RECT rc = { 0, 0, 220, 48 }; // Top-level popup, always-on-top, tool window (no taskbar/Alt-Tab button). + // Created with the full "active" frame (caption + sizing border) so the + // window is resizable from the first show; ApplyFrameStyle strips it back + // to a clean look when the window is inactive. m_captionVisible (true) + // matches this initial style. HWND hwnd = __super::Create(NULL, rc, L"Taskbar Calculator", - WS_POPUP | WS_CAPTION | WS_SYSMENU, + WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME, WS_EX_TOPMOST | WS_EX_TOOLWINDOW); if (!hwnd) return FALSE; @@ -165,8 +169,18 @@ void CCalculatorWindow::RebuildForDpi(UINT dpi) // Size the CLIENT area from the (now DPI-scaled) font, scaling the fixed // paddings/minimums by the same DPI, then adjust for the current frame. POINTL ideal = CalcIdealSize(); - LONG clientW = ideal.x + ::MulDiv(24, (int)dpi, 96); - LONG minW = ::MulDiv(160, (int)dpi, 96); if (clientW < minW) clientW = minW; + + // Width: a saved custom width (normalized to 96 DPI) scaled to this DPI, + // otherwise the default auto-fit width. Never below the 5-digit minimum. + const LONG minClientW = MinClientWidth(dpi); + LONG clientW; + DWORD w96; + if (Settings::LoadWindowWidth(w96)) + clientW = ::MulDiv((LONG)w96, (int)dpi, 96); + else + clientW = ideal.x + ::MulDiv(24, (int)dpi, 96); + if (clientW < minClientW) clientW = minClientW; + LONG clientH = ideal.y + ::MulDiv(8, (int)dpi, 96); LONG minH = ::MulDiv(28, (int)dpi, 96); if (clientH < minH) clientH = minH; @@ -261,32 +275,35 @@ POINTL CCalculatorWindow::CalcMinimalSize() const return pt; } -POINTL CCalculatorWindow::CalcIdealSize() const +SIZE CCalculatorWindow::MeasureString(LPCTSTR text) const { - if(!IsWindow()) return CalcMinimalSize(); - -// m_sDateString = m_pDateFormat->FormatDateString(m_stLocalTime); - - HDC hic = ::CreateIC(_T("DISPLAY"), NULL, NULL, NULL); - - CString testString = TEXT("99.999,99"); - SIZE size = { 0 }; + HDC hic = ::CreateIC(_T("DISPLAY"), NULL, NULL, NULL); { SelectGdiObject gdiFont(hic, m_ptrVisualStyle->GetFont()); - - const BOOL bRes = ::GetTextExtentPoint32(hic, - testString, testString.GetLength(), &size); + const BOOL bRes = ::GetTextExtentPoint32(hic, text, (int)_tcslen(text), &size); ATLASSERT(bRes); } - ::DeleteDC(hic); + return size; +} - const POINTL pt = { size.cx, size.cy }; +POINTL CCalculatorWindow::CalcIdealSize() const +{ + if(!IsWindow()) return CalcMinimalSize(); + const SIZE size = MeasureString(TEXT("99.999,99")); + const POINTL pt = { size.cx, size.cy }; return pt; } +LONG CCalculatorWindow::MinClientWidth(UINT dpi) const +{ + // Worst-case 5 digits: sign + thousands separator + five digits. + const SIZE size = MeasureString(TEXT("-99.999")); + return size.cx + ::MulDiv(24, (int)dpi, 96); +} + BOOL CCalculatorWindow::HasFocus() const { return m_fHasFocus; @@ -441,13 +458,57 @@ LRESULT CCalculatorWindow::OnExitSizeMove(UINT, WPARAM, LPARAM, BOOL&) // then behind) the taskbar. ClampToWorkArea(); - // Persist the canonical position (client origin), independent of caption state. + // Persist the canonical position (client origin) and, only if this was an + // actual resize, the width (normalized to 96 DPI so it scales + // proportionally on other monitors). A plain move keeps the auto-fit width. RECT rc; GetClientRect(&rc); + if (m_didResize) + Settings::SaveWindowWidth((DWORD)::MulDiv(rc.right - rc.left, 96, (int)m_dpi)); ::MapWindowPoints(m_hWnd, NULL, (LPPOINT)&rc, 2); Settings::SaveWindowPos(POINT{ rc.left, rc.top }); return 0; } +LRESULT CCalculatorWindow::OnEnterSizeMove(UINT, WPARAM, LPARAM, BOOL&) +{ + // Capture the values the interactive-resize handler needs. Height is + // locked; width cannot go below the 5-digit minimum for the active frame. + m_didResize = false; + RECT wr; GetWindowRect(&wr); + m_fixedFrameHeight = wr.bottom - wr.top; + + RECT r = { 0, 0, MinClientWidth(m_dpi), 100 }; + ::AdjustWindowRectExForDpi(&r, + (DWORD)GetWindowLongPtr(GWL_STYLE), FALSE, + (DWORD)GetWindowLongPtr(GWL_EXSTYLE), m_dpi); + m_minResizeWidth = r.right - r.left; + return 0; +} + +LRESULT CCalculatorWindow::OnSizing(UINT, WPARAM wParam, LPARAM lParam, BOOL&) +{ + RECT* rc = (RECT*)lParam; + m_didResize = true; + + // Horizontal only: keep the height fixed. + rc->bottom = rc->top + m_fixedFrameHeight; + + // Enforce the minimum width by adjusting whichever edge is being dragged. + if ((rc->right - rc->left) < m_minResizeWidth) { + switch (wParam) { + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + case WMSZ_BOTTOMLEFT: + rc->left = rc->right - m_minResizeWidth; + break; + default: + rc->right = rc->left + m_minResizeWidth; + break; + } + } + return TRUE; +} + LRESULT CCalculatorWindow::OnActivate(UINT, WPARAM wParam, LPARAM, BOOL& bHandled) { // Caption + close button only while the window is active. @@ -468,14 +529,14 @@ void CCalculatorWindow::ApplyFrameStyle(bool visible) LONG_PTR style = GetWindowLongPtr(GWL_STYLE); if (visible) - style |= (WS_CAPTION | WS_SYSMENU); + style |= (WS_CAPTION | WS_SYSMENU | WS_THICKFRAME); else - style &= ~(WS_CAPTION | WS_SYSMENU); + style &= ~(WS_CAPTION | WS_SYSMENU | WS_THICKFRAME); SetWindowLongPtr(GWL_STYLE, style); RECT r = rc; - AdjustWindowRectEx(&r, (DWORD)style, FALSE, - (DWORD)GetWindowLongPtr(GWL_EXSTYLE)); + AdjustWindowRectExForDpi(&r, (DWORD)style, FALSE, + (DWORD)GetWindowLongPtr(GWL_EXSTYLE), m_dpi); // Guard: never push the top edge above the work area of the window's monitor. MONITORINFO mi = { sizeof(mi) }; diff --git a/CalculatorWindow.h b/CalculatorWindow.h index 7931e7b..11297a6 100644 --- a/CalculatorWindow.h +++ b/CalculatorWindow.h @@ -21,6 +21,7 @@ public: POINTL CalcMinimalSize() const; POINTL CalcIdealSize() const; + LONG MinClientWidth(UINT dpi) const; BOOL HasFocus() const; LRESULT OnEditKeyDown(HWND hWnd, WPARAM wParam, LPARAM lParam); @@ -45,6 +46,8 @@ BEGIN_MSG_MAP(CCalculatorWindow) MESSAGE_HANDLER(WM_EXITSIZEMOVE, OnExitSizeMove) MESSAGE_HANDLER(WM_ACTIVATE, OnActivate) MESSAGE_HANDLER(WM_DPICHANGED, OnDpiChanged) + MESSAGE_HANDLER(WM_ENTERSIZEMOVE, OnEnterSizeMove) + MESSAGE_HANDLER(WM_SIZING, OnSizing) END_MSG_MAP() // Handler prototypes: @@ -64,8 +67,11 @@ private: LRESULT OnExitSizeMove(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); LRESULT OnDpiChanged(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnEnterSizeMove(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnSizing(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); void Paint(HDC hdc, const RECT& rcPaint) const; + SIZE MeasureString(LPCTSTR text) const; // UINT_PTR SetUpdateTimer(); calc m_calc; @@ -82,6 +88,9 @@ private: BOOL m_fHasFocus; bool m_shown = false; UINT m_dpi = 96; + int m_fixedFrameHeight = 0; + int m_minResizeWidth = 0; + bool m_didResize = false; // a WM_SIZING happened in the current move/size loop CAutoPtr m_ptrVisualStyle; void ApplyFrameStyle(bool visible); void ClampToWorkArea(); diff --git a/Settings.cpp b/Settings.cpp index 20225fa..93e5989 100644 --- a/Settings.cpp +++ b/Settings.cpp @@ -46,4 +46,12 @@ namespace Settings { void SaveVisible(bool visible) { WriteDword(L"Visible", visible ? 1 : 0); } + bool LoadWindowWidth(DWORD& out) { + DWORD w; + if (ReadDword(L"Width96", w) && w > 0) { out = w; return true; } + return false; + } + void SaveWindowWidth(DWORD width96) { + WriteDword(L"Width96", width96); + } } diff --git a/Settings.h b/Settings.h index d50a7c8..d148f8d 100644 --- a/Settings.h +++ b/Settings.h @@ -6,4 +6,6 @@ namespace Settings { void SaveWindowPos(POINT pt); bool LoadVisible(bool def); // returns saved visibility or `def` void SaveVisible(bool visible); + bool LoadWindowWidth(DWORD& out); // client width normalized to 96 DPI; true if saved + void SaveWindowWidth(DWORD width96); } diff --git a/docs/superpowers/plans/2026-07-23-horizontal-resize.md b/docs/superpowers/plans/2026-07-23-horizontal-resize.md new file mode 100644 index 0000000..77a680c --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-horizontal-resize.md @@ -0,0 +1,437 @@ +# Horizontal Window Resize 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:** Let the user resize the calculator window horizontally (height fixed), with a minimum width that always shows at least 5 digits, and a width that persists across launches and scales with DPI. + +**Architecture:** Add `WS_THICKFRAME` to the active frame style (resize only when focused). Constrain to horizontal and enforce the minimum via `WM_SIZING` (interactive-only, so it never fights the programmatic DPI/theme resizes). Persist the width normalized to 96 DPI in the registry and re-apply it, DPI-scaled, in `RebuildForDpi`. + +**Tech Stack:** C++17, ATL `CWindowImpl`, raw Win32, MSBuild (VS 2022 `v143`), Windows SDK 10.0.26100.0. + +## Global Constraints + +- **Platform:** Windows 10 1607+ (`_WIN32_WINNT 0x0A00`, in `targetver.h`). `AdjustWindowRectExForDpi` available. +- **Build (primary x64):** `msbuild TaskbarCalculator.vcxproj /p:Configuration=Release /p:Platform=x64` and Debug. Build the `.vcxproj`, not the `.sln`. +- **Engine tests stay green:** `tests\build-and-run.cmd` → `16/16 passed`. No engine changes here. +- **Version:** target 3.0.5 — bump `VER_FIX` 4 → 5 in `version.h`. +- **Minimum width guarantee:** measured worst-case string is `-99.999` (5 digits + thousands separator + sign). +- **Persisted width:** registry `Width96` (`REG_DWORD`) under `HKCU\Software\TaskbarCalculator`, storing the client width normalized to 96 DPI (`MulDiv(clientW, 96, dpi)`). +- **Style:** TCHAR/`_T()`, ATL `CString`, raw Win32 — match the surrounding file. +- **GUI is verified visually** (build + engine tests are the automated gates; resize behavior confirmed by the maintainer). + +--- + +### Task 1: Persist a custom width in Settings + version bump + +**Files:** +- Modify: `Settings.h` (declare two functions) +- Modify: `Settings.cpp` (implement, using existing `ReadDword`/`WriteDword`) +- Modify: `version.h` (VER_FIX 4 → 5) + +**Interfaces:** +- Produces: `bool Settings::LoadWindowWidth(DWORD& out)` (true if a saved width exists); + `void Settings::SaveWindowWidth(DWORD width96)`. + +- [ ] **Step 1: Declare the functions** + +In `Settings.h`, add inside `namespace Settings` (after `SaveVisible`): + +```cpp + bool LoadWindowWidth(DWORD& out); // client width normalized to 96 DPI; true if saved + void SaveWindowWidth(DWORD width96); +``` + +- [ ] **Step 2: Implement them** + +In `Settings.cpp`, add inside `namespace Settings` (after `SaveVisible`): + +```cpp + bool LoadWindowWidth(DWORD& out) { + DWORD w; + if (ReadDword(L"Width96", w) && w > 0) { out = w; return true; } + return false; + } + void SaveWindowWidth(DWORD width96) { + WriteDword(L"Width96", width96); + } +``` + +- [ ] **Step 3: Bump the version** + +In `version.h`, change `#define VER_FIX 4` to `#define VER_FIX 5`. + +- [ ] **Step 4: Build Release x64** + +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Release /p:Platform=x64` +Expected: build succeeds (0 errors). (No behavior change yet — the functions are unused until Task 3.) + +- [ ] **Step 5: Commit** + +```bash +git add Settings.h Settings.cpp version.h +git commit -m "feat: add width persistence setting (Width96) for resizable window + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 2: Shared text measurement + 5-digit minimum width + +**Files:** +- Modify: `CalculatorWindow.h` (declare `MeasureString`, `MinClientWidth`) +- Modify: `CalculatorWindow.cpp` (refactor `CalcIdealSize`, add the two helpers) + +**Interfaces:** +- Produces: `SIZE CCalculatorWindow::MeasureString(LPCTSTR text) const`; + `LONG CCalculatorWindow::MinClientWidth(UINT dpi) const`. +- Consumes: existing `m_ptrVisualStyle`, `SelectGdiObject` (file-local class). + +- [ ] **Step 1: Declare the helpers** + +In `CalculatorWindow.h`, add near `CalcIdealSize` (public section): + +```cpp + LONG MinClientWidth(UINT dpi) const; +``` + +and in the private section (near `Paint`): + +```cpp + SIZE MeasureString(LPCTSTR text) const; +``` + +- [ ] **Step 2: Add `MeasureString` and refactor `CalcIdealSize`** + +In `CalculatorWindow.cpp`, replace the whole `CalcIdealSize` body: + +```cpp +POINTL CCalculatorWindow::CalcIdealSize() const +{ + if(!IsWindow()) return CalcMinimalSize(); + +// m_sDateString = m_pDateFormat->FormatDateString(m_stLocalTime); + + HDC hic = ::CreateIC(_T("DISPLAY"), NULL, NULL, NULL); + + CString testString = TEXT("99.999,99"); + + SIZE size = { 0 }; + { + SelectGdiObject gdiFont(hic, m_ptrVisualStyle->GetFont()); + + const BOOL bRes = ::GetTextExtentPoint32(hic, + testString, testString.GetLength(), &size); + ATLASSERT(bRes); + } + + ::DeleteDC(hic); + + const POINTL pt = { size.cx, size.cy }; + + return pt; +} +``` + +with: + +```cpp +SIZE CCalculatorWindow::MeasureString(LPCTSTR text) const +{ + SIZE size = { 0 }; + HDC hic = ::CreateIC(_T("DISPLAY"), NULL, NULL, NULL); + { + SelectGdiObject gdiFont(hic, m_ptrVisualStyle->GetFont()); + const BOOL bRes = ::GetTextExtentPoint32(hic, text, (int)_tcslen(text), &size); + ATLASSERT(bRes); + } + ::DeleteDC(hic); + return size; +} + +POINTL CCalculatorWindow::CalcIdealSize() const +{ + if(!IsWindow()) return CalcMinimalSize(); + + const SIZE size = MeasureString(TEXT("99.999,99")); + const POINTL pt = { size.cx, size.cy }; + return pt; +} + +LONG CCalculatorWindow::MinClientWidth(UINT dpi) const +{ + // Worst-case 5 digits: sign + thousands separator + five digits. + const SIZE size = MeasureString(TEXT("-99.999")); + return size.cx + ::MulDiv(24, (int)dpi, 96); +} +``` + +- [ ] **Step 3: Build Release x64** + +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Release /p:Platform=x64` +Expected: build succeeds. (Behavior unchanged: `CalcIdealSize` returns the same value; `MinClientWidth` is unused until Task 3/4.) + +- [ ] **Step 4: Commit** + +```bash +git add CalculatorWindow.h CalculatorWindow.cpp +git commit -m "refactor: extract MeasureString; add MinClientWidth (5-digit floor) + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 3: Apply the saved/DPI-scaled width; save it on resize + +**Files:** +- Modify: `CalculatorWindow.cpp` (`RebuildForDpi`, `OnExitSizeMove`) + +**Interfaces:** +- Consumes: `Settings::LoadWindowWidth`/`SaveWindowWidth` (Task 1); `MinClientWidth` (Task 2); `m_dpi`. + +- [ ] **Step 1: Use the saved width (DPI-scaled) in `RebuildForDpi`** + +In `CalculatorWindow.cpp`, replace the size-computation block inside `RebuildForDpi` — from `POINTL ideal = CalcIdealSize();` through the `clientH` min line: + +```cpp + POINTL ideal = CalcIdealSize(); + LONG clientW = ideal.x + ::MulDiv(24, (int)dpi, 96); + LONG minW = ::MulDiv(160, (int)dpi, 96); if (clientW < minW) clientW = minW; + LONG clientH = ideal.y + ::MulDiv(8, (int)dpi, 96); + LONG minH = ::MulDiv(28, (int)dpi, 96); if (clientH < minH) clientH = minH; +``` + +with: + +```cpp + POINTL ideal = CalcIdealSize(); + + // Width: a saved custom width (normalized to 96 DPI) scaled to this DPI, + // otherwise the default auto-fit width. Never below the 5-digit minimum. + const LONG minClientW = MinClientWidth(dpi); + LONG clientW; + DWORD w96; + if (Settings::LoadWindowWidth(w96)) + clientW = ::MulDiv((LONG)w96, (int)dpi, 96); + else + clientW = ideal.x + ::MulDiv(24, (int)dpi, 96); + if (clientW < minClientW) clientW = minClientW; + + LONG clientH = ideal.y + ::MulDiv(8, (int)dpi, 96); + LONG minH = ::MulDiv(28, (int)dpi, 96); if (clientH < minH) clientH = minH; +``` + +- [ ] **Step 2: Save the width on resize in `OnExitSizeMove`** + +In `CalculatorWindow.cpp`, replace `OnExitSizeMove`: + +```cpp +LRESULT CCalculatorWindow::OnExitSizeMove(UINT, WPARAM, LPARAM, BOOL&) +{ + // Keep the window within the monitor work area so it never sits over (and + // then behind) the taskbar. + ClampToWorkArea(); + + // 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; +} +``` + +with: + +```cpp +LRESULT CCalculatorWindow::OnExitSizeMove(UINT, WPARAM, LPARAM, BOOL&) +{ + // Keep the window within the monitor work area so it never sits over (and + // then behind) the taskbar. + ClampToWorkArea(); + + // Persist the canonical position (client origin) and the width, normalized + // to 96 DPI so it scales proportionally on other monitors. + RECT rc; GetClientRect(&rc); + Settings::SaveWindowWidth((DWORD)::MulDiv(rc.right - rc.left, 96, (int)m_dpi)); + ::MapWindowPoints(m_hWnd, NULL, (LPPOINT)&rc, 2); + Settings::SaveWindowPos(POINT{ rc.left, rc.top }); + return 0; +} +``` + +- [ ] **Step 3: Build Release x64** + +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Release /p:Platform=x64` +Expected: build succeeds. (Still no interactive resize until Task 4, but a `Width96` written by hand would now be honored on launch.) + +- [ ] **Step 4: Commit** + +```bash +git add CalculatorWindow.cpp +git commit -m "feat: apply and persist a DPI-scaled custom window width + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +### Task 4: Enable horizontal resizing with a locked height + +**Files:** +- Modify: `CalculatorWindow.h` (message map + handler decls + two members) +- Modify: `CalculatorWindow.cpp` (`ApplyFrameStyle`; add `OnEnterSizeMove`, `OnSizing`) + +**Interfaces:** +- Consumes: `MinClientWidth` (Task 2); `m_dpi`. +- Produces: `LRESULT OnEnterSizeMove(...)`, `LRESULT OnSizing(...)`; members `int m_fixedFrameHeight`, `int m_minResizeWidth`. + +- [ ] **Step 1: Add message-map entries and handler declarations** + +In `CalculatorWindow.h`, add to the message map (after the `WM_DPICHANGED` line): + +```cpp + MESSAGE_HANDLER(WM_ENTERSIZEMOVE, OnEnterSizeMove) + MESSAGE_HANDLER(WM_SIZING, OnSizing) +``` + +Add to the private handler prototypes (after `OnDpiChanged`): + +```cpp + LRESULT OnEnterSizeMove(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); + LRESULT OnSizing(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); +``` + +Add the members near `m_dpi`: + +```cpp + int m_fixedFrameHeight = 0; + int m_minResizeWidth = 0; +``` + +- [ ] **Step 2: Add `WS_THICKFRAME` to the active frame style** + +In `CalculatorWindow.cpp`, inside `ApplyFrameStyle`, replace: + +```cpp + 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)); +``` + +with: + +```cpp + if (visible) + style |= (WS_CAPTION | WS_SYSMENU | WS_THICKFRAME); + else + style &= ~(WS_CAPTION | WS_SYSMENU | WS_THICKFRAME); + SetWindowLongPtr(GWL_STYLE, style); + + RECT r = rc; + AdjustWindowRectExForDpi(&r, (DWORD)style, FALSE, + (DWORD)GetWindowLongPtr(GWL_EXSTYLE), m_dpi); +``` + +- [ ] **Step 3: Implement `OnEnterSizeMove` and `OnSizing`** + +In `CalculatorWindow.cpp`, add after `OnExitSizeMove`: + +```cpp +LRESULT CCalculatorWindow::OnEnterSizeMove(UINT, WPARAM, LPARAM, BOOL&) +{ + // Capture the values the interactive-resize handler needs. Height is + // locked; width cannot go below the 5-digit minimum for the active frame. + RECT wr; GetWindowRect(&wr); + m_fixedFrameHeight = wr.bottom - wr.top; + + RECT r = { 0, 0, MinClientWidth(m_dpi), 100 }; + ::AdjustWindowRectExForDpi(&r, + (DWORD)GetWindowLongPtr(GWL_STYLE), FALSE, + (DWORD)GetWindowLongPtr(GWL_EXSTYLE), m_dpi); + m_minResizeWidth = r.right - r.left; + return 0; +} + +LRESULT CCalculatorWindow::OnSizing(UINT, WPARAM wParam, LPARAM lParam, BOOL&) +{ + RECT* rc = (RECT*)lParam; + + // Horizontal only: keep the height fixed. + rc->bottom = rc->top + m_fixedFrameHeight; + + // Enforce the minimum width by adjusting whichever edge is being dragged. + if ((rc->right - rc->left) < m_minResizeWidth) { + switch (wParam) { + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + case WMSZ_BOTTOMLEFT: + rc->left = rc->right - m_minResizeWidth; + break; + default: + rc->right = rc->left + m_minResizeWidth; + break; + } + } + return TRUE; +} +``` + +- [ ] **Step 4: Build Debug + Release x64** + +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Release /p:Platform=x64` +Run: `msbuild TaskbarCalculator.vcxproj /p:Configuration=Debug /p:Platform=x64` +Expected: both succeed (0 errors). + +- [ ] **Step 5: Engine tests** + +Run: `tests\build-and-run.cmd` +Expected: `16/16 passed`. + +- [ ] **Step 6: Maintainer visual check** + +Launch `x64\Release\TaskbarCalculator.exe`. Expected: +- With the window focused, a sizing border is present; dragging a left/right edge or a + corner changes the **width** only; the **height** never changes. +- The window cannot be shrunk below the width of `-99.999` (~5 digits). +- Close and relaunch: the chosen width is restored. +- On a second monitor at different scaling, the width scales proportionally and still + respects the 5-digit minimum. + +- [ ] **Step 7: Commit** + +```bash +git add CalculatorWindow.h CalculatorWindow.cpp +git commit -m "feat: allow horizontal resizing with a fixed height and 5-digit minimum + +Add WS_THICKFRAME to the active frame and constrain resizing to the +horizontal axis via WM_SIZING (interactive-only, so it does not fight +the DPI/theme resizes). Height stays locked to one line; width cannot +drop below the width of \"-99.999\". + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Self-Review + +**Spec coverage:** +- Enable resize (WS_THICKFRAME active-only) → Task 4 Step 2. ✅ +- Horizontal constraint + min via WM_SIZING/WM_ENTERSIZEMOVE → Task 4 Steps 1,3. ✅ +- 5-digit minimum (`-99.999`) + shared MeasureString → Task 2. ✅ +- Width persistence (Width96, DPI-normalized) → Task 1 (storage) + Task 3 (apply/save). ✅ +- DPI-proportional width → Task 3 Step 1 (`MulDiv(w96, dpi, 96)`). ✅ +- AdjustWindowRectExForDpi in ApplyFrameStyle → Task 4 Step 2. ✅ +- Version 3.0.5 → Task 1 Step 3. ✅ +- Unchanged (clamp, topmost, caption, engine) → no task touches them beyond the frame-style edit. ✅ + +**Placeholder scan:** No TBD/vague steps; every code step has complete code. ✅ + +**Type consistency:** `MeasureString(LPCTSTR)->SIZE`, `MinClientWidth(UINT)->LONG`, `LoadWindowWidth(DWORD&)->bool`, `SaveWindowWidth(DWORD)`, members `m_fixedFrameHeight`/`m_minResizeWidth` (int) used consistently across Tasks 2–4. `Width96` key name identical in Task 1 and Task 3. ✅ diff --git a/docs/superpowers/specs/2026-07-23-horizontal-resize-design.md b/docs/superpowers/specs/2026-07-23-horizontal-resize-design.md new file mode 100644 index 0000000..907601b --- /dev/null +++ b/docs/superpowers/specs/2026-07-23-horizontal-resize-design.md @@ -0,0 +1,106 @@ +# Horizontal window resize — design + +**Date:** 2026-07-23 +**Version target:** 3.0.5 (bump `VER_FIX` 4 → 5) + +## Problem / goal + +Let the user resize the calculator window **horizontally** by dragging a side/corner, +so a wider window shows more digits. Height stays fixed to a single line of text (no +vertical resize). The width cannot shrink below a minimum that always keeps at least +**5 digits** visible, measured worst-case as `-99.999` (5 digits + thousands separator + +sign). The chosen width persists across launches and scales proportionally with the +monitor DPI. + +## Current state (relevant) + +- `CCalculatorWindow` is a `WS_POPUP | WS_CAPTION | WS_SYSMENU` tool window + (`WS_EX_TOPMOST | WS_EX_TOOLWINDOW`), no sizing border. +- It auto-sizes to content: `RebuildForDpi` computes the client size from + `CalcIdealSize()` (text extent of `99.999,99`) and DPI-scaled paddings. +- `ApplyFrameStyle(bool)` toggles `WS_CAPTION | WS_SYSMENU` on activate/deactivate + (caption auto-hide), keeping the client rect fixed on screen. +- `OnExitSizeMove` clamps to the work area and persists the position (`Settings::SaveWindowPos`). +- `Settings` stores `PosX`/`PosY`/`Visible` as `REG_DWORD` under + `HKCU\Software\TaskbarCalculator`. + +## Design + +### 1. Enable resizing (active only) — `ApplyFrameStyle` + +Add `WS_THICKFRAME` to the active frame style alongside `WS_CAPTION | WS_SYSMENU`, and +remove it when inactive. The sizing border appears only when the window has focus, keeping +the clean look at rest (consistent with the existing caption auto-hide). While here, change +that method's `AdjustWindowRectEx` to `AdjustWindowRectExForDpi(..., m_dpi)` for DPI +correctness (it currently uses the non-DPI variant). + +### 2. Constrain to horizontal + enforce minimum — `WM_ENTERSIZEMOVE` / `WM_SIZING` + +`WS_THICKFRAME` allows resizing from every edge; restrict it to horizontal via `WM_SIZING` +rather than `WM_GETMINMAXINFO`, because `WM_SIZING` fires **only** during an interactive +resize and therefore cannot interfere with the programmatic resizes done by `RebuildForDpi` +and `ApplyFrameStyle`. + +- **`WM_ENTERSIZEMOVE`** (new handler): capture the current outer window height + (`m_fixedFrameHeight`) and the minimum outer window width for the current DPI/style + (`m_minResizeWidth`, from `MinClientWidth(m_dpi)` adjusted by the active frame). These are + also computed for a move (drag body), harmlessly unused there. +- **`WM_SIZING`** (new handler): given the proposed window rect (`LPRECT` in `lParam`): + - lock height: `rc->bottom = rc->top + m_fixedFrameHeight`; + - enforce min width: if narrower than `m_minResizeWidth`, move the dragged edge + (`WMSZ_LEFT`/`TOPLEFT`/`BOTTOMLEFT` → adjust `left`; otherwise adjust `right`). + - return `TRUE` (rect modified). + +Note: dragging the pure top/bottom edge does nothing useful (height locked); the primary +gesture is left/right/corner. Acceptable for a one-line utility. + +### 3. Minimum-width measurement — `MinClientWidth` + shared `MeasureString` + +Factor the text-measuring boilerplate currently inside `CalcIdealSize` into a private +`SIZE MeasureString(LPCTSTR text) const` (creates an info DC, selects the current display +font, calls `GetTextExtentPoint32`). `CalcIdealSize` measures `99.999,99` through it; +`MinClientWidth(UINT dpi)` measures `-99.999` and returns its width plus the same +DPI-scaled horizontal padding used for the default width. + +### 4. Width persistence, DPI-proportional — `Settings` + `RebuildForDpi` + +- **`Settings`**: add `LoadWindowWidth(DWORD& out)` / `SaveWindowWidth(DWORD)` backed by a + new `Width96` `REG_DWORD`. The stored value is the client width **normalized to 96 DPI** + (`MulDiv(clientW, 96, m_dpi)`), so it is DPI- and monitor-independent. +- **`OnExitSizeMove`**: after the existing work-area clamp, also save the width: + `Settings::SaveWindowWidth(MulDiv(clientW, 96, m_dpi))` (client width from + `GetClientRect`). Position saving stays as is. +- **`RebuildForDpi`**: choose the target client width: + - `minClientW = MinClientWidth(dpi)`; + - `defaultClientW = CalcIdealSize().x + MulDiv(24, dpi, 96)` (today's auto-fit); + - if `Settings::LoadWindowWidth(w96)` → `clientW = MulDiv(w96, dpi, 96)`, else + `clientW = defaultClientW`; + - `if (clientW < minClientW) clientW = minClientW`. + The height computation is unchanged. The old hard-coded `160` min-width floor is + replaced by `minClientW`. + +### 5. Unchanged + +Position/taskbar clamp, always-on-top, caption auto-hide semantics, DPI scaling of font +and height, `WM_DPICHANGED` handling, and the `calc` engine (tests stay 16/16). + +### 6. Version + +`VER_FIX` 4 → 5 (**3.0.5**). + +## Testing / verification + +- Engine tests unchanged: `tests\build-and-run.cmd` → `16/16`. +- Build Debug + Release x64 succeed. +- Visual (maintainer): dragging a side/corner resizes width only; height fixed; cannot + shrink below ~5 digits (`-99.999` fits); width persists across restart; on a monitor with + different scaling the width scales proportionally and never drops below the 5-digit min. + +## Risks / notes + +- `WM_SIZING` height-lock uses the height captured at `WM_ENTERSIZEMOVE`; correct because + height never changes during a horizontal resize. +- Adding `WS_THICKFRAME` only in the active style means the first activation grows the + frame; `ApplyFrameStyle` already preserves the client rect across the style change. +- `Width96` normalization assumes `m_dpi` is valid when saving (it is: set in + `CreateStandalone` before any user interaction). diff --git a/version.h b/version.h index 72a0f76..dec6340 100644 --- a/version.h +++ b/version.h @@ -3,7 +3,7 @@ // Semantic version — edit these by hand (NOT in the VS resource editor). #define VER_MAJOR 3 #define VER_MINOR 0 -#define VER_FIX 4 +#define VER_FIX 5 // Per-build counter. version_build.h is regenerated on every build by the // IncrementBuildCounter MSBuild target (BeforeTargets="ResourceCompile"), so it