Skip to content

fix(textinput): center single-line text correctly at >100% display scale#16302

Draft
FaithfulAudio wants to merge 2 commits into
microsoft:0.83-stablefrom
FacilitronWorks:fix/textinput-vertical-centering-dpi
Draft

fix(textinput): center single-line text correctly at >100% display scale#16302
FaithfulAudio wants to merge 2 commits into
microsoft:0.83-stablefrom
FacilitronWorks:fix/textinput-vertical-centering-dpi

Conversation

@FaithfulAudio

@FaithfulAudio FaithfulAudio commented Jul 11, 2026

Copy link
Copy Markdown

Problem

On a single-line <TextInput> at any display scale > 100% (125%, 250%, …), the text and caret sit too low in the control and are clipped at the bottom.

Root cause

WindowsTextInputComponentView::GetContentSize() measures RichEdit content via TxGetNaturalSize, which returns sizes in the device pixels of the measuring DC. The DC comes from GetDC(nullptr) — a screen DC whose logical DPI (GetDeviceCaps(hdc, LOGPIXELS)) is the system DPI (typically 96), unrelated to the per-monitor scale in pointScaleFactor. The code conflated the two: it built the HIMETRIC extent with dpi = pointScaleFactor * GetDpiForSystem() and divided the returned natural size by pointScaleFactor. Net: measured content height is short by exactly the display scale, so calculateContentVerticalOffset() centers with an undersized contentHeight and pushes the text down and off the bottom.

Fix

Normalize both conversions by the DC's real DPI: DIP↔HIMETRIC always uses 96 (DIPs are 1/96in by definition); device px → DIP uses px * 96 / GetDeviceCaps(hdc, LOGPIXELS). pointScaleFactor/GetDpiForSystem() are no longer used in the measurement. Gdi32.lib is linked explicitly (#pragma comment) — GetDeviceCaps is a Gdi32 export and Microsoft.ReactNative does not otherwise reference it (LNK2019 without it in a framework source build).

Validation

Bug reproduced in a production RNW 0.83.2 new-arch app (Facilitron FIT — an Expo monorepo app shipping a full Windows target) on Windows 11 ARM64, Debug, physical panel at a true 250% display scale. A stock single-line TextInput (deep Libraries/Components/TextInput/TextInput import, 44-DIP box, fontSize 18) parks its text on the bottom border and clips, while the app-side workaround this patch obsoletes (bottom padding fontSize*2.3*(1-1/scale)) centers the same input:

before: raw vs shimmed at 250%

This diff has been compiled into Microsoft.ReactNative from source on this machine (yarn-patch framework source build; the changed measurement verified present in the built binary — and the Gdi32.lib pragma requirement was found that way). Remaining for upstream: CI build + pixel-level after-shots at 100/125/150/250%.

Caveats for reviewers: base commit 8e869d7 is itself "[0.83] fix text input scaling (#16291)" — please confirm this refines (does not regress) that change. Authored against 0.83-stable to match our production app; happy to re-cut onto main if preferred. Separately noted (not addressed here): pointScaleFactor can go stale across monitor moves (no WM_DPICHANGED refresh).

Microsoft Reviewers: Open in CodeFlow

FaithfulAudio and others added 2 commits July 11, 2026 11:57
WindowsTextInputComponentView::GetContentSize() measured RichEdit content
with a DPI of `pointScaleFactor * GetDpiForSystem()` and then divided the
returned natural size by `pointScaleFactor`. TxGetNaturalSize actually reports
in the device pixels of the measuring DC (GetDC(nullptr) -> screen DC at the
system DPI, typically 96), which is independent of the per-monitor display
scale in pointScaleFactor. As a result content height was under-measured by
exactly the display scale at any scale > 100%, and calculateContentVerticalOffset()
over-centered the text so it parked low and was bottom-clipped.

Normalize both conversions by the DC's real DPI (GetDeviceCaps LOGPIXELS):
extent uses DIP<->HIMETRIC at the fixed 96 DIPs/inch, and device px are
converted back to DIPs with px*96/hdcDpi. Link Gdi32 explicitly for GetDeviceCaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@azure-pipelines

Copy link
Copy Markdown
Contributor
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@FaithfulAudio

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree [company="Facilitron"]

@FaithfulAudio

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Facilitron"

@FaithfulAudio

Copy link
Copy Markdown
Author

Update — honest validation results from a patched framework source build, including a finding reviewers should weigh.

We compiled this fix into Microsoft.ReactNative from source (binary-gated: patched dll confirmed as the loaded module) and re-ran the probe. On THIS machine the raw input rendered pixel-identical before/after — for an instructive reason: the machine is DPI-degenerate for this bug (system DPI is 240, equal to the 2.5 pointScaleFactor, so the old ÷pointScaleFactor and corrected ÷(hdcDpi/96) coincide). The patch's correctness argument stands for the common case where system DPI ≠ per-monitor scale (e.g. 96-DPI system with a 250% monitor), but we could not demonstrate a visual delta on this hardware.

More significant: programmatic ink measurement shows the rendered offset equals (boxHeight − 0)/2 — i.e. GetContentSize is effectively returning contentHeight ≈ 0 here, in stock and patched code. We believe the cy extent computation (LONG_MAX·2540/96) overflows LONG, which would zero the measured natural size and may be the dominant root cause of the mis-centering — worth investigating alongside (or ahead of) the DPI normalization in this PR. Happy to rework the PR to clamp/fix the extent overflow if maintainers agree with that reading.

After-shot (unchanged rendering, this machine): after

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.

1 participant