Skip to content

fix(gui): the Windows main window keeps its size and position across Frameless toggles and restarts (#6303). Principle VIII. - #6310

Merged
ten9876 merged 3 commits into
aethersdr:mainfrom
chibondking:fix/6303-window-drift
Oct 9, 2026
Merged

ten9876 merged 3 commits into
aethersdr:mainfrom
chibondking:fix/6303-window-drift

Conversation

@chibondking

@chibondking chibondking commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #6303.

What

On Windows with Frameless Window on, the main window's size and position drift on every Frameless toggle and on every restart. Each change is saved, so the drift builds up. Measured on main (Windows 10, 85% UI scale): every restart makes the client area 6 physical px wider and moves it 3–6 px left and 3 px down.

Root cause

With Qt::ExpandedClientAreaHint, Qt 6.12's Windows plugin doesn't take its frame margins from the real non-client area. qwindowswindow.cpp:3069-3081 subtracts a fixed qRound(scaleFactor) * 2 border from the system margins and guesses the top margin from what's left over. At :3057-3062 it stops recomputing them once the window rect and client rect are the same size. The real expanded frame is L8 T0 R8 B8, so Qt's bookkeeping is off:

  • geometry() reads back 7 logical px wider than the client Windows reports, even right after a setGeometry() produced that client.
  • restoreGeometry() rebuilds the size from the saved blob's frame rect. One saved blob had a normal rect of 1407×400 but a frame rect of 1389×391, and it relaunched 1424 wide.

MainWindow::setFramelessWindow() (save geometry(), re-create, setGeometry()) and the startup restore both round-trip through those numbers.

How

On Windows only, every behaviour change is inside #ifdef Q_OS_WIN. The main window keeps its client rect in physical pixels, read and set through Windows, instead of through Qt's frame margins. The rects live in one versioned feature document (Principle V), MainWindowNativeGeometry, with one entry per window role:

{"schemaVersion":1,
 "main":        {"x":131,"y":45,"width":1234,"height":411,"dpr":0.85},
 "fullMode":    {...},
 "minimalMode": {...}}
  • closeEvent() stores GetClientRect / ClientToScreen and the window's device pixel ratio as main, and as minimalMode when quitting in Minimal Mode. Only in the expanded mode and a normal (not minimized/maximized/full-screen) window; a maximized close keeps the last normal rect.
  • reapplyStartupGeometryAfterShow(), after the existing restoreGeometry(), puts main back with SetWindowPos. The window is sized from the frame it has now, and only when the pixel ratio matches. A UI or display scale change falls back to Qt's restore, as today. The off-screen clamp after it is unchanged.
  • Minimal Mode round trip. FullModeGeometry and MinimalModeGeometry go through restoreGeometry() and drift by the same margin error, so each is paired with its native rect (fullMode, minimalMode), saved and re-applied beside it in toggleMinimalMode().
  • setFramelessWindow() keeps the same client rect across the flag change. The toggle no longer depends on fix(gui): no light strip on the Windows 10 frame edges under the expanded chrome (#6266). Principle VIII. #6269's Windows 10 frame change, and it works the same on Windows 11, which fix(gui): no light strip on the Windows 10 frame edges under the expanded chrome (#6266). Principle VIII. #6269 doesn't touch.

A saved entry is ignored — and Qt's restore runs — when it is missing, from another schema version, at another pixel ratio, fractional, non-numeric, or outside the 16-bit Win32 coordinate range. The ratio is a JSON double, so it round-trips at full precision.

Only the main window takes ExpandedClientAreaHint. Pop-out pans, floating containers and dialogs switch Qt::FramelessWindowHint, whose frame margins are real, so they don't have this drift and are unchanged.

WindowChrome::windowRectForClient(), withNativeClientRect() and savedNativeClientRect() are pure helpers; the HWND calls and the document read/write are small MainWindow members.

Testing

Proof with the agent automation bridge (Windows 10 22H2, Intel HD 520, 85% UI scale, FLEX-6600 connected)

Builds came from the fork's Windows Installer workflow (Qt 6.12). Frameless was switched with the bridge (invoke "Frameless Window" trigger). Restarts were real closes: WM_CLOSE to the main window, so closeEvent() saves as it does for a user. Every rect is read from Windows (GetWindowRect, GetClientRect + ClientToScreen, DWM extended frame bounds), never from Qt.

Restart with Frameless on (client rect, physical px):

Build Launch 0 Launch 1 Launch 2 Launch 3
main 65b72a65 (installed 26.10.1) 151,21 1210×340 148,24 1216×340 143,27 1222×340 137,30 1228×340
first commit cb8712da 246,42 1210×411 246,42 1216×411 246,42 1222×411 246,42 1228×411
this PR c12b1ec6 131,45 1234×411 131,45 1234×411 131,45 1234×411 131,45 1234×411

On this PR's build the saved value was 131,45,1234,411@0.85, which matches what came back.

Frameless toggle (client rect, physical px):

Build Before → On → Off
main 165,44 1099×340 165,44 1099×340 (Qt's geometry() now 7 px wider) 162,47 1105×340
this PR 131,45 1234×411 131,45 1234×411 131,45 1234×411

Unit tests (local, Qt 6.12.0 pinned, MSVC 2022, offscreen)

  • The unified_title_bar_test checks for windowRectForClient (expanded frame, uneven borders, caption, asymmetric left/right) and the document helpers (round trip through compact JSON, other role, scale mismatch, full-precision scale, nothing saved, out of range, malformed, fractional, newer schema, empty) all pass on Linux (Qt 6.12, offscreen).
  • Mutation-checked: swapping the left/right border, dropping the range bound, storing the ratio as a float, dropping the schema check and dropping the integer check each turn the test red; dropping the caption term or the pixel-ratio comparison did the same in the earlier Windows run.
  • One existing check in the same binary, "four radios fit whole in an 1800 px bar", fails in my local Windows offscreen run with and without this change (it also fails on a tree that's main plus theme-only edits). CI on main (888ac58b, run 37865594009) is green, so this looks like a local font or metrics difference, not this PR.

Not verified

  • Windows 11. The fix measures the frame live from Windows, so it doesn't depend on the border sizes, but only Windows 10 was available.
  • Maximized and full-screen windows, which the new code deliberately skips; they weren't drifting.
  • Multi-monitor and mixed-DPI setups.
  • The Minimal Mode round trip and the feature-document storage on Windows. The document helpers are unit-tested on Linux; the Win32 paths compile only on check-windows and were not re-driven on Windows after the document change.
  • UI-scale or display-scale changes between sessions. The saved rect is ignored, and Qt's normal restore runs, by design.

Other platforms

macOS and Linux compile the same code as before. The pure helpers in WindowChrome.h compile everywhere but are only called from Windows code and the test.

Related

🤖 Generated with Claude Code

chibondking and others added 2 commits October 8, 2026 19:11
…ss Window on (aethersdr#6303). Principle VIII.

On Windows with Qt's expanded client area (Frameless Window on), Qt 6.12
doesn't take its frame margins from the real non-client area. It
subtracts a fixed border from the system margins
(qwindowswindow.cpp:3069), and stops recomputing them once the window
rect and the client rect are the same size (:3061). restoreGeometry()
rebuilds the window from the blob's frame rect using those margins, so
every launch came back a different size. Measured on Windows 10 at 85%
UI scale: the saved blob had a normal rect of 1407x400 but a frame rect
of 1389x391, and the relaunch came back 1424 wide (17 px more).

reapplyStartupGeometryAfterShow() now puts back the blob's normal
geometry, which is the client rect saveGeometry() recorded, right after
restoreGeometry(). It only does this on Windows, in the expanded mode,
in the normal window state. The off-screen clamp after it is unchanged.
WindowChrome::savedNormalGeometry() reads that rect from the blob, and
unified_title_bar_test covers it with a blob whose frame and normal
rects differ.

The issue also reports drift on each Frameless toggle. That comes from
MainWindow::setFramelessWindow() reapplying Qt's geometry() across the
flag change; geometry() is wrong by the same margin error. aethersdr#6269 fixes
it on Windows 10, where the window rect then equals the client rect;
toggles measured exact on that build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not Qt's frame margins (aethersdr#6303). Principle VIII.

The first attempt put back the saveGeometry() blob's normal rect after
startup. On the CI build (Windows 10, 85% UI scale, Frameless Window
on), that held the position, but the client still grew by 6 physical
px each launch: 1210, 1216, 1222, 1228. Under the expanded client area,
Qt's geometry() itself reads back 7 logical px wider than the
setGeometry() that produced it, so the rect being saved was already
wrong.

The main window now saves and restores its client rect in physical
pixels, read and set through Windows:

- closeEvent() stores GetClientRect/ClientToScreen with the window's
  device pixel ratio (MainWindowNativeClientRect), in the expanded mode
  and the normal window state only.
- reapplyStartupGeometryAfterShow() puts it back with SetWindowPos,
  sizing the window from the frame it has now, when the pixel ratio
  matches. A UI or display scale change falls back to Qt's restore.
- setFramelessWindow() keeps the same client rect across the flag
  change, so the toggle no longer depends on aethersdr#6269's Windows 10 frame
  change and works the same on Windows 11.

WindowChrome::windowRectForClient() and the format/parse pair are pure
and covered in unified_title_bar_test. Mutating the caption term out of
the margin math, or the pixel-ratio check out of the parser, fails it.
The blob parser from the first attempt is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chibondking
chibondking marked this pull request as ready for review October 9, 2026 03:12
@chibondking
chibondking requested a review from a team as a code owner October 9, 2026 03:12

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the root-cause write-up is the best part of the PR. qwindowswindow.cpp:3057-3062 short-circuiting the margin recompute once window rect == client rect, plus the qRound(scaleFactor) * 2 border subtraction, explains both halves of #6303 in a way the issue's table alone did not, and the four-launch table with the literal saved value 131,45,1234,411@0.85 is the kind of proof that makes a Windows-only fix reviewable from a Mac.

One thing I want settled before merge, and it is a one-line change either way. Everything else is a nit.

1. Issue fit

Solves it, for the main window. The issue's three symptoms map cleanly onto the three hunks: Off→On width (setFramelessWindow, MainWindow.cpp:9979-9994), On→Off position (same hunk — the client rect is captured before configure() and re-applied after show()), and restart drift (closeEvent :4099-4107 → reapplyStartupGeometryAfterShow :3876-3889). The frame is measured live from the HWND rather than assumed, so the fix does not depend on #6269's Windows 10 frame change, as the body claims. Physical pixels end to end, which is what the issue thread's item 4 asked for.

Not addressed, and worth naming in the body: the issue thread's item 5 (sibling top-levels — see nits).

2. Scope

File Change Claimed? Verdict
src/gui/MainWindow.cpp nativeClientRect() / setNativeClientRect(), the startup re-apply, the closeEvent save, the toggle capture/apply Yes In scope
src/gui/MainWindow.h the two new private member declarations Yes In scope
src/gui/WindowChrome.h windowRectForClient, formatNativeClientRect, parseNativeClientRect, 3 new includes Yes In scope
tests/unified_title_bar_test.cpp 8 checks for the three new helpers Yes In scope

4 files, +152/−0, no deletions, no - lines to read for a removed guard, no CHANGELOG entry, no settings migration, no protocol or capability surface. Everything in the diff is explained by the issue. Both commits SIGNED.

One scope item for the maintainer, not a violation: MainWindowNativeClientRect is a new loose flat AppSettings key (MainWindow.cpp:3883, :4103). docs/agents/settings.md:30-31 says "New configuration follows Constitution Principle V: one object per feature, not another loose flat key." Against that: window geometry is app-global, not radio-scoped, and its four existing siblings (MainWindowGeometry, MainWindowState, MinimalModeGeometry, FramelessWindow) are all flat keys, so folding this one into a document would leave it the odd one out. I read this as a maintainer call rather than a blocker — but the body skipped the PR template checklist, whose "No new flat-key AppSettings calls" box would have been the place this surfaced. Worth a line in the body either way.

3. Blockers

None confirmed. One item I could not settle headless, which is a blocker if it confirms and a style nit if it does not:

1. src/gui/MainWindow.cpp:3881, :4102, :9982 — the three new gates use strict equality on a flag set.

        && windowState() == Qt::WindowNoState) {
    const bool nativeRestorable = windowFlags().testFlag(Qt::ExpandedClientAreaHint)
        && windowState() == Qt::WindowNoState;
    const QRect nativeClient = wasVisible && windowState() == Qt::WindowNoState
        ? nativeClientRect() : QRect();

QWidget::windowState() returns Qt::WindowStates, and Qt documents the result as an OR'ed combination of WindowMinimized, WindowMaximized, WindowFullScreen and Qt::WindowActive. If WindowActive (0x8) is present on a focused window, all three gates are false whenever the main window has keyboard focus — which is exactly the state it is in when a user clicks the X to quit, or clicks View → Frameless Window. closeEvent would then store the empty string, the toggle would capture QRect(), and the fix would be inert for every real user while still passing your proof: invoke "Frameless Window" trigger and a posted WM_CLOSE do not require the window to be foreground, so the bridge run may never have exercised the active case.

Supporting evidence from the tree: every pre-existing window-state test in this file and its siblings masks rather than compares — MainWindow.cpp:4119 !(windowState() & (Qt::WindowMaximized | Qt::WindowFullScreen)), :10147, :10204, MainWindow_Menus.cpp:149 and :162 windowState() & ~Qt::WindowMinimized, MainWindow.cpp:3983 windowState().testFlag(...). Your three new sites are the only strict-equality uses of windowState() in src/.

I cannot resolve this from here — no Windows, no Qt sources on this box, and the question is about Qt's runtime value, not about your code. One qDebug() << windowState() in closeEvent, quitting via the X button rather than the bridge, settles it. The masked form is correct whichever way it lands, so it is worth taking regardless — it matches :4119's shape and keeps "normal window" meaning the same thing in all four places:

    const bool nativeRestorable = windowFlags().testFlag(Qt::ExpandedClientAreaHint)
        && !(windowState()
             & (Qt::WindowMinimized | Qt::WindowMaximized | Qt::WindowFullScreen));

4. Nits (non-blocking)

  • src/gui/WindowChrome.h:124-126 — the parsed rect has no upper bound. Credit: security-audit (CodeGuard), confirmed against the code.
        const QRect client(n[0], n[1], n[2], n[3]);
        return client.width() > 0 && client.height() > 0 ? client : QRect();
    toInt bounds each field to int, so "0,0,2147483647,2147483647@0.85" parses as valid, and windowRectForClient's client.width() + left + right (:84) is then signed-overflow UB before SetWindowPos sees it. A merely absurd value (100000×100000) does not overflow but does stick: the off-screen rescue at :3896-3913 only move()s, never resizes. The input is the user's own settings file, so this is self-inflicted rather than a privilege crossing — hence a nit, not a gate. A qBound-style cap (32767, or the virtual-desktop bounds) in parseNativeClientRect would close it, and a case would slot straight in beside the zero-size check at tests/unified_title_bar_test.cpp:166-169.
  • src/gui/WindowChrome.h:94-114 — the DPR guard is asymmetric. formatNativeClientRect writes the ratio with .arg(dpr, 0, 'g', 6), but parseNativeClientRect compares the 6-significant-digit value against the live full-precision one with qFuzzyCompare, whose tolerance is relative 1e-12. Any ratio needing more than 6 significant digits — e.g. a Windows custom scale of 133% (128/96 = 1.3333333…) times an 85% UI scale — never matches its own saved value, so the restore silently never fires and the window drifts as on main. The reporter's 0.85 and the common 125/150/175% steps are all exact at 6 digits, so this is an edge, not the main path. Comparing the formatted strings, or widening to 'g', 17, makes the guard symmetric by construction.
  • Sibling top-levels still drift. setFramelessWindow propagates the same chrome change to m_panStack, the container manager, m_connPanel, the tracked PersistentDialogs and m_titleBar's child dialogs (MainWindow.cpp:9996-10010), and PanFloatingWindow / FloatingContainerWindow / CanonWindow / the Strip* and Client* editors each save their own saveGeometry() blob through the same wrong margins. #6303 reports only the main window and the PR title says so, so this is completeness rather than a defect — but the body reads as if the drift class is closed. Worth one sentence plus a follow-up issue. Same for FullModeGeometry: a user who quits in Minimal Mode gets the minimal client rect restored correctly, then toggling back to full mode restores FullModeGeometry through Qt and lands on the drifted size.
  • closeEvent clears the key when the gate fails (:4103-4107). Quitting maximized discards a perfectly good saved normal client rect, so the first un-maximize after that launch comes back at Qt's drifted size. Keeping the previous value instead of writing QString() would be strictly better, and costs a read.
  • unified_title_bar_test is not in any ci.yml -R filter (grep over .github/workflows/*.yml returns nothing for it), so the 8 new checks compile in PR CI but only run in the full suite. Nothing to fix — just so the green badge is not read as "the new checks passed."
  • Your local "four radios fit whole in an 1800 px bar" failure is almost certainly not yours: that binary's fit checks are font-metric dependent and go red on hosts whose default sans differs from the CI image's. Your "fails with and without the change, and on main plus theme-only edits" reads correctly to me.

Socket tests: none. The added checks are pure-function calls on WindowChrome helpers — no QTcpServer/QUdpSocket/QLocalServer, no bind/listen/connectToHost, no peer process, no Fake* device class.

Test layer: right call, as far as it goes. The three helpers are pure value transforms and belong in a socket-free CTest, which is where you put them. The Win32 plumbing (GetClientRect/SetWindowPos, the two gates, the ordering against applyWindowsCaptionStyles) has no deterministic socket-free seam — it needs a real HWND on a real display — so bridge evidence is the honest ceiling and you supplied it. I am not asking for a fake-window harness.

5. What I tried to break, and could not

  • windowRectForClient against its own tests. Checks 1 and 3 both still pass if the body is replaced with return windowNow; — but check 2 (a different client rect, QRect(300,200,1000,300) → QRect(292,200,1016,308)) discriminates, and so does the mutation you report (dropping the caption term). The margin maths is right for negative x/y too, which multi-monitor setups need, and the three cases cover both the uneven expanded frame (L8/T0/R8/B8) and system decorations (31 px caption). The tests are not asserting the implementation back to itself.
  • Double application of the saved blob. The constructor's restoreGeometry() (:2592) and the post-show one (:3876) both still run, which the issue thread flagged as suspect. It does not accumulate here: the blob's +7 px error is a constant function of the saved bytes, not a function of the current geometry, and the SetWindowPos at :3820 runs last and overwrites both. I could not construct an ordering where the Qt restore wins.
  • Ordering around the HWND re-creation. WindowChrome::configure re-creates the native window, so I checked that winId() in setNativeClientRect cannot be the stale HWND: it is called after show(), and showEvent → applyWindowsCaptionStyles() (SetWindowLongPtr + SWP_FRAMECHANGED, :3776-3795) runs synchronously inside that show(), so the frame is already in its final shape when GetWindowRect measures it. The capture-before / apply-after split is the right way round.
  • The off-screen rescue. setNativeClientRect runs before the frame-center screen test (:3891-3913), so a saved rect on a now-disconnected monitor is still caught and re-centred — it loses the size correction on that path, but it does not leave the window invisible.
  • Degradation paths. DPR mismatch, empty key, malformed text, zero-size rect, GetClientRect failing, winId() null, maximized or minimized at close — every one of them falls back to Qt's existing restore rather than to a worse state. No new silent-failure mode that leaves the window somewhere it was not before.
  • Platform containment. Every behaviour change is inside #ifdef Q_OS_WIN; the three new WindowChrome.h helpers are unguarded inline functions, which is what lets the test compile on Linux and macOS. check-windows is the only lane that compiles the Q_OS_WIN arm and it is green on c12b1ec, along with build, check-macos, Static checks and the sanitizer configure.
  • CodeGuard's 63 findings. Walked them with the security-audit subagent. The CS-SEC-PASSWORD hit is QrzClient.cpp:38 m_password = password; — parameter plumbing, no literal. The CS-INJ-role_override hit is MainWindow.cpp:11523, a comment reading // CNN override: if carrierScore > 0.70, … about the QRM classifier; the homoglyph signal is the ≥/≤ in the adjacent comment block. Every CMDI-AMP is the C++ && operator and every CMDI-BACKTICK is a backtick inside a // comment; the only process launch in the file is list-form QProcess::startDetached with no shell. All pre-existing and outside this diff. Only the unbounded-rect lead above survived, and it is the one item that touches PR-introduced code.

All of the above is reasoned from the code in the head checkout. I have no Windows machine, no build and no way to drive the GUI, so I did not reproduce any of it at runtime — the empirical half of this PR rests on your bridge tables, which I read as internally consistent.

6. Recommendation

Request changes — pending one verification. The approach is right (physical pixels, frame measured live, graceful fallback, Windows-only), the evidence is real, and the scope is tight. The only thing standing between this and an approval is the windowState() question in blocker candidate 1: if Qt::WindowActive is in that flag set, the fix is inert for exactly the user-driven close and user-driven toggle the issue reports, and your proof would not have caught it. Take the masked form at all three sites — it is correct either way and matches :4119 — and confirm with one user-initiated close. If it comes back clean, this is approve-with-nits, and the nits can all follow later. The flat-key question is a maintainer call, not mine.


🤖 aethersdr-agent · cost: $10.8472 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (68 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6310/docs/architecture/aetherd-touchpoint-tags.json:1115
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6310/src/core/LogManager.cpp:153
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6310/src/core/LogManager.cpp:160
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6310/src/core/LogManager.cpp:353
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6310/src/core/LogManager.cpp:127
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6310/src/core/LogManager.cpp:175
  • [HIGH] CS-SEC-PASSWORD — Secret detected: CS-SEC-PASSWORD (generic) in src/core/QrzClient.cpp /tmp/aetherclaude/pr-6310/src/core/QrzClient.cpp:38
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:9482
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:9594
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:9713
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:9714
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:9715
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:6014
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:6093
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:520
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:550
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:553
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:575
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:3605
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:1270
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:1794
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:1886
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:1963
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:2005
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:2040
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:2319
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:3547
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:3638
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:5956
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:7762
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:7827
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:7901
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:7986
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:8167
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:8370
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:9583
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:6706
  • [CRITICAL] CS-INJ-role_override — Prompt injection: role override attempt in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:11523
  • [CRITICAL] CS-INJ-role_override — Prompt injection: role override attempt (detected after NFKC normalization) in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:11523
  • [HIGH] CS-INJ-homoglyph — Mixed Unicode scripts detected (possible homoglyph attack) in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6310/src/gui/MainWindow.cpp:0

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $11.0309 · model: claude-opus-5

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — PR #6310 (head c12b1ec6)

Issue fit: Solves #6303 for the main window. All three symptoms in the issue map to a hunk. Off→On width and On→Off position are handled by setFramelessWindow() (src/gui/MainWindow.cpp:9979-9994), which captures the native client rect before configure() and puts it back after show(). Restart drift is handled by the closeEvent() save (:4099-4107) and the re-apply in reapplyStartupGeometryAfterShow() (:3876-3889). The frame is measured live from the HWND every time, not assumed, so the fix doesn't depend on #6269's Windows 10 frame change. Every behaviour change sits inside #ifdef Q_OS_WIN, so Linux and macOS run exactly the code they ran before. The pure helpers in WindowChrome.h compile everywhere and are called only from Windows code and the test. I found no blockers.

Scope

File Change Claimed in body? Verdict
src/gui/MainWindow.cpp nativeClientRect() / setNativeClientRect(), the startup re-apply, the closeEvent save, the toggle capture/apply Yes In scope
src/gui/MainWindow.h Two private members, inside the existing #ifdef Q_OS_WIN block Yes In scope
src/gui/WindowChrome.h windowRectForClient, formatNativeClientRect, parseNativeClientRect, plus 3 Qt includes Yes In scope
tests/unified_title_bar_test.cpp 8 checks on the three helpers Yes In scope

The issue explains everything in the diff. There are no - lines, no CHANGELOG entry, and no protocol, capability or migration surface. Both commits are on-topic and dated from this PR.

One item for the maintainer (@ten9876), not a violation: MainWindowNativeClientRect is a new flat AppSettings key (src/gui/MainWindow.cpp:3883, :4103). docs/agents/settings.md:30-31 says: "New configuration follows Constitution Principle V: one object per feature, not another loose flat key." On the other side, this is app-global window state rather than configuration, and its siblings (MainWindowGeometry, MainWindowState, MinimalModeGeometry) are all flat. Folding only this one into a document would make it the odd one out. Please rule on it rather than let it pass silently.

Blockers

None.

Nits (non-blocking)

  1. src/gui/WindowChrome.h:97-99 — the saved scale is written at 6 significant digits but compared at full precision. formatNativeClientRect writes .arg(dpr, 0, 'g', 6). parseNativeClientRect (:109) then compares that rounded value with the live ratio using qFuzzyCompare, whose relative tolerance is about 1e-12. Any ratio that needs more than 6 digits never matches its own saved value, so the restore silently does nothing and the window drifts as it does on main. One example is a Windows custom scale whose DPI isn't a multiple of 24, such as 110 DPI: 110/96 × 0.85 = 0.97395833…, saved as 0.973958. The standard 25%-step scales times the two-decimal QT_SCALE_FACTOR (src/main.cpp:355) have at most 5 significant digits, so the reporter's setup and the common setups are fine. This is an edge case, not the main path. Writing 17 digits makes the round trip exact, because close and launch compute the ratio the same way:

        return QStringLiteral("%1,%2,%3,%4@%5")
            .arg(client.x()).arg(client.y()).arg(client.width()).arg(client.height())
            .arg(dpr, 0, 'g', 17);

    The bot review raised the same point; I agree with it.

  2. src/gui/WindowChrome.h:124-125 — the parsed rect has no upper bound. "0,0,2147483647,2147483647@0.85" parses as valid. windowRectForClient's client.width() + left + right (:87) then overflows a signed int. A merely huge value doesn't overflow, but it does stick, because the off-screen rescue at MainWindow.cpp:3896-3913 only moves the window and never resizes it. The value comes from the user's own settings, so this is self-inflicted. A cap of 32767 per dimension, the Win32 coordinate limit, would close it.

  3. tests/unified_title_bar_test.cpp:140-169 — two of the new checks can't fail for the reason they name. I mutation-tested the helpers on Linux; details are below.

    • With right replaced by left in windowRectForClient, every check still passed. All three fixtures have left == right (8/8), so "keeps the expanded frame's uneven borders" only covers top ≠ bottom. A fixture with different left and right borders would pin it.
    • The width() > 0 && height() > 0 guard at :125 is redundant: QRect(1,2,0,4).isValid() is already false. The "an empty rect gives no rect" check therefore passes with the guard removed. Either drop the guard or leave it as belt and braces; the test just doesn't prove it either way.
  4. src/gui/MainWindow.cpp:4103-4107 — quitting maximized, minimized or non-expanded overwrites a good saved rect with QString(). After a maximized quit, the first un-maximize on the next launch comes from Qt's drifted normal geometry. This is no worse than main. Leaving the previous value in place would be better, but it would also need a guard against that value going stale once Frameless is turned off.

  5. Sibling top-levels still use Qt's margins. FullModeGeometry (the Minimal→Full toggle), the floating pan and container windows, and the PersistentDialogs all still round-trip saveGeometry() through the same wrong expanded-frame margins. #6303 and the title only claim the main window, so this is about completeness, not a defect. A sentence in the body and a follow-up issue would keep "drift fixed" from reading as broader than it is.

  6. Merge order with #6269. Both PRs add code right after applyWindowsCaptionStyles() (MainWindow.cpp:~3796) and at MainWindow.h:352. git merge-tree of the two heads gives CONFLICT (content) in both files. The conflict is textual and trivial, but whichever PR lands second needs a rebase. The body says this PR works with or without #6269, which matches the code because the frame is measured live.

Correcting the bot review

  • windowState() == Qt::WindowNoState and Qt::WindowActive: in Qt 6.12, a widget's state only carries WindowActive when someone passes it to QWidget::setWindowState(). qwidgetwindow.cpp:1135 says "QWindow does currently not know 'active'", and handleWindowStateChangedEvent only carries over a WindowActive that was already there (:1147). Nothing in src/ sets Qt::WindowActive; grep -rn WindowActive src finds only an unrelated automation member. The author's saved value 131,45,1234,411@0.85 also proves the close-time gate was open. The masked form !(windowState() & (Min|Max|FullScreen)) would match :4119 and is nicer for consistency, but it isn't a blocker.
  • "unified_title_bar_test is not in any ci.yml -R filter": that is how the project runs tests. New tests register in tests/tests.cmake (done, :7410-7436) and run in the sanitizer lanes, not in ci.yml. It is not a coverage gap.

What I verified vs. read

  • Built and ran unified_title_bar_test at c12b1ec6 on Linux (Qt 6.12, offscreen): passes, including the "four radios fit whole in an 1800 px bar" check that fails in the author's local Windows run. That supports the author's reading that their failure comes from local font metrics.

  • Mutation-tested the helpers:

    • Removing the caption (top) term fails "the caption height goes above the client rect".
    • Removing the DPR comparison fails "a different scale falls back to Qt's restore".
    • right→left and removing the zero-size guard both survive (nit 3).
    • The tree was restored and the test passed again afterwards.
  • Engine boundary: check_engine_boundary.py over the three touched sources gives identical findings on the merge base and the head (12 tracked legacy, 0 new). The only includes added are QRect/QString/QStringList.

  • Socket preflight: no socket-owning or fake-peer test was added or modified. The new checks are pure function calls.

  • CI freshness: the checks are green, but they ran on a merge created 03:12Z. main has moved since then (4f16919, 04:34Z). None of the three new commits touch these files, and git merge-tree against current main is clean.

  • Reasoned from code only (not testable here): all the Win32 behaviour. I tried two attacks against the code:

    • Is the frame settled when setNativeClientRect measures it after the toggle? Yes. show() → showEvent → applyWindowsCaptionStyles() runs SWP_FRAMECHANGED synchronously first.
    • Does a missing saved monitor strand the window? No. The re-apply runs before the existing off-screen centre check, so a rect restored onto a disconnected monitor at a matching DPR is still pulled back on-screen.

    Not covered by any review here: Windows 11, mixed-DPI and multi-monitor setups, and the maximized/full-screen paths. The body discloses all of these.

  • Did not drive the app through the automation bridge. Every change is #ifdef Q_OS_WIN and this reviewer is on Linux, so a demo session would only exercise unchanged code. The author's Windows bridge tables are the runtime evidence, and they are consistent with the code.

@ten9876 ten9876 self-assigned this Oct 9, 2026
…ometry document (aethersdr#6303). Principle V.

The native client rect is saved as one versioned JSON object under
MainWindowNativeGeometry, one entry per window role, rather than a loose
flat key. Besides the launch rect ("main") it now covers the Minimal Mode
round trip: FullModeGeometry and MinimalModeGeometry go through
restoreGeometry() and drift by the same expanded-frame margin error, so
each is paired with its native client rect ("fullMode", "minimalMode").
Only MainWindow takes ExpandedClientAreaHint, so no other window drifts.

- The scale is stored as a JSON double, which round-trips at full
  precision; a 110 DPI display under a 0.85 UI scale now matches.
- Saved values outside the 16-bit Win32 coordinate range, fractional
  pixels, a non-numeric field or another schema version give no rect.
- Closing maximized keeps the last normal rect instead of erasing it.
- "Normal window" masks the minimized/maximized/full-screen bits, as
  the Minimal Mode geometry checks beside it do, rather than comparing
  the whole state for equality.
- Tests: an asymmetric left/right frame, out-of-range, fractional,
  newer-schema and full-precision cases, each failing when its guard is
  removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 force-pushed the fix/6303-window-drift branch from 46defc2 to 7004082 Compare October 9, 2026 05:08

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 700408230, after a landing pass on top of the two author commits. Maintainer decisions: fold the native rect into a feature document, and fold in the remaining drift surfaces.

Fixed in 700408230

  1. Flat key → one feature document (Principle V; docs/agents/settings.md:30-31). MainWindowNativeClientRect is replaced by one versioned JSON object, MainWindowNativeGeometry, with entries main / fullMode / minimalMode (src/gui/WindowChrome.h withNativeClientRect() / savedNativeClientRect(), kNativeGeometrySchemaVersion = 1). The key never shipped, so no migration is needed.
  2. The Minimal Mode round trip drifted the same way. FullModeGeometry and MinimalModeGeometry both go through restoreGeometry() and carry the same margin error, so each is now paired with its native rect, saved and re-applied in toggleMinimalMode() and closeEvent() (MainWindow.cpp, the saveNativeClientRect() / restoreNativeClientRect() call sites).
  3. Scale precision (my review's nit, and the bot's): the ratio is now a JSON double, so it round-trips at full precision by construction. Test: "the scale survives storage at full precision" (110/96 × 0.85).
  4. No upper bound (the bot's / CodeGuard's nit): values outside ±32767 (the 16-bit Win32 coordinate range), fractional pixels, non-numeric fields and other schema versions give no rect. This also rules out the signed overflow in windowRectForClient.
  5. Tests that could not fail. Added an asymmetric frame (left 3 px, right 11 px). The redundant zero-size guard is gone because QRect validity covers it; the new range, fractional and schema checks are the ones that discriminate.
  6. A maximized close no longer erases a good saved rect (closeEvent: saves main only when restorable).
  7. windowState() strict equality (the bot's potential blocker): replaced with the masked minimized/maximized/full-screen test that the neighbouring Minimal Mode checks use. On Qt 6.12 it was not live: QWidget's state only gains WindowActive through an explicit setWindowState() (qwidgetwindow.cpp:1135), and src/ never makes that call. The masked form is still the house shape.

Refuted / no change

  • Sibling top-levels "still drift" (the bot's nit 3): only MainWindow takes ExpandedClientAreaHint (grep -rn ExpandedClientAreaHint src/gui finds MainWindow.cpp, WindowChrome.h, TitleBar.cpp, mac/). PanFloatingWindow::setFramelessMode, PersistentDialog::setFramelessMode, CanonWindow and the container windows switch Qt::FramelessWindowHint. Under that hint Qt's frame margins are real, and the #6303 mechanism (qwindowswindow.cpp:3057-3081) does not apply to them.
  • "Not in a ci.yml filter": the sanitizer sweep runs the unfiltered tree, so this is not a coverage gap.
  • CodeGuard's 68 findings: everything outside the unbounded-rect lead (item 4) is pre-existing and outside this diff (&& operators, backticks in comments, list-form QProcess), as the bot's own triage also concluded.

Verified

  • unified_title_bar_test passes on Linux (Qt 6.12, offscreen, lean build at 700408230).
  • Mutations, each turning the test red, file restored afterwards: swapping right for left, dropping the range bound, storing the ratio as a float, dropping the schema check, dropping the integer check.
  • Static gates: the engine-boundary findings for MainWindow / WindowChrome are identical on the merge base and the head. gen_touchpoint_manifest --check, check_capability_records, check_command_plane and check_qsettings are clean.
  • check-windows is the only compile proof of the Q_OS_WIN arms, and it is green on this head.

Not verified

  • The Win32 paths were not run on Windows after the document change: the Minimal Mode round trip, the document storage, and Windows 11 / mixed DPI. The author's Windows 10 bridge tables cover the main-window launch and toggle logic, which the document change only re-routes.
  • I did not drive the app through the bridge. All behaviour is inside #ifdef Q_OS_WIN, so a Linux demo session exercises none of it.
  • The automated /code-review pass was not run in this landing pass; the equivalent pass was done by hand.

@ten9876
ten9876 merged commit abdd4f2 into aethersdr:main Oct 9, 2026
5 checks passed
ten9876 added a commit that referenced this pull request Oct 9, 2026
…nded chrome (#6266). Principle VIII. (#6269)

## Summary

Fixes #6266.

On Windows 10 with Frameless Window on, a light 1–2 px strip ran along
the left, right and bottom edges of the main window. It started with
#6198.

**Cause:** under `Qt::ExpandedClientAreaHint`, Qt 6.12's
`QWindowsGeometryHint::handleCalculateSize` takes over the top caption,
but keeps a resize-border inset on the left, right and bottom as
non-client. Windows 11 draws that area invisibly. Windows 10 paints it
as a flat light strip, whatever the OS colour scheme or accent settings
are.

**Changes:**
- **`MainWindow::nativeEvent` (Windows 10 only):** answers
`WM_NCCALCSIZE` with the whole window as client area when
`WindowChrome::claimsWholeWindowAsClient()` holds —
`ExpandedClientAreaHint` set and the OS older than Windows 11 (build
22000). While maximized (not full screen), it insets the sides by
`SM_CXSIZEFRAME + SM_CXPADDEDBORDER` and the top and bottom by
`SM_CYSIZEFRAME + SM_CXPADDEDBORDER`, at the window's DPI, so nothing
sits past the screen edge. No `WM_NCHITTEST` code is added: Qt's
`handleNonClientHitTest` already returns
`HTLEFT`/`HTRIGHT`/`HTBOTTOM`/corners from the client edges, which now
are the window edges.
- **`MainWindow::applyWindowsFrameColor` (Windows 11 only):** sets
`DWMWA_BORDER_COLOR` to `color.background.app` on show and on every
`themeChanged`, with Frameless Window on **or off**, so the DWM border
follows the theme. `dwmapi` is linked on Windows only.
- **Window geometry:** the native client-rect save/restore from #6303
(#6310) runs after this handler has taken effect.
`applyWindowsCaptionStyles()`'s `SWP_FRAMECHANGED` in `showEvent` routes
`WM_NCCALCSIZE` through it before `setFramelessWindow()` and the startup
/ minimal-mode re-apply measure the frame, so on Windows 10 the measured
frame is zero and a saved client rect is restored as the window rect
exactly. A comment at that call names the dependency.
- `docs/architecture/window-chrome.md` describes both, and that edge
resize on this path rests on Qt 6.12's hit-test alone.

macOS and Linux are untouched. On Windows 10, Frameless Window off is
untouched. Snap Layouts (#6224) is not addressed.

## Constitution principle honored

Principle VIII: the window edge is continuous with the app's chrome on
all four sides.

## Test plan

- [x] Windows 10 22H2 (19045), FLEX-6600, dark theme, Frameless Window
on: the strip is gone on the left, right and bottom (bridge luminance
readback in the PR thread).
- [x] Windows 10: resize from all four edges and all four corners;
maximize and restore; Win+Up and Aero Snap (checked by hand).
- [x] Windows 10: Frameless Window off is unchanged (checked by hand).
- [x] `unified_title_bar_test` (any host): the engage predicate is on
for Windows 10 under the expanded chrome, off for Windows 11 build 22000
and off for Frameless off; a zero frame restores a client rect as the
window rect. Both predicate checks fail when the predicate is mutated.
- [ ] Windows 10, after the rebase onto #6310: window position and size
across Frameless off → on → off, three relaunch cycles, a maximized
relaunch then un-maximize, and View → Minimal Mode twice.
- [ ] Windows 11: border follows `color.background.app` and updates when
the theme changes.
- [ ] CI (`check-windows` is the compile proof for the Windows-only
lines).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: CJ Johnson <chibondking@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: main window grows 7 px wider and creeps left/down on every Frameless Window toggle, and the drift is saved across restarts

2 participants