diff --git a/change/@office-iss-react-native-win32-0aa51800-3890-481f-88db-5b01c087e4e2.json b/change/@office-iss-react-native-win32-0aa51800-3890-481f-88db-5b01c087e4e2.json new file mode 100644 index 00000000000..177543c1231 --- /dev/null +++ b/change/@office-iss-react-native-win32-0aa51800-3890-481f-88db-5b01c087e4e2.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Update require's from TouchableWin32 to be imports", + "packageName": "@office-iss/react-native-win32", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-aab9fc58-e498-4310-bd8e-f341801827a4.json b/change/react-native-windows-aab9fc58-e498-4310-bd8e-f341801827a4.json new file mode 100644 index 00000000000..c94dba63816 --- /dev/null +++ b/change/react-native-windows-aab9fc58-e498-4310-bd8e-f341801827a4.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Center single line textinputs", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/packages/@office-iss/react-native-win32/src-win/Libraries/Components/Touchable/TouchableWin32.tsx b/packages/@office-iss/react-native-win32/src-win/Libraries/Components/Touchable/TouchableWin32.tsx index 3e5371419f2..443a06480ec 100644 --- a/packages/@office-iss/react-native-win32/src-win/Libraries/Components/Touchable/TouchableWin32.tsx +++ b/packages/@office-iss/react-native-win32/src-win/Libraries/Components/Touchable/TouchableWin32.tsx @@ -27,10 +27,13 @@ import { } from './TouchableWin32.Types'; import { IKeyboardEvent } from '../View/ViewPropTypes'; -const BoundingDimensions = require('./BoundingDimensions'); -const Position = require('./Position'); +// @ts-ignore +import BoundingDimensions from './BoundingDimensions'; +// @ts-ignore +import Position from './Position'; -const {findNodeHandle} = require('../../ReactNative/RendererProxy'); +// @ts-ignore +import {findNodeHandle} from '../../ReactNative/RendererProxy'; /** * Extracts a single touch, generally this is the active touch or touch that diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap index 9281d259c6e..46f69a00b16 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TextInputComponentTest.test.ts.snap @@ -381,9 +381,9 @@ exports[`TextInput Tests Text have cursorColor 1`] = ` "Brush Type": "ColorBrush", "Color": "rgba(0, 128, 0, 255)", }, - "Offset": "83, 5, 0", + "Offset": "89, 6, 0", "Opacity": 0, - "Size": "1, 19", + "Size": "1, 21", "Visual Type": "SpriteVisual", }, ], @@ -1884,9 +1884,9 @@ exports[`TextInput Tests TextInputs can clear on submit 1`] = ` "Brush Type": "ColorBrush", "Color": "rgba(0, 0, 0, 255)", }, - "Offset": "5, 5, 0", + "Offset": "5, 6, 0", "Opacity": 0, - "Size": "1, 19", + "Size": "1, 21", "Visual Type": "SpriteVisual", }, ], @@ -2805,9 +2805,9 @@ exports[`TextInput Tests TextInputs can have caretHidden 1`] = ` "Brush Type": "ColorBrush", "Color": "rgba(0, 0, 0, 255)", }, - "Offset": "83, 5, 0", + "Offset": "89, 6, 0", "Opacity": 0, - "Size": "1, 19", + "Size": "1, 21", "Visual Type": "SpriteVisual", }, ], @@ -4993,9 +4993,9 @@ exports[`TextInput Tests TextInputs can select text on focus 1`] = ` "Brush Type": "ColorBrush", "Color": "rgba(0, 0, 0, 255)", }, - "Offset": "83, 5, 0", + "Offset": "89, 6, 0", "Opacity": 0, - "Size": "1, 19", + "Size": "1, 21", "Visual Type": "SpriteVisual", }, ], @@ -5226,7 +5226,7 @@ exports[`TextInput Tests TextInputs can submit with custom key, multilined and s }, "Offset": "5, 5, 0", "Opacity": 0, - "Size": "1, 19", + "Size": "1, 21", "Visual Type": "SpriteVisual", }, ], diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index 0f4b8a966f8..843c42e8d2f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -246,6 +246,8 @@ struct CompTextHost : public winrt::implements { //@cmember Converts screen coordinates of a specified point to the client coordinates BOOL TxScreenToClient(LPPOINT lppt) override { winrt::Windows::Foundation::Point pt{static_cast(lppt->x), static_cast(lppt->y)}; + pt.X -= m_outer->m_contentOffsetPx.x; + pt.Y -= m_outer->m_contentOffsetPx.y; auto localpt = m_outer->ScreenToLocal(pt); lppt->x = static_cast(localpt.X); lppt->y = static_cast(localpt.Y); @@ -255,9 +257,14 @@ struct CompTextHost : public winrt::implements { //@cmember Converts the client coordinates of a specified point to screen coordinates BOOL TxClientToScreen(LPPOINT lppt) override { winrt::Windows::Foundation::Point pt{static_cast(lppt->x), static_cast(lppt->y)}; + + if (!m_outer->m_parent) { + return false; + } + auto screenpt = m_outer->LocalToScreen(pt); - lppt->x = static_cast(screenpt.X); - lppt->y = static_cast(screenpt.Y); + lppt->x = static_cast(screenpt.X) + m_outer->m_contentOffsetPx.x; + lppt->y = static_cast(screenpt.Y) + m_outer->m_contentOffsetPx.y; return true; } @@ -276,20 +283,25 @@ struct CompTextHost : public winrt::implements { //@cmember Retrieves the coordinates of a window's client area HRESULT TxGetClientRect(LPRECT prc) override { *prc = m_outer->getClientRect(); + + prc->top += m_outer->m_contentOffsetPx.y; + prc->bottom += m_outer->m_contentOffsetPx.y - + static_cast(m_outer->m_layoutMetrics.contentInsets.bottom * m_outer->m_layoutMetrics.pointScaleFactor); + prc->left += m_outer->m_contentOffsetPx.x; + prc->right += m_outer->m_contentOffsetPx.x - + static_cast(m_outer->m_layoutMetrics.contentInsets.right * m_outer->m_layoutMetrics.pointScaleFactor); + return S_OK; } //@cmember Get the view rectangle relative to the inset HRESULT TxGetViewInset(LPRECT prc) override { // Inset is in HIMETRIC - constexpr float HmPerInchF = 2540.0f; - constexpr float PointsPerInch = 96.0f; - constexpr float dipToHm = HmPerInchF / PointsPerInch; - prc->left = static_cast(m_outer->m_layoutMetrics.contentInsets.left * dipToHm); - prc->top = static_cast(m_outer->m_layoutMetrics.contentInsets.top * dipToHm); - prc->bottom = static_cast(m_outer->m_layoutMetrics.contentInsets.bottom * dipToHm); - prc->right = static_cast(m_outer->m_layoutMetrics.contentInsets.right * dipToHm); + prc->left = 0; + prc->top = 0; + prc->bottom = 0; + prc->right = 0; return NOERROR; } @@ -492,11 +504,6 @@ AutoCorrectOffCallback(LANGID langid, const WCHAR *pszBefore, WCHAR *pszAfter, L facebook::react::AttributedString WindowsTextInputComponentView::getAttributedString() const { // Use BaseTextShadowNode to get attributed string from children - auto childTextAttributes = facebook::react::TextAttributes::defaultTextAttributes(); - childTextAttributes.fontSizeMultiplier = m_fontSizeMultiplier; - - childTextAttributes.apply(windowsTextInputProps().textAttributes); - auto attributedString = facebook::react::AttributedString{}; // auto attachments = facebook::react::BaseTextShadowNode::Attachments{}; @@ -1114,6 +1121,9 @@ void WindowsTextInputComponentView::updateProps( !facebook::react::floatEquality( oldTextInputProps.textAttributes.letterSpacing, newTextInputProps.textAttributes.letterSpacing) || oldTextInputProps.textAttributes.fontFamily != newTextInputProps.textAttributes.fontFamily || + oldTextInputProps.textAttributes.fontStyle != newTextInputProps.textAttributes.fontStyle || + oldTextInputProps.textAttributes.textDecorationLineType != + newTextInputProps.textAttributes.textDecorationLineType || !facebook::react::floatEquality( oldTextInputProps.textAttributes.maxFontSizeMultiplier, newTextInputProps.textAttributes.maxFontSizeMultiplier)) { @@ -1129,6 +1139,7 @@ void WindowsTextInputComponentView::updateProps( } if (oldTextInputProps.multiline != newTextInputProps.multiline) { + m_recalculateContentVerticalOffset = true; m_multiline = newTextInputProps.multiline; m_propBitsMask |= TXTBIT_MULTILINE | TXTBIT_WORDWRAP; if (newTextInputProps.multiline) { @@ -1278,6 +1289,10 @@ void WindowsTextInputComponentView::updateLayoutMetrics( unsigned int newWidth = static_cast(layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor); unsigned int newHeight = static_cast(layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor); + if (newHeight != m_imgHeight || oldLayoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor) { + m_recalculateContentVerticalOffset = true; + } + if (newWidth != m_imgWidth || newHeight != m_imgHeight) { m_drawingSurface = nullptr; // Invalidate surface if we get a size change } @@ -1416,6 +1431,8 @@ void WindowsTextInputComponentView::FinalizeUpdates( void WindowsTextInputComponentView::UpdatePropertyBits() noexcept { if (m_propBitsMask != 0) { + if ((m_propBits & TXTBIT_CHARFORMATCHANGE) == TXTBIT_CHARFORMATCHANGE) + m_recalculateContentVerticalOffset = true; DrawBlock db(*this); winrt::check_hresult(m_textServices->OnTxPropertyBitsChange(m_propBitsMask, m_propBits)); m_propBitsMask = 0; @@ -1427,6 +1444,10 @@ void WindowsTextInputComponentView::InternalFinalize() noexcept { if (m_mounted) { UpdatePropertyBits(); + if (m_recalculateContentVerticalOffset) { + calculateContentVerticalOffset(); + } + ensureDrawingSurface(); if (m_needsRedraw) { DrawText(); @@ -1488,12 +1509,6 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept { // m_crText = RemoveAlpha(fontDetails.FontColor); // } - // set font face - // cfNew.dwMask |= CFM_FACE; - // NetUIWzCchCopy(cfNew.szFaceName, _countof(cfNew.szFaceName), fontDetails.FontName.c_str()); - // cfNew.bPitchAndFamily = FF_DONTCARE; - - // set font size -- 15 to convert twips to pt const auto &props = windowsTextInputProps(); float fontSize = (std::isnan(props.textAttributes.fontSize) ? facebook::react::TextAttributes::defaultTextAttributes().fontSize @@ -1504,8 +1519,8 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept { fontSize *= (maxFontSizeMultiplier >= 1.0f) ? std::min(maxFontSizeMultiplier, m_fontSizeMultiplier) : m_fontSizeMultiplier; - // TODO get fontSize from props.textAttributes, or defaultTextAttributes, or fragment? cfNew.dwMask |= CFM_SIZE; + // set font size -- 15 to convert twips to pt cfNew.yHeight = static_cast(fontSize * 15); // set bold @@ -1513,18 +1528,26 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept { cfNew.wWeight = props.textAttributes.fontWeight ? static_cast(*props.textAttributes.fontWeight) : DWRITE_FONT_WEIGHT_NORMAL; - // set font style - // cfNew.dwMask |= (CFM_ITALIC | CFM_STRIKEOUT | CFM_UNDERLINE); - // int dFontStyle = fontDetails.FontStyle; - // if (dFontStyle & FS_Italic) { - // cfNew.dwEffects |= CFE_ITALIC; - // } - // if (dFontStyle & FS_StrikeOut) { - // cfNew.dwEffects |= CFE_STRIKEOUT; - //} - // if (dFontStyle & FS_Underline) { - // cfNew.dwEffects |= CFE_UNDERLINE; - // } + // set font style (italic) + cfNew.dwMask |= CFM_ITALIC; + if (props.textAttributes.fontStyle == facebook::react::FontStyle::Italic || + props.textAttributes.fontStyle == facebook::react::FontStyle::Oblique) { + cfNew.dwEffects |= CFE_ITALIC; + } + + // set text decoration (underline and strikethrough) + cfNew.dwMask |= (CFM_UNDERLINE | CFM_STRIKEOUT); + if (props.textAttributes.textDecorationLineType.has_value()) { + auto decorationType = *props.textAttributes.textDecorationLineType; + if (decorationType == facebook::react::TextDecorationLineType::Underline || + decorationType == facebook::react::TextDecorationLineType::UnderlineStrikethrough) { + cfNew.dwEffects |= CFE_UNDERLINE; + } + if (decorationType == facebook::react::TextDecorationLineType::Strikethrough || + decorationType == facebook::react::TextDecorationLineType::UnderlineStrikethrough) { + cfNew.dwEffects |= CFE_STRIKEOUT; + } + } // set font family if (!props.textAttributes.fontFamily.empty()) { @@ -1532,7 +1555,11 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept { std::wstring fontFamily = std::wstring(props.textAttributes.fontFamily.begin(), props.textAttributes.fontFamily.end()); wcsncpy_s(cfNew.szFaceName, fontFamily.c_str(), LF_FACESIZE); + } else { + cfNew.dwMask |= CFM_FACE; + wcsncpy_s(cfNew.szFaceName, L"Segoe UI\0", LF_FACESIZE); } + cfNew.bPitchAndFamily = FF_DONTCARE; // set char offset cfNew.dwMask |= CFM_OFFSET; @@ -1541,7 +1568,8 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept { // set letter spacing float letterSpacing = props.textAttributes.letterSpacing; if (!std::isnan(letterSpacing)) { - updateLetterSpacing(letterSpacing); + cfNew.dwMask |= CFM_SPACING; + cfNew.sSpacing = static_cast(letterSpacing * 20); // Convert to TWIPS } // set charset @@ -1657,7 +1685,7 @@ winrt::com_ptr<::IDWriteTextLayout> WindowsTextInputComponentView::CreatePlaceho const auto &props = windowsTextInputProps(); facebook::react::TextAttributes textAttributes = props.textAttributes; if (std::isnan(props.textAttributes.fontSize)) { - textAttributes.fontSize = 12.0f; + facebook::react::TextAttributes::defaultTextAttributes().fontSize; } textAttributes.fontSizeMultiplier = m_fontSizeMultiplier; fragment1.string = props.placeholder; @@ -1674,6 +1702,26 @@ winrt::com_ptr<::IDWriteTextLayout> WindowsTextInputComponentView::CreatePlaceho return textLayout; } +void WindowsTextInputComponentView::calculateContentVerticalOffset() noexcept { + m_recalculateContentVerticalOffset = false; + + const auto &props = windowsTextInputProps(); + + m_contentOffsetPx = { + static_cast(m_layoutMetrics.contentInsets.left * m_layoutMetrics.pointScaleFactor), + static_cast(m_layoutMetrics.contentInsets.top * m_layoutMetrics.pointScaleFactor)}; + + if (props.multiline) { + // Align to the top for multiline + return; + } + + auto [contentWidth, contentHeight] = GetContentSize(); + + m_contentOffsetPx.y += static_cast(std::round( + ((m_layoutMetrics.getContentFrame().size.height - contentHeight) / 2) * m_layoutMetrics.pointScaleFactor)); +} + void WindowsTextInputComponentView::DrawText() noexcept { m_needsRedraw = true; if (m_cDrawBlock || theme()->IsEmpty() || !m_textServices) { @@ -1699,16 +1747,13 @@ void WindowsTextInputComponentView::DrawText() noexcept { assert(d2dDeviceContext->GetUnitMode() == D2D1_UNIT_MODE_DIPS); RECTL rc{ - static_cast(offset.x), - static_cast(offset.y), - static_cast(offset.x) + static_cast(m_imgWidth), - static_cast(offset.y) + static_cast(m_imgHeight)}; + offset.x + m_contentOffsetPx.x, + offset.y + m_contentOffsetPx.y, + offset.x + m_contentOffsetPx.x + static_cast(m_imgWidth), + offset.y + m_contentOffsetPx.y + static_cast(m_imgHeight)}; RECT rcClient{ - static_cast(offset.x), - static_cast(offset.y), - static_cast(offset.x) + static_cast(m_imgWidth), - static_cast(offset.y) + static_cast(m_imgHeight)}; + offset.x, offset.y, offset.x + static_cast(m_imgWidth), offset.y + static_cast(m_imgHeight)}; { m_cDrawBlock++; // Dont use AutoDrawBlock as we are already in draw, and dont need to draw again. @@ -1763,8 +1808,8 @@ void WindowsTextInputComponentView::DrawText() noexcept { // draw text d2dDeviceContext->DrawTextLayout( D2D1::Point2F( - static_cast((offset.x + m_layoutMetrics.contentInsets.left) / m_layoutMetrics.pointScaleFactor), - static_cast((offset.y + m_layoutMetrics.contentInsets.top) / m_layoutMetrics.pointScaleFactor)), + static_cast(offset.x + m_contentOffsetPx.x) / m_layoutMetrics.pointScaleFactor, + static_cast(offset.y + m_contentOffsetPx.y) / m_layoutMetrics.pointScaleFactor), textLayout.get(), brush.get(), D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); @@ -1840,22 +1885,6 @@ void WindowsTextInputComponentView::autoCapitalizeOnUpdateProps( } } -void WindowsTextInputComponentView::updateLetterSpacing(float letterSpacing) noexcept { - CHARFORMAT2W cf = {}; - cf.cbSize = sizeof(CHARFORMAT2W); - cf.dwMask = CFM_SPACING; - cf.sSpacing = static_cast(letterSpacing * 20); // Convert to TWIPS - - LRESULT res; - - // Apply to all existing text like placeholder - winrt::check_hresult(m_textServices->TxSendMessage(EM_SETCHARFORMAT, SCF_ALL, reinterpret_cast(&cf), &res)); - - // Apply to future text input - winrt::check_hresult( - m_textServices->TxSendMessage(EM_SETCHARFORMAT, SCF_SELECTION, reinterpret_cast(&cf), &res)); -} - void WindowsTextInputComponentView::updateAutoCorrect(bool enable) noexcept { LRESULT lresult; winrt::check_hresult(m_textServices->TxSendMessage( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index 634b5884ab9..d9915d6acc6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -117,10 +117,10 @@ struct WindowsTextInputComponentView const std::string &previousCapitalizationType, const std::string &newcapitalizationType) noexcept; - void updateLetterSpacing(float letterSpacing) noexcept; void updateAutoCorrect(bool value) noexcept; void updateSpellCheck(bool value) noexcept; void ShowContextMenu(const winrt::Windows::Foundation::Point &position) noexcept; + void calculateContentVerticalOffset() noexcept; winrt::Windows::UI::Composition::CompositionSurfaceBrush m_brush{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::ICaretVisual m_caretVisual{nullptr}; @@ -145,6 +145,9 @@ struct WindowsTextInputComponentView bool m_hasFocus{false}; bool m_clearTextOnSubmit{false}; bool m_multiline{false}; + LONG m_contentVerticalOffsetPx{0}; // Used to center single line text within the client rect + bool m_recalculateContentVerticalOffset{true}; + POINT m_contentOffsetPx{0, 0}; DWORD m_propBitsMask{0}; DWORD m_propBits{0}; HCURSOR m_hcursor{nullptr}; diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp index 082c44faacd..c9fd6c8e9f8 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp @@ -96,7 +96,10 @@ void WindowsTextLayoutManager::GetTextLayout( winrt::com_ptr spTextFormat; - float fontSizeText = outerFragment.textAttributes.fontSize; + float fontSizeText = + (std::isnan(outerFragment.textAttributes.fontSize) + ? facebook::react::TextAttributes::defaultTextAttributes().fontSize + : outerFragment.textAttributes.fontSize); if (outerFragment.textAttributes.allowFontScaling.value_or(true) && !std::isnan(outerFragment.textAttributes.fontSizeMultiplier)) { float maxFontSizeMultiplierText = cDefaultMaxFontSizeMultiplier; @@ -287,7 +290,9 @@ void WindowsTextLayoutManager::GetTextLayout( maxFontSizeMultiplier = (!std::isnan(attributes.maxFontSizeMultiplier) ? attributes.maxFontSizeMultiplier : cDefaultMaxFontSizeMultiplier); - float fontSize = attributes.fontSize; + float fontSize = + (std::isnan(attributes.fontSize) ? facebook::react::TextAttributes::defaultTextAttributes().fontSize + : attributes.fontSize); if (attributes.allowFontScaling.value_or(true) && (!std::isnan(attributes.fontSizeMultiplier))) { fontSize *= (maxFontSizeMultiplier >= 1.0f) ? std::min(maxFontSizeMultiplier, attributes.fontSizeMultiplier) : attributes.fontSizeMultiplier;