Skip to content

Commit ca33bda

Browse files
committed
Center single line textinputs (microsoft#15754)
* Center single line textinputs * Change files * snapshot
1 parent 6bdad28 commit ca33bda

5 files changed

Lines changed: 97 additions & 61 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Center single line textinputs",
4+
"packageName": "react-native-windows",
5+
"email": "30809111+acoates-ms@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,9 @@ exports[`TextInput Tests Text have cursorColor 1`] = `
381381
"Brush Type": "ColorBrush",
382382
"Color": "rgba(0, 128, 0, 255)",
383383
},
384-
"Offset": "83, 5, 0",
384+
"Offset": "89, 6, 0",
385385
"Opacity": 0,
386-
"Size": "1, 19",
386+
"Size": "1, 21",
387387
"Visual Type": "SpriteVisual",
388388
},
389389
],
@@ -1884,9 +1884,9 @@ exports[`TextInput Tests TextInputs can clear on submit 1`] = `
18841884
"Brush Type": "ColorBrush",
18851885
"Color": "rgba(0, 0, 0, 255)",
18861886
},
1887-
"Offset": "5, 5, 0",
1887+
"Offset": "5, 6, 0",
18881888
"Opacity": 0,
1889-
"Size": "1, 19",
1889+
"Size": "1, 21",
18901890
"Visual Type": "SpriteVisual",
18911891
},
18921892
],
@@ -2805,9 +2805,9 @@ exports[`TextInput Tests TextInputs can have caretHidden 1`] = `
28052805
"Brush Type": "ColorBrush",
28062806
"Color": "rgba(0, 0, 0, 255)",
28072807
},
2808-
"Offset": "83, 5, 0",
2808+
"Offset": "89, 6, 0",
28092809
"Opacity": 0,
2810-
"Size": "1, 19",
2810+
"Size": "1, 21",
28112811
"Visual Type": "SpriteVisual",
28122812
},
28132813
],
@@ -4993,9 +4993,9 @@ exports[`TextInput Tests TextInputs can select text on focus 1`] = `
49934993
"Brush Type": "ColorBrush",
49944994
"Color": "rgba(0, 0, 0, 255)",
49954995
},
4996-
"Offset": "83, 5, 0",
4996+
"Offset": "89, 6, 0",
49974997
"Opacity": 0,
4998-
"Size": "1, 19",
4998+
"Size": "1, 21",
49994999
"Visual Type": "SpriteVisual",
50005000
},
50015001
],
@@ -5226,7 +5226,7 @@ exports[`TextInput Tests TextInputs can submit with custom key, multilined and s
52265226
},
52275227
"Offset": "5, 5, 0",
52285228
"Opacity": 0,
5229-
"Size": "1, 19",
5229+
"Size": "1, 21",
52305230
"Visual Type": "SpriteVisual",
52315231
},
52325232
],

vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ struct CompTextHost : public winrt::implements<CompTextHost, ITextHost> {
246246
//@cmember Converts screen coordinates of a specified point to the client coordinates
247247
BOOL TxScreenToClient(LPPOINT lppt) override {
248248
winrt::Windows::Foundation::Point pt{static_cast<float>(lppt->x), static_cast<float>(lppt->y)};
249+
pt.X -= m_outer->m_contentOffsetPx.x;
250+
pt.Y -= m_outer->m_contentOffsetPx.y;
249251
auto localpt = m_outer->ScreenToLocal(pt);
250252
lppt->x = static_cast<LONG>(localpt.X);
251253
lppt->y = static_cast<LONG>(localpt.Y);
@@ -255,9 +257,14 @@ struct CompTextHost : public winrt::implements<CompTextHost, ITextHost> {
255257
//@cmember Converts the client coordinates of a specified point to screen coordinates
256258
BOOL TxClientToScreen(LPPOINT lppt) override {
257259
winrt::Windows::Foundation::Point pt{static_cast<float>(lppt->x), static_cast<float>(lppt->y)};
260+
261+
if (!m_outer->m_parent) {
262+
return false;
263+
}
264+
258265
auto screenpt = m_outer->LocalToScreen(pt);
259-
lppt->x = static_cast<LONG>(screenpt.X);
260-
lppt->y = static_cast<LONG>(screenpt.Y);
266+
lppt->x = static_cast<LONG>(screenpt.X) + m_outer->m_contentOffsetPx.x;
267+
lppt->y = static_cast<LONG>(screenpt.Y) + m_outer->m_contentOffsetPx.y;
261268
return true;
262269
}
263270

@@ -276,20 +283,25 @@ struct CompTextHost : public winrt::implements<CompTextHost, ITextHost> {
276283
//@cmember Retrieves the coordinates of a window's client area
277284
HRESULT TxGetClientRect(LPRECT prc) override {
278285
*prc = m_outer->getClientRect();
286+
287+
prc->top += m_outer->m_contentOffsetPx.y;
288+
prc->bottom += m_outer->m_contentOffsetPx.y -
289+
static_cast<LONG>(m_outer->m_layoutMetrics.contentInsets.bottom * m_outer->m_layoutMetrics.pointScaleFactor);
290+
prc->left += m_outer->m_contentOffsetPx.x;
291+
prc->right += m_outer->m_contentOffsetPx.x -
292+
static_cast<LONG>(m_outer->m_layoutMetrics.contentInsets.right * m_outer->m_layoutMetrics.pointScaleFactor);
293+
279294
return S_OK;
280295
}
281296

282297
//@cmember Get the view rectangle relative to the inset
283298
HRESULT TxGetViewInset(LPRECT prc) override {
284299
// Inset is in HIMETRIC
285-
constexpr float HmPerInchF = 2540.0f;
286-
constexpr float PointsPerInch = 96.0f;
287-
constexpr float dipToHm = HmPerInchF / PointsPerInch;
288300

289-
prc->left = static_cast<LONG>(m_outer->m_layoutMetrics.contentInsets.left * dipToHm);
290-
prc->top = static_cast<LONG>(m_outer->m_layoutMetrics.contentInsets.top * dipToHm);
291-
prc->bottom = static_cast<LONG>(m_outer->m_layoutMetrics.contentInsets.bottom * dipToHm);
292-
prc->right = static_cast<LONG>(m_outer->m_layoutMetrics.contentInsets.right * dipToHm);
301+
prc->left = 0;
302+
prc->top = 0;
303+
prc->bottom = 0;
304+
prc->right = 0;
293305

294306
return NOERROR;
295307
}
@@ -492,11 +504,6 @@ AutoCorrectOffCallback(LANGID langid, const WCHAR *pszBefore, WCHAR *pszAfter, L
492504
facebook::react::AttributedString WindowsTextInputComponentView::getAttributedString() const {
493505
// Use BaseTextShadowNode to get attributed string from children
494506

495-
auto childTextAttributes = facebook::react::TextAttributes::defaultTextAttributes();
496-
childTextAttributes.fontSizeMultiplier = m_fontSizeMultiplier;
497-
498-
childTextAttributes.apply(windowsTextInputProps().textAttributes);
499-
500507
auto attributedString = facebook::react::AttributedString{};
501508
// auto attachments = facebook::react::BaseTextShadowNode::Attachments{};
502509

@@ -1114,6 +1121,9 @@ void WindowsTextInputComponentView::updateProps(
11141121
!facebook::react::floatEquality(
11151122
oldTextInputProps.textAttributes.letterSpacing, newTextInputProps.textAttributes.letterSpacing) ||
11161123
oldTextInputProps.textAttributes.fontFamily != newTextInputProps.textAttributes.fontFamily ||
1124+
oldTextInputProps.textAttributes.fontStyle != newTextInputProps.textAttributes.fontStyle ||
1125+
oldTextInputProps.textAttributes.textDecorationLineType !=
1126+
newTextInputProps.textAttributes.textDecorationLineType ||
11171127
!facebook::react::floatEquality(
11181128
oldTextInputProps.textAttributes.maxFontSizeMultiplier,
11191129
newTextInputProps.textAttributes.maxFontSizeMultiplier)) {
@@ -1129,6 +1139,7 @@ void WindowsTextInputComponentView::updateProps(
11291139
}
11301140

11311141
if (oldTextInputProps.multiline != newTextInputProps.multiline) {
1142+
m_recalculateContentVerticalOffset = true;
11321143
m_multiline = newTextInputProps.multiline;
11331144
m_propBitsMask |= TXTBIT_MULTILINE | TXTBIT_WORDWRAP;
11341145
if (newTextInputProps.multiline) {
@@ -1278,6 +1289,10 @@ void WindowsTextInputComponentView::updateLayoutMetrics(
12781289
unsigned int newWidth = static_cast<unsigned int>(layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor);
12791290
unsigned int newHeight = static_cast<unsigned int>(layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor);
12801291

1292+
if (newHeight != m_imgHeight || oldLayoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor) {
1293+
m_recalculateContentVerticalOffset = true;
1294+
}
1295+
12811296
if (newWidth != m_imgWidth || newHeight != m_imgHeight) {
12821297
m_drawingSurface = nullptr; // Invalidate surface if we get a size change
12831298
}
@@ -1416,6 +1431,8 @@ void WindowsTextInputComponentView::FinalizeUpdates(
14161431

14171432
void WindowsTextInputComponentView::UpdatePropertyBits() noexcept {
14181433
if (m_propBitsMask != 0) {
1434+
if ((m_propBits & TXTBIT_CHARFORMATCHANGE) == TXTBIT_CHARFORMATCHANGE)
1435+
m_recalculateContentVerticalOffset = true;
14191436
DrawBlock db(*this);
14201437
winrt::check_hresult(m_textServices->OnTxPropertyBitsChange(m_propBitsMask, m_propBits));
14211438
m_propBitsMask = 0;
@@ -1427,6 +1444,10 @@ void WindowsTextInputComponentView::InternalFinalize() noexcept {
14271444
if (m_mounted) {
14281445
UpdatePropertyBits();
14291446

1447+
if (m_recalculateContentVerticalOffset) {
1448+
calculateContentVerticalOffset();
1449+
}
1450+
14301451
ensureDrawingSurface();
14311452
if (m_needsRedraw) {
14321453
DrawText();
@@ -1488,12 +1509,6 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept {
14881509
// m_crText = RemoveAlpha(fontDetails.FontColor);
14891510
// }
14901511

1491-
// set font face
1492-
// cfNew.dwMask |= CFM_FACE;
1493-
// NetUIWzCchCopy(cfNew.szFaceName, _countof(cfNew.szFaceName), fontDetails.FontName.c_str());
1494-
// cfNew.bPitchAndFamily = FF_DONTCARE;
1495-
1496-
// set font size -- 15 to convert twips to pt
14971512
const auto &props = windowsTextInputProps();
14981513
float fontSize =
14991514
(std::isnan(props.textAttributes.fontSize) ? facebook::react::TextAttributes::defaultTextAttributes().fontSize
@@ -1504,8 +1519,8 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept {
15041519
fontSize *=
15051520
(maxFontSizeMultiplier >= 1.0f) ? std::min(maxFontSizeMultiplier, m_fontSizeMultiplier) : m_fontSizeMultiplier;
15061521

1507-
// TODO get fontSize from props.textAttributes, or defaultTextAttributes, or fragment?
15081522
cfNew.dwMask |= CFM_SIZE;
1523+
// set font size -- 15 to convert twips to pt
15091524
cfNew.yHeight = static_cast<LONG>(fontSize * 15);
15101525

15111526
// set bold
@@ -1532,7 +1547,11 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept {
15321547
std::wstring fontFamily =
15331548
std::wstring(props.textAttributes.fontFamily.begin(), props.textAttributes.fontFamily.end());
15341549
wcsncpy_s(cfNew.szFaceName, fontFamily.c_str(), LF_FACESIZE);
1550+
} else {
1551+
cfNew.dwMask |= CFM_FACE;
1552+
wcsncpy_s(cfNew.szFaceName, L"Segoe UI\0", LF_FACESIZE);
15351553
}
1554+
cfNew.bPitchAndFamily = FF_DONTCARE;
15361555

15371556
// set char offset
15381557
cfNew.dwMask |= CFM_OFFSET;
@@ -1541,7 +1560,8 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept {
15411560
// set letter spacing
15421561
float letterSpacing = props.textAttributes.letterSpacing;
15431562
if (!std::isnan(letterSpacing)) {
1544-
updateLetterSpacing(letterSpacing);
1563+
cfNew.dwMask |= CFM_SPACING;
1564+
cfNew.sSpacing = static_cast<SHORT>(letterSpacing * 20); // Convert to TWIPS
15451565
}
15461566

15471567
// set charset
@@ -1657,7 +1677,7 @@ winrt::com_ptr<::IDWriteTextLayout> WindowsTextInputComponentView::CreatePlaceho
16571677
const auto &props = windowsTextInputProps();
16581678
facebook::react::TextAttributes textAttributes = props.textAttributes;
16591679
if (std::isnan(props.textAttributes.fontSize)) {
1660-
textAttributes.fontSize = 12.0f;
1680+
facebook::react::TextAttributes::defaultTextAttributes().fontSize;
16611681
}
16621682
textAttributes.fontSizeMultiplier = m_fontSizeMultiplier;
16631683
fragment1.string = props.placeholder;
@@ -1674,6 +1694,26 @@ winrt::com_ptr<::IDWriteTextLayout> WindowsTextInputComponentView::CreatePlaceho
16741694
return textLayout;
16751695
}
16761696

1697+
void WindowsTextInputComponentView::calculateContentVerticalOffset() noexcept {
1698+
m_recalculateContentVerticalOffset = false;
1699+
1700+
const auto &props = windowsTextInputProps();
1701+
1702+
m_contentOffsetPx = {
1703+
static_cast<LONG>(m_layoutMetrics.contentInsets.left * m_layoutMetrics.pointScaleFactor),
1704+
static_cast<LONG>(m_layoutMetrics.contentInsets.top * m_layoutMetrics.pointScaleFactor)};
1705+
1706+
if (props.multiline) {
1707+
// Align to the top for multiline
1708+
return;
1709+
}
1710+
1711+
auto [contentWidth, contentHeight] = GetContentSize();
1712+
1713+
m_contentOffsetPx.y += static_cast<LONG>(std::round(
1714+
((m_layoutMetrics.getContentFrame().size.height - contentHeight) / 2) * m_layoutMetrics.pointScaleFactor));
1715+
}
1716+
16771717
void WindowsTextInputComponentView::DrawText() noexcept {
16781718
m_needsRedraw = true;
16791719
if (m_cDrawBlock || theme()->IsEmpty() || !m_textServices) {
@@ -1699,16 +1739,13 @@ void WindowsTextInputComponentView::DrawText() noexcept {
16991739
assert(d2dDeviceContext->GetUnitMode() == D2D1_UNIT_MODE_DIPS);
17001740

17011741
RECTL rc{
1702-
static_cast<LONG>(offset.x),
1703-
static_cast<LONG>(offset.y),
1704-
static_cast<LONG>(offset.x) + static_cast<LONG>(m_imgWidth),
1705-
static_cast<LONG>(offset.y) + static_cast<LONG>(m_imgHeight)};
1742+
offset.x + m_contentOffsetPx.x,
1743+
offset.y + m_contentOffsetPx.y,
1744+
offset.x + m_contentOffsetPx.x + static_cast<LONG>(m_imgWidth),
1745+
offset.y + m_contentOffsetPx.y + static_cast<LONG>(m_imgHeight)};
17061746

17071747
RECT rcClient{
1708-
static_cast<LONG>(offset.x),
1709-
static_cast<LONG>(offset.y),
1710-
static_cast<LONG>(offset.x) + static_cast<LONG>(m_imgWidth),
1711-
static_cast<LONG>(offset.y) + static_cast<LONG>(m_imgHeight)};
1748+
offset.x, offset.y, offset.x + static_cast<LONG>(m_imgWidth), offset.y + static_cast<LONG>(m_imgHeight)};
17121749

17131750
{
17141751
m_cDrawBlock++; // Dont use AutoDrawBlock as we are already in draw, and dont need to draw again.
@@ -1763,8 +1800,8 @@ void WindowsTextInputComponentView::DrawText() noexcept {
17631800
// draw text
17641801
d2dDeviceContext->DrawTextLayout(
17651802
D2D1::Point2F(
1766-
static_cast<FLOAT>((offset.x + m_layoutMetrics.contentInsets.left) / m_layoutMetrics.pointScaleFactor),
1767-
static_cast<FLOAT>((offset.y + m_layoutMetrics.contentInsets.top) / m_layoutMetrics.pointScaleFactor)),
1803+
static_cast<FLOAT>(offset.x + m_contentOffsetPx.x) / m_layoutMetrics.pointScaleFactor,
1804+
static_cast<FLOAT>(offset.y + m_contentOffsetPx.y) / m_layoutMetrics.pointScaleFactor),
17681805
textLayout.get(),
17691806
brush.get(),
17701807
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT);
@@ -1840,22 +1877,6 @@ void WindowsTextInputComponentView::autoCapitalizeOnUpdateProps(
18401877
}
18411878
}
18421879

1843-
void WindowsTextInputComponentView::updateLetterSpacing(float letterSpacing) noexcept {
1844-
CHARFORMAT2W cf = {};
1845-
cf.cbSize = sizeof(CHARFORMAT2W);
1846-
cf.dwMask = CFM_SPACING;
1847-
cf.sSpacing = static_cast<SHORT>(letterSpacing * 20); // Convert to TWIPS
1848-
1849-
LRESULT res;
1850-
1851-
// Apply to all existing text like placeholder
1852-
winrt::check_hresult(m_textServices->TxSendMessage(EM_SETCHARFORMAT, SCF_ALL, reinterpret_cast<LPARAM>(&cf), &res));
1853-
1854-
// Apply to future text input
1855-
winrt::check_hresult(
1856-
m_textServices->TxSendMessage(EM_SETCHARFORMAT, SCF_SELECTION, reinterpret_cast<LPARAM>(&cf), &res));
1857-
}
1858-
18591880
void WindowsTextInputComponentView::updateAutoCorrect(bool enable) noexcept {
18601881
LRESULT lresult;
18611882
winrt::check_hresult(m_textServices->TxSendMessage(

vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ struct WindowsTextInputComponentView
117117
const std::string &previousCapitalizationType,
118118
const std::string &newcapitalizationType) noexcept;
119119

120-
void updateLetterSpacing(float letterSpacing) noexcept;
121120
void updateAutoCorrect(bool value) noexcept;
122121
void updateSpellCheck(bool value) noexcept;
123122
void ShowContextMenu(const winrt::Windows::Foundation::Point &position) noexcept;
123+
void calculateContentVerticalOffset() noexcept;
124124

125125
winrt::Windows::UI::Composition::CompositionSurfaceBrush m_brush{nullptr};
126126
winrt::Microsoft::ReactNative::Composition::Experimental::ICaretVisual m_caretVisual{nullptr};
@@ -145,6 +145,9 @@ struct WindowsTextInputComponentView
145145
bool m_hasFocus{false};
146146
bool m_clearTextOnSubmit{false};
147147
bool m_multiline{false};
148+
LONG m_contentVerticalOffsetPx{0}; // Used to center single line text within the client rect
149+
bool m_recalculateContentVerticalOffset{true};
150+
POINT m_contentOffsetPx{0, 0};
148151
DWORD m_propBitsMask{0};
149152
DWORD m_propBits{0};
150153
HCURSOR m_hcursor{nullptr};

vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ void WindowsTextLayoutManager::GetTextLayout(
9696

9797
winrt::com_ptr<IDWriteTextFormat> spTextFormat;
9898

99-
float fontSizeText = outerFragment.textAttributes.fontSize;
99+
float fontSizeText =
100+
(std::isnan(outerFragment.textAttributes.fontSize)
101+
? facebook::react::TextAttributes::defaultTextAttributes().fontSize
102+
: outerFragment.textAttributes.fontSize);
100103
if (outerFragment.textAttributes.allowFontScaling.value_or(true) &&
101104
!std::isnan(outerFragment.textAttributes.fontSizeMultiplier)) {
102105
float maxFontSizeMultiplierText = cDefaultMaxFontSizeMultiplier;
@@ -287,7 +290,9 @@ void WindowsTextLayoutManager::GetTextLayout(
287290
maxFontSizeMultiplier =
288291
(!std::isnan(attributes.maxFontSizeMultiplier) ? attributes.maxFontSizeMultiplier
289292
: cDefaultMaxFontSizeMultiplier);
290-
float fontSize = attributes.fontSize;
293+
float fontSize =
294+
(std::isnan(attributes.fontSize) ? facebook::react::TextAttributes::defaultTextAttributes().fontSize
295+
: attributes.fontSize);
291296
if (attributes.allowFontScaling.value_or(true) && (!std::isnan(attributes.fontSizeMultiplier))) {
292297
fontSize *= (maxFontSizeMultiplier >= 1.0f) ? std::min(maxFontSizeMultiplier, attributes.fontSizeMultiplier)
293298
: attributes.fontSizeMultiplier;

0 commit comments

Comments
 (0)