diff --git a/change/react-native-windows-fix-textinput-centering-dpi.json b/change/react-native-windows-fix-textinput-centering-dpi.json new file mode 100644 index 00000000000..f9479e65b4f --- /dev/null +++ b/change/react-native-windows-fix-textinput-centering-dpi.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix single-line TextInput text/caret sitting low and bottom-clipped at >100% display scale (GetContentSize DPI normalization)", + "packageName": "react-native-windows", + "email": "collindanielschneide@gmail.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index 2fd8fcde9a1..cc435bd6a99 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -55,6 +55,12 @@ std::string BstrToStdString(BSTR bstr, int cp = CP_UTF8) { return str; } +// GetDeviceCaps (used in GetContentSize to normalize RichEdit's device-pixel +// measurements to DIPs) is exported from Gdi32. Microsoft.ReactNative does not +// otherwise reference Gdi32, so link it explicitly to avoid an unresolved +// external symbol (LNK2019) in builds that don't already pull it in transitively. +#pragma comment(lib, "Gdi32.lib") + MSO_CLASS_GUID(ITextHost, "13E670F4-1A5A-11cf-ABEB-00AA00B65EA1") // IID_ITextHost MSO_CLASS_GUID(ITextServices, "8D33F740-CF58-11CE-A89D-00AA006CADC5") // IID_ITextServices MSO_CLASS_GUID(ITextServices2, "8D33F741-CF58-11CE-A89D-00AA006CADC5") // IID_ITextServices2 @@ -1314,13 +1320,25 @@ std::pair WindowsTextInputComponentView::GetContentSize() const no // Use the layout width as the constraint (always multiline) float availableWidth = m_layoutMetrics.frame.size.width; - float scale = m_layoutMetrics.pointScaleFactor; - float dpi = m_layoutMetrics.pointScaleFactor * GetDpiForSystem(); + + // TxGetNaturalSize measures in the *device pixels of `hdc`*. GetDC(nullptr) + // returns a screen DC whose logical DPI (GetDeviceCaps LOGPIXELS) is the system + // DPI - typically 96 - and is unrelated to the per-monitor display scale carried + // in pointScaleFactor. The previous code conflated the two + // (dpi = pointScaleFactor * GetDpiForSystem()) and then divided the returned + // natural size by pointScaleFactor, so at any display scale > 100% the measured + // content height came back short by exactly that scale and + // calculateContentVerticalOffset() over-centered the text (parked low / + // bottom-clipped). Normalize both conversions by the DC's real DPI instead. DIPs + // are 1/96in by definition, so the DIP<->HIMETRIC leg always uses 96. + const int hdcDpiX = GetDeviceCaps(hdc, LOGPIXELSX); + const int hdcDpiY = GetDeviceCaps(hdc, LOGPIXELSY); constexpr float HIMETRIC_PER_INCH = 2540.0f; + constexpr float DIPS_PER_INCH = 96.0f; SIZE extentHimetric = { - static_cast(availableWidth * scale * HIMETRIC_PER_INCH / dpi), - static_cast(std::numeric_limits::max() * HIMETRIC_PER_INCH / dpi)}; + static_cast(availableWidth * HIMETRIC_PER_INCH / DIPS_PER_INCH), + static_cast(std::numeric_limits::max() * HIMETRIC_PER_INCH / DIPS_PER_INCH)}; SIZE naturalSize = {0, 0}; @@ -1340,8 +1358,10 @@ std::pair WindowsTextInputComponentView::GetContentSize() const no return {0.0f, 0.0f}; } - float contentWidth = static_cast(naturalSize.cx) / scale; - float contentHeight = static_cast(naturalSize.cy) / scale; + // naturalSize is in the DC's device pixels; convert device px -> DIPs using the + // DC's actual DPI (px * 96 / hdcDpi), not pointScaleFactor. + float contentWidth = static_cast(naturalSize.cx) * DIPS_PER_INCH / hdcDpiX; + float contentHeight = static_cast(naturalSize.cy) * DIPS_PER_INCH / hdcDpiY; return {contentWidth, contentHeight}; }