fix(tray): stop fractional-DPI tray-flyout resize oscillation via two-state cycle detection (#261) - #272
Conversation
…cle detection (#261) At 125% DPI the anchored tray flyout can flap between two physical heights 379<->386 px (2-4 Hz): measure setSize layout feedback keeps disagreeing with the native integer snap. Replace the 2logical deadband with a pure decision (lib/traySizing.decideTrayHeight) that commits ANY real change (even +5 physical px) but detects a bounded physical A->B->A pair (span <=8, span 7 observed), then retains the larger member and suppresses in-pair flips. Lock clears on out-of-pair changes, width/min-max/zoom/DPI changes. Applied-frame reconciliation adopts the candidate as committed when its physical frame is already on screen. surface maxHeight now follows the RETAINED height (post-decision), so DOM constraint and window never diverge. No timers, no observer gating, no broad deadband.
…kers The CI runner's scheduling can misalign a nudge with its pass (reveal count increments are not pass-unique). Wait on the surface maxHeight marker each pass sets instead; identical markers fall back to reveal+1.
|
Validation checkpoint (native smoke on head
Merge gate stands: an exact 125% / 120-DPI proof (reporter condition) is still required before this PR merges. Everything else — unit (13/13), hook integration, full frontend (246/246), tsc, build, CI — is green on this head. |
|
Final validation — reporter-condition (125%) CUA proof on head Acceptance environment this run: Windows Settings scale set to 125% via the visible Settings UI, fresh runtime DPI = 120 (DpiValue = 1), and app-reported scale factor = 1.25. The earlier
All gates green on this head (13/13 unit, hook integration, 246/246 full frontend, tsc, build, CI). Landing per maintainer approval. |
Fixes #261
Summary
The anchored tray flyout can flap between two native heights at fractional DPI — the reporter (Win11 25H2, 125% scale, AppliedDPI 120, app zoom 100%) saw it alternate 379 ↔ 386 physical px at 2–4 Hz; expanding the on-pace graph (a big geometry change) stops it permanently. Both maintainer rigs (native seeded app, and a physics-faithful Chromium sandbox at deviceScaleFactor 1.25 running the same DOM + auto-fit logic) converge, so the exact OS-layer driver (WebView2 composition at fractional scale on 25H2) remains unconfirmed. What is confirmed is the feedback class: measure (logical px) → setSize (integer physical snap) → layout → measure again, disagreeing by a few px per pass.
This PR targets that class deterministically, replacing the old 2-logical-px deadband with an explicit, pure sizing-decision function (
src/lib/traySizing.ts):innerSize()readback after a fractional snap/clamp), the setSize is suppressed and the committed frame is replaced by the candidate's own — DOM constraint and comparisons now describe on-screen reality — without manufacturing a fake history transition.surface.style.maxHeightfollows the retained height. The DOM constraint is now assigned AFTER the decision from the retained committed height, so the surface and the window can never diverge (the old order clamped the surface to the freshly measured candidate even when the old window size was retained — a clip/instability source).No timers, no debounces, no ResizeObserver gating: measurement always runs; only setSize/re-anchor commits are decided. Anchoring (applySize → reanchor on real commits only), manual/remembered sizing, zoom scaling (#266), min/max clamps, other tabs, and PopOut behavior are untouched — Rust/bridge untouched entirely.
Regression coverage (the exact 379↔386 pair)
src/lib/traySizing.test.ts— 13 pure tests modeling deviceScaleFactor 1.25: the reporter pair303.2↔308.8 logical = 379↔386 physicaldetects once and converges to the larger member with zero further commits; boundary/clearing matrix (out-of-pair, width, min/max, zoom, DPI); lone-change and >8-span flips stay commits; applied-frame reconciliation (incl.prioruntouched) and exact non-equality still committing.src/hooks/useTrayPanelLayout.sizing.test.tsx— real-hook integration at devicePixelRatio 1.25 with integer-physical snapping readbacks: stable +5-physical growth commits withmaxHeightfollowing; alternation converges with zero extra setSize/re-anchor whilesurface.maxHeightstays at the retained larger height during suppressed passes; genuine growth/shrink clear and commit; a deliberately snapped readback (requested 674, applied 669) reconciles to the candidate with no churn and provably no accidental A↔B trigger.Validation
tsc --noEmit: cleanpnpm build(locale drift check + tsc + vite): passFollow-ups (not in this PR)
CODEXBAR_RESIZE_PROBEdiagnostics branch stays local for the reporter build.Do NOT merge yet — pending CUA proof + review sign-off.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.