diff --git a/change/react-native-windows-b828ab6b-3220-4d51-b6c9-3db3b91887c1.json b/change/react-native-windows-b828ab6b-3220-4d51-b6c9-3db3b91887c1.json new file mode 100644 index 00000000000..1be214f37b8 --- /dev/null +++ b/change/react-native-windows-b828ab6b-3220-4d51-b6c9-3db3b91887c1.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "[Fabric] LayoutDirection and FontSizeMultiplier support", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp index 7179a0329e3..1b3c1a05bd3 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp @@ -51,6 +51,7 @@ void UpdateRootViewSizeToAppWindow( if (window.Presenter().as().State() != winrt::Microsoft::UI::Windowing::OverlappedPresenterState::Minimized) { winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; constraints.MaximumSize = constraints.MinimumSize = size; rootView.Arrange(constraints, {0, 0}); } diff --git a/packages/playground/windows/playground-composition/Playground-Composition.cpp b/packages/playground/windows/playground-composition/Playground-Composition.cpp index 871638cc64b..e1d010fcd62 100644 --- a/packages/playground/windows/playground-composition/Playground-Composition.cpp +++ b/packages/playground/windows/playground-composition/Playground-Composition.cpp @@ -128,6 +128,7 @@ struct WindowData { winrt::Microsoft::ReactNative::ReactInstanceSettings m_instanceSettings{nullptr}; bool m_useLiftedComposition{true}; bool m_sizeToContent{false}; + bool m_forceRTL{false}; winrt::Windows::UI::Composition::Desktop::DesktopWindowTarget m_target{nullptr}; LONG m_height{0}; LONG m_width{0}; @@ -255,6 +256,10 @@ struct WindowData { m_bridge = winrt::Microsoft::UI::Content::DesktopChildSiteBridge::Create( g_liftedCompositor, winrt::Microsoft::UI::GetWindowIdFromWindow(hwnd)); + if (m_forceRTL) { + m_bridge.LayoutDirectionOverride(winrt::Microsoft::UI::Content::ContentLayoutDirection::RightToLeft); + } + auto appContent = m_compRootView.Island(); m_bridge.Connect(appContent); @@ -262,7 +267,7 @@ struct WindowData { m_compRootView.ScaleFactor(ScaleFactor(hwnd)); winrt::Microsoft::ReactNative::LayoutConstraints constraints; - constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::LeftToRight; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; constraints.MaximumSize = constraints.MinimumSize = {m_width / ScaleFactor(hwnd), m_height / ScaleFactor(hwnd)}; @@ -327,6 +332,7 @@ struct WindowData { SystemCompositionContextHelper::CreateVisual(root)); m_compRootView.ScaleFactor(ScaleFactor(hwnd)); winrt::Microsoft::ReactNative::LayoutConstraints contraints; + contraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; contraints.MaximumSize = contraints.MinimumSize = {m_width / ScaleFactor(hwnd), m_height / ScaleFactor(hwnd)}; m_compRootView.Arrange(contraints, {0, 0}); @@ -371,8 +377,10 @@ struct WindowData { OutputDebugStringA("Instance Unload completed\n"); uidispatch.Post([&]() { - m_bridge.Close(); - m_bridge = nullptr; + if (m_bridge) { + m_bridge.Close(); + m_bridge = nullptr; + } }); assert(asyncStatus == winrt::Windows::Foundation::AsyncStatus::Completed); }); @@ -380,6 +388,14 @@ struct WindowData { m_instanceSettings = nullptr; m_host = nullptr; } break; + case IDM_TOGGLE_LAYOUT_DIRECTION: { + if (m_bridge) { + m_bridge.LayoutDirectionOverride( + (m_forceRTL) ? winrt::Microsoft::UI::Content::ContentLayoutDirection::LeftToRight + : winrt::Microsoft::UI::Content::ContentLayoutDirection::RightToLeft); + } + m_forceRTL = !m_forceRTL; + } } return 0; @@ -400,7 +416,7 @@ struct WindowData { winrt::Windows::Foundation::Size size{m_width / ScaleFactor(hwnd), m_height / ScaleFactor(hwnd)}; if (!IsIconic(hwnd)) { winrt::Microsoft::ReactNative::LayoutConstraints constraints; - constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::LeftToRight; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; constraints.MinimumSize = constraints.MaximumSize = size; if (m_sizeToContent) { ApplyConstraintsForContentSizedWindow(constraints); diff --git a/packages/playground/windows/playground-composition/Playground-Composition.rc b/packages/playground/windows/playground-composition/Playground-Composition.rc index cff2595c7bd..adbcf9460ac 100644 --- a/packages/playground/windows/playground-composition/Playground-Composition.rc +++ b/packages/playground/windows/playground-composition/Playground-Composition.rc @@ -58,6 +58,7 @@ BEGIN MENUITEM "&New Window\tCtrl+N", IDM_NEWWINDOW MENUITEM "&Refresh\tF5", IDM_REFRESH MENUITEM "&Unload", IDM_UNLOAD + MENUITEM "Toggle Layout &Direction", IDM_TOGGLE_LAYOUT_DIRECTION MENUITEM SEPARATOR MENUITEM "&Settings...\tAlt+S", IDM_SETTINGS MENUITEM SEPARATOR diff --git a/packages/playground/windows/playground-composition/resource.h b/packages/playground/windows/playground-composition/resource.h index a542b5251d9..ead109499da 100644 --- a/packages/playground/windows/playground-composition/resource.h +++ b/packages/playground/windows/playground-composition/resource.h @@ -25,6 +25,7 @@ #define IDC_JSENGINELABEL 111 #define IDC_SIZETOCONTENT 112 #define IDM_UNLOAD 113 +#define IDM_TOGGLE_LAYOUT_DIRECTION 114 #define IDI_ICON1 1008 // Next default values for new objects diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp index 59f310e84b6..0f5a12f6435 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp @@ -262,8 +262,8 @@ void ComponentView::HandleCommand(const winrt::Microsoft::ReactNative::HandleCom } } -winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView * -ComponentView::rootComponentView() noexcept { +winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *ComponentView::rootComponentView() + const noexcept { if (m_rootView) return m_rootView; diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index b0bae54934f..540a2714b85 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -93,7 +93,8 @@ struct ComponentView : public ComponentViewT { facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept; virtual void prepareForRecycle() noexcept; virtual facebook::react::Props::Shared props() noexcept; - virtual winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *rootComponentView() noexcept; + virtual winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *rootComponentView() + const noexcept; virtual void parent(const winrt::Microsoft::ReactNative::ComponentView &parent) noexcept; virtual winrt::Microsoft::ReactNative::ComponentView Parent() const noexcept; virtual winrt::IVectorView Children() const noexcept; @@ -253,7 +254,7 @@ struct ComponentView : public ComponentViewT { bool m_mounted : 1 {false}; const facebook::react::Tag m_tag; winrt::IInspectable m_userData; - winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *m_rootView{nullptr}; + mutable winrt::Microsoft::ReactNative::Composition::implementation::RootComponentView *m_rootView{nullptr}; mutable winrt::Microsoft::ReactNative::Composition::implementation::Theme *m_theme{nullptr}; const winrt::Microsoft::ReactNative::ReactContext m_reactContext; winrt::Microsoft::ReactNative::ComponentView m_parent{nullptr}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionHwndHost.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionHwndHost.cpp index 5d6b9a5e561..60e1a152ed5 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionHwndHost.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionHwndHost.cpp @@ -102,6 +102,7 @@ void CompositionHwndHost::UpdateSize() noexcept { // Do not relayout when minimized if (!IsIconic(m_hwnd)) { winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; constraints.MinimumSize = constraints.MaximumSize = size; m_compRootView.Arrange(constraints, {0, 0}); } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp index 3aaeff5a6ab..d01e812cd5d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.cpp @@ -98,21 +98,36 @@ void CompositionReactViewInstance::UninitRootView() noexcept { } } -void ApplyConstraints( +void ReactNativeIsland::ApplyConstraints( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraintsIn, - facebook::react::LayoutConstraints &layoutConstraintsOut) noexcept { + facebook::react::LayoutConstraints &layoutConstraintsOut) const noexcept { layoutConstraintsOut.minimumSize = {layoutConstraintsIn.MinimumSize.Width, layoutConstraintsIn.MinimumSize.Height}; layoutConstraintsOut.maximumSize = {layoutConstraintsIn.MaximumSize.Width, layoutConstraintsIn.MaximumSize.Height}; - layoutConstraintsOut.layoutDirection = - static_cast(layoutConstraintsIn.LayoutDirection); + if (layoutConstraintsIn.LayoutDirection == winrt::Microsoft::ReactNative::LayoutDirection::Undefined) { + if (m_island) { + layoutConstraintsOut.layoutDirection = + (m_island.LayoutDirection() == winrt::Microsoft::UI::Content::ContentLayoutDirection::LeftToRight) + ? facebook::react::LayoutDirection::LeftToRight + : facebook::react::LayoutDirection::RightToLeft; + } else if (m_hwnd) { + auto styles = GetWindowLongPtrW(m_hwnd, GWL_EXSTYLE); + layoutConstraintsOut.layoutDirection = ((styles & WS_EX_LAYOUTRTL) == WS_EX_LAYOUTRTL) + ? facebook::react::LayoutDirection::RightToLeft + : facebook::react::LayoutDirection::LeftToRight; + } + } else { + layoutConstraintsOut.layoutDirection = + static_cast(layoutConstraintsIn.LayoutDirection); + } } -ReactNativeIsland::ReactNativeIsland() noexcept {} - -#ifdef USE_WINUI3 ReactNativeIsland::ReactNativeIsland(const winrt::Microsoft::UI::Composition::Compositor &compositor) noexcept - : m_compositor(compositor) {} -#endif + : m_compositor(compositor), + m_layoutConstraints({{0, 0}, {0, 0}, winrt::Microsoft::ReactNative::LayoutDirection::Undefined}) { + InitTextScaleMultiplier(); +} + +ReactNativeIsland::ReactNativeIsland() noexcept : ReactNativeIsland(nullptr) {} ReactNativeIsland::~ReactNativeIsland() noexcept { #ifdef USE_WINUI3 @@ -248,6 +263,10 @@ void ReactNativeIsland::ScaleFactor(float value) noexcept { } } +float ReactNativeIsland::FontSizeMultiplier() const noexcept { + return m_textScaleMultiplier; +} + int64_t ReactNativeIsland::RootTag() const noexcept { return m_rootTag; } @@ -431,10 +450,9 @@ void ReactNativeIsland::UninitRootView() noexcept { uiManager->stopSurface(static_cast(RootTag())); // This is needed to ensure that the unmount JS logic is completed before the the instance is shutdown during - // instance destruction. Aligns with similar code in ReactInstanceWin::DetachRootView for paper Future: Instead this - // method should return a Promise, which should be resolved when the JS logic is complete. - // The task will auto set the event on destruction to ensure that the event is set if the JS Queue has already been - // shutdown + // instance destruction. Aligns with similar code in ReactInstanceWin::DetachRootView for paper Future: Instead + // this method should return a Promise, which should be resolved when the JS logic is complete. The task will auto + // set the event on destruction to ensure that the event is set if the JS Queue has already been shutdown Mso::ManualResetEvent mre; m_context.JSDispatcher().Post([autoMRE = std::make_unique(AutoMRE{mre})]() {}); mre.Wait(); @@ -499,9 +517,8 @@ facebook::react::AttributedStringBox CreateLoadingAttributedString() noexcept { return facebook::react::AttributedStringBox{attributedString}; } -facebook::react::Size MeasureLoading( - const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints, - float scaleFactor) { +facebook::react::Size ReactNativeIsland::MeasureLoading( + const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints) const noexcept { facebook::react::LayoutConstraints fbLayoutConstraints; ApplyConstraints(layoutConstraints, fbLayoutConstraints); @@ -514,7 +531,7 @@ facebook::react::Size MeasureLoading( winrt::check_hresult(textLayout->GetMetrics(&tm)); return fbLayoutConstraints.clamp( - {loadingActivityHorizontalOffset * scaleFactor + tm.width, loadingBarHeight * scaleFactor}); + {loadingActivityHorizontalOffset * m_scaleFactor + tm.width, loadingBarHeight * m_scaleFactor}); } winrt::event_token ReactNativeIsland::SizeChanged( @@ -544,7 +561,7 @@ void ReactNativeIsland::NotifySizeChanged() noexcept { if (rootComponentView) { size = rootComponentView->layoutMetrics().frame.size; } else if (m_loadingVisual) { - size = MeasureLoading(m_layoutConstraints, m_scaleFactor); + size = MeasureLoading(m_layoutConstraints); } m_size = {size.width, size.height}; @@ -638,11 +655,34 @@ void ReactNativeIsland::ShowInstanceLoading() noexcept { InternalRootVisual().InsertAt(m_loadingVisual, m_hasRenderedVisual ? 1 : 0); } +void ReactNativeIsland::InitTextScaleMultiplier() noexcept { + m_uiSettings = winrt::Windows::UI::ViewManagement::UISettings(); + m_textScaleMultiplier = static_cast(m_uiSettings.TextScaleFactor()); + m_textScaleChangedRevoker = m_uiSettings.TextScaleFactorChanged( + winrt::auto_revoke, + [this](const winrt::Windows::UI::ViewManagement::UISettings &uiSettings, const winrt::IInspectable &) { + if (m_context) { + m_context.UIDispatcher().Post( + [wkThis = get_weak(), textScaleMultiplier = static_cast(uiSettings.TextScaleFactor())]() { + if (auto strongThis = wkThis.get()) { + strongThis->m_textScaleMultiplier = textScaleMultiplier; + strongThis->Arrange(strongThis->m_layoutConstraints, strongThis->m_viewportOffset); + } + }); + } + }); +} + winrt::Windows::Foundation::Size ReactNativeIsland::Measure( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints, - const winrt::Windows::Foundation::Point &viewportOffset) const noexcept { + const winrt::Windows::Foundation::Point &viewportOffset) const { facebook::react::Size size{0, 0}; + if (layoutConstraints.LayoutDirection != winrt::Microsoft::ReactNative::LayoutDirection::LeftToRight && + layoutConstraints.LayoutDirection != winrt::Microsoft::ReactNative::LayoutDirection::RightToLeft && + layoutConstraints.LayoutDirection != winrt::Microsoft::ReactNative::LayoutDirection::Undefined) + winrt::throw_hresult(E_INVALIDARG); + facebook::react::LayoutConstraints constraints; ApplyConstraints(layoutConstraints, constraints); @@ -650,15 +690,14 @@ winrt::Windows::Foundation::Size ReactNativeIsland::Measure( if (auto fabricuiManager = ::Microsoft::ReactNative::FabricUIManager::FromProperties( winrt::Microsoft::ReactNative::ReactPropertyBag(m_context.Properties()))) { facebook::react::LayoutContext context; - // TODO scaling factor + context.fontSizeMultiplier = m_textScaleMultiplier; context.pointScaleFactor = static_cast(m_scaleFactor); - context.fontSizeMultiplier = static_cast(m_scaleFactor); context.viewportOffset = {viewportOffset.X, viewportOffset.Y}; size = fabricuiManager->measureSurface(static_cast(m_rootTag), constraints, context); } } else if (m_loadingVisual) { - size = MeasureLoading(layoutConstraints, m_scaleFactor); + size = MeasureLoading(layoutConstraints); } auto clampedSize = constraints.clamp(size); @@ -667,7 +706,12 @@ winrt::Windows::Foundation::Size ReactNativeIsland::Measure( void ReactNativeIsland::Arrange( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints, - const winrt::Windows::Foundation::Point &viewportOffset) noexcept { + const winrt::Windows::Foundation::Point &viewportOffset) { + if (layoutConstraints.LayoutDirection != winrt::Microsoft::ReactNative::LayoutDirection::LeftToRight && + layoutConstraints.LayoutDirection != winrt::Microsoft::ReactNative::LayoutDirection::RightToLeft && + layoutConstraints.LayoutDirection != winrt::Microsoft::ReactNative::LayoutDirection::Undefined) + winrt::throw_hresult(E_INVALIDARG); + m_layoutConstraints = layoutConstraints; m_viewportOffset = viewportOffset; facebook::react::LayoutConstraints fbLayoutConstraints; @@ -677,8 +721,8 @@ void ReactNativeIsland::Arrange( if (auto fabricuiManager = ::Microsoft::ReactNative::FabricUIManager::FromProperties( winrt::Microsoft::ReactNative::ReactPropertyBag(m_context.Properties()))) { facebook::react::LayoutContext context; + context.fontSizeMultiplier = m_textScaleMultiplier; context.pointScaleFactor = static_cast(m_scaleFactor); - context.fontSizeMultiplier = static_cast(m_scaleFactor); context.viewportOffset = {viewportOffset.X, viewportOffset.Y}; fabricuiManager->constraintSurfaceLayout( @@ -686,12 +730,11 @@ void ReactNativeIsland::Arrange( } } else if (m_loadingVisual) { // TODO: Resize to align loading - auto s = fbLayoutConstraints.clamp(MeasureLoading(layoutConstraints, m_scaleFactor)); + auto s = fbLayoutConstraints.clamp(MeasureLoading(layoutConstraints)); NotifySizeChanged(); } } -#ifdef USE_WINUI3 winrt::Microsoft::UI::Content::ContentIsland ReactNativeIsland::Island() { if (!m_compositor) { return nullptr; @@ -745,6 +788,9 @@ winrt::Microsoft::UI::Content::ContentIsland ReactNativeIsland::Island() { if (args.DidRasterizationScaleChange()) { pThis->ScaleFactor(island.RasterizationScale()); } + if (args.DidLayoutDirectionChange()) { + pThis->Arrange(pThis->m_layoutConstraints, pThis->m_viewportOffset); + } #ifndef USE_EXPERIMENTAL_WINUI3 // Use this in place of Connected/Disconnected events for now. -- Its not quite what we // want, but it will do for now. if (args.DidSiteVisibleChange()) { @@ -777,7 +823,6 @@ winrt::Microsoft::UI::Content::ContentIsland ReactNativeIsland::Island() { } return m_island; } -#endif void ReactNativeIsland::OnMounted() noexcept { if (m_mounted) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h index 28c0de9c6d2..8f292357515 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ReactNativeIsland.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "CompositionEventHandler.h" #include "ReactHost/React.h" @@ -46,10 +47,8 @@ struct ReactNativeIsland ReactNativeIsland() noexcept; ~ReactNativeIsland() noexcept; -#ifdef USE_WINUI3 ReactNativeIsland(const winrt::Microsoft::UI::Composition::Compositor &compositor) noexcept; winrt::Microsoft::UI::Content::ContentIsland Island(); -#endif // property ReactViewHost ReactNative::IReactViewHost ReactViewHost() noexcept; @@ -72,6 +71,8 @@ struct ReactNativeIsland float ScaleFactor() noexcept; void ScaleFactor(float value) noexcept; + float FontSizeMultiplier() const noexcept; + winrt::event_token SizeChanged( winrt::Windows::Foundation::EventHandler const &handler) noexcept; @@ -90,10 +91,10 @@ struct ReactNativeIsland winrt::Windows::Foundation::Size Measure( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints, - const winrt::Windows::Foundation::Point &viewportOffset) const noexcept; + const winrt::Windows::Foundation::Point &viewportOffset) const; void Arrange( const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints, - const winrt::Windows::Foundation::Point &viewportOffset) noexcept; + const winrt::Windows::Foundation::Point &viewportOffset); winrt::Microsoft::ReactNative::FocusNavigationResult NavigateFocus( const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept; @@ -143,6 +144,9 @@ struct ReactNativeIsland winrt::IInspectable m_uiaProvider{nullptr}; int64_t m_rootTag{-1}; float m_scaleFactor{1.0}; + float m_textScaleMultiplier{1.0}; + winrt::Windows::UI::ViewManagement::UISettings::TextScaleFactorChanged_revoker m_textScaleChangedRevoker; + winrt::Windows::UI::ViewManagement::UISettings m_uiSettings{nullptr}; winrt::Windows::Foundation::Size m_size{0, 0}; winrt::Microsoft::ReactNative::ReactContext m_context; winrt::Microsoft::ReactNative::IReactViewHost m_reactViewHost; @@ -168,6 +172,12 @@ struct ReactNativeIsland void UpdateRootVisualSize() noexcept; void UpdateLoadingVisualSize() noexcept; Composition::Experimental::IDrawingSurfaceBrush CreateLoadingVisualBrush() noexcept; + void ApplyConstraints( + const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraintsIn, + facebook::react::LayoutConstraints &layoutConstraintsOut) const noexcept; + facebook::react::Size MeasureLoading( + const winrt::Microsoft::ReactNative::LayoutConstraints &layoutConstraints) const noexcept; + void InitTextScaleMultiplier() noexcept; }; } // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index 6dd450526c8..9d3cd495d62 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -40,8 +40,8 @@ winrt::Microsoft::ReactNative::ComponentView RootComponentView::Create( return winrt::make(compContext, tag, reactContext); } -RootComponentView *RootComponentView::rootComponentView() noexcept { - return this; +RootComponentView *RootComponentView::rootComponentView() const noexcept { + return const_cast(this); } void RootComponentView::updateLayoutMetrics( @@ -211,6 +211,15 @@ winrt::IInspectable RootComponentView::UiaProviderFromPoint(const POINT &ptPixel return winrt::get_self(view)->EnsureUiaProvider(); } +float RootComponentView::FontSizeMultiplier() const noexcept { + if (auto rootView = m_wkRootView.get()) { + return winrt::get_self(rootView) + ->FontSizeMultiplier(); + } + assert(false); + return 1.0f; +} + winrt::Microsoft::UI::Content::ContentIsland RootComponentView::parentContentIsland() noexcept { if (auto rootView = m_wkRootView.get()) { return winrt::get_self(rootView)->Island(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h index 7277b4a98e1..42b3ec70468 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h @@ -36,7 +36,7 @@ struct RootComponentView : RootComponentViewT { return false; } - m_outer->m_caretVisual.Position( - {x - (m_outer->m_layoutMetrics.frame.origin.x * m_outer->m_layoutMetrics.pointScaleFactor), - y - (m_outer->m_layoutMetrics.frame.origin.y * m_outer->m_layoutMetrics.pointScaleFactor)}); + auto pt = m_outer->getClientOffset(); + m_outer->m_caretVisual.Position({x - pt.x, y - pt.y}); return true; } @@ -262,7 +261,7 @@ struct CompTextHost : public winrt::implements { //@cmember Retrieves the coordinates of a window's client area HRESULT TxGetClientRect(LPRECT prc) override { - *prc = m_outer->m_rcClient; + *prc = m_outer->getClientRect(); return S_OK; } @@ -455,6 +454,7 @@ facebook::react::AttributedString WindowsTextInputComponentView::getAttributedSt // Use BaseTextShadowNode to get attributed string from children auto childTextAttributes = facebook::react::TextAttributes::defaultTextAttributes(); + childTextAttributes.fontSizeMultiplier = m_fontSizeMultiplier; childTextAttributes.apply(windowsTextInputProps().textAttributes); @@ -464,9 +464,9 @@ facebook::react::AttributedString WindowsTextInputComponentView::getAttributedSt // BaseTextShadowNode only gets children. We must detect and prepend text // value attributes manually. auto text = GetTextFromRichEdit(); - // if (!m_props->text.empty()) { if (!text.empty()) { auto textAttributes = facebook::react::TextAttributes::defaultTextAttributes(); + textAttributes.fontSizeMultiplier = m_fontSizeMultiplier; textAttributes.apply(windowsTextInputProps().textAttributes); auto fragment = facebook::react::AttributedString::Fragment{}; fragment.string = text; @@ -492,35 +492,7 @@ WindowsTextInputComponentView::WindowsTextInputComponentView( compContext, tag, reactContext, - ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) { - /* - m_textChangedRevoker = - m_element.TextChanged(winrt::auto_revoke, [this](auto sender, xaml::Controls::TextChangedEventArgs args) { - auto data = m_state->getData(); - data.attributedString = getAttributedString(); - data.mostRecentEventCount = m_nativeEventCount; - m_state->updateState(std::move(data)); - - if (m_eventEmitter && !m_comingFromJS) { - auto emitter = std::static_pointer_cast(m_eventEmitter); - facebook::react::WindowsTextInputEventEmitter::OnChange onChangeArgs; - onChangeArgs.text = winrt::to_string(m_element.Text()); - onChangeArgs.eventCount = ++m_nativeEventCount; - emitter->onChange(onChangeArgs); - } - }); - - m_SelectionChangedRevoker = m_element.SelectionChanged(winrt::auto_revoke, [this](auto sender, auto args) { - if (m_eventEmitter) { - auto emitter = std::static_pointer_cast(m_eventEmitter); - facebook::react::WindowsTextInputEventEmitter::OnSelectionChange onSelectionChangeArgs; - onSelectionChangeArgs.selection.start = m_element.SelectionStart(); - onSelectionChangeArgs.selection.end = m_element.SelectionStart() + m_element.SelectionLength(); - emitter->onSelectionChange(onSelectionChangeArgs); - } - }); - */ -} + ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) {} void WindowsTextInputComponentView::HandleCommand( const winrt::Microsoft::ReactNative::HandleCommandArgs &args) noexcept { @@ -531,26 +503,27 @@ void WindowsTextInputComponentView::HandleCommand( auto commandName = args.CommandName(); if (commandName == L"setTextAndSelection") { int eventCount, begin, end; - winrt::hstring text; + std::optional text; winrt::Microsoft::ReactNative::ReadArgs(args.CommandArgs(), eventCount, text, begin, end); if (eventCount >= m_nativeEventCount) { m_comingFromJS = true; - UpdateText(winrt::to_string(text)); - - SELCHANGE sc; - memset(&sc, 0, sizeof(sc)); - sc.chrg.cpMin = static_cast(begin); - sc.chrg.cpMax = static_cast(end); - sc.seltyp = (begin == end) ? SEL_EMPTY : SEL_TEXT; - - LRESULT res; - /* - winrt::check_hresult(m_textServices->TxSendMessage( - EM_SELCHANGE, 0 , reinterpret_cast(&sc), &res)); - */ - winrt::check_hresult( - m_textServices->TxSendMessage(EM_SETSEL, static_cast(begin), static_cast(end), &res)); + { + if (text.has_value()) { + DrawBlock db(*this); + UpdateText(winrt::to_string(text.value())); + } + + SELCHANGE sc; + memset(&sc, 0, sizeof(sc)); + sc.chrg.cpMin = static_cast(begin); + sc.chrg.cpMax = static_cast(end); + sc.seltyp = (begin == end) ? SEL_EMPTY : SEL_TEXT; + + LRESULT res; + winrt::check_hresult( + m_textServices->TxSendMessage(EM_SETSEL, static_cast(begin), static_cast(end), &res)); + } m_comingFromJS = false; } @@ -989,37 +962,35 @@ void WindowsTextInputComponentView::updateProps( *std::static_pointer_cast(oldProps ? oldProps : viewProps()); const auto &newTextInputProps = *std::static_pointer_cast(props); - DWORD propBitsMask = 0; - DWORD propBits = 0; - Super::updateProps(props, oldProps); if (!facebook::react::floatEquality( oldTextInputProps.textAttributes.fontSize, newTextInputProps.textAttributes.fontSize) || + (oldTextInputProps.textAttributes.allowFontScaling != newTextInputProps.textAttributes.allowFontScaling) || oldTextInputProps.textAttributes.fontWeight != newTextInputProps.textAttributes.fontWeight) { - propBitsMask |= TXTBIT_CHARFORMATCHANGE; - propBits |= TXTBIT_CHARFORMATCHANGE; + m_propBitsMask |= TXTBIT_CHARFORMATCHANGE; + m_propBits |= TXTBIT_CHARFORMATCHANGE; } if (oldTextInputProps.secureTextEntry != newTextInputProps.secureTextEntry) { - propBitsMask |= TXTBIT_USEPASSWORD; + m_propBitsMask |= TXTBIT_USEPASSWORD; if (newTextInputProps.secureTextEntry) { - propBits |= TXTBIT_USEPASSWORD; + m_propBits |= TXTBIT_USEPASSWORD; } } if (oldTextInputProps.multiline != newTextInputProps.multiline) { m_multiline = newTextInputProps.multiline; - propBitsMask |= TXTBIT_MULTILINE | TXTBIT_WORDWRAP; + m_propBitsMask |= TXTBIT_MULTILINE | TXTBIT_WORDWRAP; if (newTextInputProps.multiline) { - propBits |= TXTBIT_MULTILINE | TXTBIT_WORDWRAP; + m_propBits |= TXTBIT_MULTILINE | TXTBIT_WORDWRAP; } } if (oldTextInputProps.editable != newTextInputProps.editable) { - propBitsMask |= TXTBIT_READONLY; + m_propBitsMask |= TXTBIT_READONLY; if (!newTextInputProps.editable) { - propBits |= TXTBIT_READONLY; + m_propBits |= TXTBIT_READONLY; } } @@ -1051,62 +1022,7 @@ void WindowsTextInputComponentView::updateProps( m_submitKeyEvents.clear(); } - /* - if (oldTextInputProps.textAttributes.foregroundColor != newTextInputProps.textAttributes.foregroundColor) { - if (newTextInputProps.textAttributes.foregroundColor) - m_element.Foreground(newTextInputProps.textAttributes.foregroundColor.AsWindowsBrush()); - else - m_element.ClearValue(::xaml::Controls::TextBlock::ForegroundProperty()); - } - - if (oldTextInputProps.textAttributes.fontStyle != newTextInputProps.textAttributes.fontStyle) { - switch (newTextInputProps.textAttributes.fontStyle.value_or(facebook::react::FontStyle::Normal)) { - case facebook::react::FontStyle::Italic: - m_element.FontStyle(winrt::Windows::UI::Text::FontStyle::Italic); - break; - case facebook::react::FontStyle::Normal: - m_element.FontStyle(winrt::Windows::UI::Text::FontStyle::Normal); - break; - case facebook::react::FontStyle::Oblique: - m_element.FontStyle(winrt::Windows::UI::Text::FontStyle::Oblique); - break; - default: - assert(false); - } - } - - if (oldTextInputProps.textAttributes.fontFamily != newTextInputProps.textAttributes.fontFamily) { - if (newTextInputProps.textAttributes.fontFamily.empty()) - m_element.FontFamily(xaml::Media::FontFamily(L"Segoe UI")); - else - m_element.FontFamily(xaml::Media::FontFamily( - Microsoft::Common::Unicode::Utf8ToUtf16(newTextInputProps.textAttributes.fontFamily))); - } - - if (oldTextInputProps.allowFontScaling != newTextInputProps.allowFontScaling) { - m_element.IsTextScaleFactorEnabled(newTextInputProps.allowFontScaling); - } - - if (oldTextInputProps.selection.start != newTextInputProps.selection.start || - oldTextInputProps.selection.end != newTextInputProps.selection.end) { - m_element.Select( - newTextInputProps.selection.start, newTextInputProps.selection.end - newTextInputProps.selection.start); - } - - if (oldTextInputProps.autoCapitalize != newTextInputProps.autoCapitalize) { - if (newTextInputProps.autoCapitalize == "characters") { - m_element.CharacterCasing(xaml::Controls::CharacterCasing::Upper); - } else { // anything else turns off autoCap (should be "None" but - // we don't support "words"/"sentences" yet) - m_element.CharacterCasing(xaml::Controls::CharacterCasing::Normal); - } - } - */ - - if (propBitsMask != 0) { - DrawBlock db(*this); - winrt::check_hresult(m_textServices->OnTxPropertyBitsChange(propBitsMask, propBits)); - } + UpdatePropertyBits(); } void WindowsTextInputComponentView::updateState( @@ -1116,7 +1032,6 @@ void WindowsTextInputComponentView::updateState( if (!m_state) { assert(false && "State is `null` for component."); - // m_element.Text(L""); return; } @@ -1124,14 +1039,19 @@ void WindowsTextInputComponentView::updateState( m_mostRecentEventCount = m_state->getData().mostRecentEventCount; } + if (auto root = rootComponentView()) { + auto fontSizeMultiplier = root->FontSizeMultiplier(); + if (fontSizeMultiplier != m_fontSizeMultiplier) { + fontSizeMultiplier = m_fontSizeMultiplier; + m_propBitsMask |= TXTBIT_CHARFORMATCHANGE; + m_propBits |= TXTBIT_CHARFORMATCHANGE; + } + } + if (m_mostRecentEventCount == m_state->getData().mostRecentEventCount) { m_comingFromState = true; - // Only handle single/empty fragments right now -- ignore the other fragments - - UpdateText( - m_state->getData().attributedString.getFragments().size() - ? m_state->getData().attributedString.getFragments()[0].string - : ""); + auto &fragments = m_state->getData().attributedString.getFragments(); + UpdateText(fragments.size() ? fragments[0].string : ""); m_comingFromState = false; } @@ -1246,10 +1166,39 @@ std::string WindowsTextInputComponentView::GetTextFromRichEdit() const noexcept void WindowsTextInputComponentView::FinalizeUpdates( winrt::Microsoft::ReactNative::ComponentViewUpdateMask updateMask) noexcept { Super::FinalizeUpdates(updateMask); - ensureDrawingSurface(); - if (m_needsRedraw) { - DrawText(); + InternalFinalize(); +} + +void WindowsTextInputComponentView::UpdatePropertyBits() noexcept { + if (m_propBitsMask != 0) { + DrawBlock db(*this); + winrt::check_hresult(m_textServices->OnTxPropertyBitsChange(m_propBitsMask, m_propBits)); + m_propBitsMask = 0; + m_propBits = 0; + } +} + +void WindowsTextInputComponentView::InternalFinalize() noexcept { + if (m_mounted) { + UpdatePropertyBits(); + + ensureDrawingSurface(); + if (m_needsRedraw) { + DrawText(); + } + } +} + +void WindowsTextInputComponentView::onMounted() noexcept { + Super::onMounted(); + + auto fontSizeMultiplier = rootComponentView()->FontSizeMultiplier(); + if (m_fontSizeMultiplier != fontSizeMultiplier) { + m_fontSizeMultiplier = fontSizeMultiplier; + m_propBitsMask |= TXTBIT_CHARFORMATCHANGE; + m_propBits |= TXTBIT_CHARFORMATCHANGE; } + InternalFinalize(); } std::optional WindowsTextInputComponentView::getAccessiblityValue() noexcept { @@ -1294,9 +1243,9 @@ void WindowsTextInputComponentView::UpdateCharFormat() noexcept { // set font size -- 15 to convert twips to pt const auto &props = windowsTextInputProps(); - float fontSize = props.textAttributes.fontSize; - if (std::isnan(fontSize)) - fontSize = facebook::react::TextAttributes::defaultTextAttributes().fontSize; + float fontSize = m_fontSizeMultiplier * + (std::isnan(props.textAttributes.fontSize) ? facebook::react::TextAttributes::defaultTextAttributes().fontSize + : props.textAttributes.fontSize); // TODO get fontSize from props.textAttributes, or defaultTextAttributes, or fragment? cfNew.dwMask |= CFM_SIZE; cfNew.yHeight = static_cast(fontSize * 15); @@ -1363,8 +1312,8 @@ void WindowsTextInputComponentView::ensureDrawingSurface() noexcept { winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); - m_rcClient = getClientRect(); - winrt::check_hresult(m_textServices->OnTxInPlaceActivate(&m_rcClient)); + auto rc = getClientRect(); + winrt::check_hresult(m_textServices->OnTxInPlaceActivate(&rc)); LRESULT lresult; winrt::check_hresult( @@ -1394,6 +1343,7 @@ winrt::com_ptr<::IDWriteTextLayout> WindowsTextInputComponentView::CreatePlaceho if (std::isnan(props.textAttributes.fontSize)) { textAttributes.fontSize = 12.0f; } + textAttributes.fontSizeMultiplier = m_fontSizeMultiplier; fragment1.string = props.placeholder; fragment1.textAttributes = textAttributes; attributedString.appendFragment(fragment1); @@ -1443,7 +1393,11 @@ void WindowsTextInputComponentView::DrawText() noexcept { static_cast(offset.x) + static_cast(m_imgWidth), static_cast(offset.y) + static_cast(m_imgHeight)}; - winrt::check_hresult(m_textServices->OnTxInPlaceActivate(&rcClient)); + { + m_cDrawBlock++; // Dont use AutoDrawBlock as we are already in draw, and dont need to draw again. + winrt::check_hresult(m_textServices->OnTxInPlaceActivate(&rcClient)); + m_cDrawBlock--; + } const auto &props = windowsTextInputProps(); if (facebook::react::isColorMeaningful(props.backgroundColor)) { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index e355d27624f..11f5a141e6e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -65,6 +65,7 @@ struct WindowsTextInputComponentView void OnKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept override; void OnCharacterReceived(const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs &args) noexcept override; + void onMounted() noexcept override; std::optional getAccessiblityValue() noexcept override; void setAcccessiblityValue(std::string &&value) noexcept override; @@ -100,6 +101,8 @@ struct WindowsTextInputComponentView const facebook::react::SharedColor &foregroundColor) noexcept; bool ShouldSubmit( const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs &args) noexcept; + void InternalFinalize() noexcept; + void UpdatePropertyBits() noexcept; winrt::Windows::UI::Composition::CompositionSurfaceBrush m_brush{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::ICaretVisual m_caretVisual{nullptr}; @@ -113,7 +116,7 @@ struct WindowsTextInputComponentView winrt::com_ptr m_textServices; unsigned int m_imgWidth{0}, m_imgHeight{0}; std::shared_ptr m_state; - RECT m_rcClient; + float m_fontSizeMultiplier{1.0}; int64_t m_mostRecentEventCount{0}; int m_nativeEventCount{0}; bool m_comingFromJS{false}; @@ -123,6 +126,8 @@ struct WindowsTextInputComponentView bool m_drawing{false}; bool m_clearTextOnSubmit{false}; bool m_multiline{false}; + DWORD m_propBitsMask{0}; + DWORD m_propBits{0}; std::vector m_submitKeyEvents; }; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.cpp index 817b6713eab..ddaf498cb69 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.cpp @@ -23,10 +23,11 @@ void WindowsTextInputShadowNode::setContextContainer(ContextContainer *contextCo m_contextContainer = contextContainer; } -AttributedString WindowsTextInputShadowNode::getAttributedString() const { +AttributedString WindowsTextInputShadowNode::getAttributedString(const LayoutContext &layoutContext) const { // Use BaseTextShadowNode to get attributed string from children auto childTextAttributes = TextAttributes::defaultTextAttributes(); + childTextAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; childTextAttributes.apply(getConcreteProps().textAttributes); // Don't propagate the background color of the TextInput onto the attributed @@ -42,6 +43,7 @@ AttributedString WindowsTextInputShadowNode::getAttributedString() const { // value attributes manually. if (!getConcreteProps().text.empty()) { auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); auto fragment = AttributedString::Fragment{}; fragment.string = getConcreteProps().text; @@ -63,7 +65,7 @@ AttributedString WindowsTextInputShadowNode::getAttributedString() const { // display at all. // TODO T67606511: We will redefine the measurement of empty strings as part // of T67606511 -AttributedString WindowsTextInputShadowNode::getPlaceholderAttributedString() const { +AttributedString WindowsTextInputShadowNode::getPlaceholderAttributedString(const LayoutContext &layoutContext) const { // Return placeholder text, since text and children are empty. auto textAttributedString = AttributedString{}; auto fragment = AttributedString::Fragment{}; @@ -74,6 +76,7 @@ AttributedString WindowsTextInputShadowNode::getPlaceholderAttributedString() co } auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); // If there's no text, it's possible that this Fragment isn't actually @@ -90,10 +93,10 @@ void WindowsTextInputShadowNode::setTextLayoutManager(SharedTextLayoutManager te m_textLayoutManager = std::move(textLayoutManager); } -AttributedString WindowsTextInputShadowNode::getMostRecentAttributedString() const { +AttributedString WindowsTextInputShadowNode::getMostRecentAttributedString(const LayoutContext &layoutContext) const { const auto &state = getStateData(); - auto reactTreeAttributedString = getAttributedString(); + auto reactTreeAttributedString = getAttributedString(layoutContext); // Sometimes the treeAttributedString will only differ from the state // not by inherent properties (string or prop attributes), but by the frame of @@ -105,10 +108,10 @@ AttributedString WindowsTextInputShadowNode::getMostRecentAttributedString() con return (!treeAttributedStringChanged ? state.attributedString : reactTreeAttributedString); } -void WindowsTextInputShadowNode::updateStateIfNeeded() { +void WindowsTextInputShadowNode::updateStateIfNeeded(const LayoutContext &layoutContext) { ensureUnsealed(); - auto reactTreeAttributedString = getAttributedString(); + auto reactTreeAttributedString = getAttributedString(layoutContext); const auto &state = getStateData(); // Tree is often out of sync with the value of the TextInput. @@ -129,13 +132,13 @@ void WindowsTextInputShadowNode::updateStateIfNeeded() { // in the AttributedString, and when State is updated, it needs some way to // reconstruct a Fragment with default TextAttributes. auto defaultTextAttributes = TextAttributes::defaultTextAttributes(); - + defaultTextAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; defaultTextAttributes.apply(getConcreteProps().textAttributes); auto newEventCount = state.reactTreeAttributedString.isContentEqual(reactTreeAttributedString) ? 0 : getConcreteProps().mostRecentEventCount; - auto newAttributedString = getMostRecentAttributedString(); + auto newAttributedString = getMostRecentAttributedString(layoutContext); // Even if we're here and updating state, it may be only to update the layout // manager If that is the case, make sure we don't update text: pass in the @@ -172,10 +175,10 @@ Size WindowsTextInputShadowNode::measureContent( // during layout, but not during `measure`. If State is out-of-date in layout, // it's too late: measure will have already operated on old State. Thus, we // use the same value here that we *will* use in layout to update the state. - AttributedString attributedString = getMostRecentAttributedString(); + AttributedString attributedString = getMostRecentAttributedString(layoutContext); if (attributedString.isEmpty()) { - attributedString = getPlaceholderAttributedString(); + attributedString = getPlaceholderAttributedString(layoutContext); } if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) { @@ -194,7 +197,7 @@ Size WindowsTextInputShadowNode::measureContent( } void WindowsTextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(); + updateStateIfNeeded(layoutContext); ConcreteViewShadowNode::layout(layoutContext); } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.h index e8e9abf41cc..38e1b0022f1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputShadowNode.h @@ -42,8 +42,8 @@ class WindowsTextInputShadowNode final : public ConcreteViewShadowNode< /* * Returns a `AttributedString` which represents text content of the node. */ - AttributedString getAttributedString() const; - AttributedString getPlaceholderAttributedString() const; + AttributedString getAttributedString(const LayoutContext &layoutContext) const; + AttributedString getPlaceholderAttributedString(const LayoutContext &layoutContext) const; /* * Associates a shared TextLayoutManager with the node. @@ -63,13 +63,13 @@ class WindowsTextInputShadowNode final : public ConcreteViewShadowNode< /** * Get the most up-to-date attributed string for measurement and State. */ - AttributedString getMostRecentAttributedString() const; + AttributedString getMostRecentAttributedString(const LayoutContext &layoutContext) const; /* * Creates a `State` object (with `AttributedText` and * `TextLayoutManager`) if needed. */ - void updateStateIfNeeded(); + void updateStateIfNeeded(const LayoutContext &layoutContext); SharedTextLayoutManager m_textLayoutManager; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.cpp index 3ac8a64c53d..77d89ae820d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.cpp @@ -31,17 +31,4 @@ WindowsTextInputState::WindowsTextInputState( defaultThemePaddingTop(defaultThemePaddingTop), defaultThemePaddingBottom(defaultThemePaddingBottom) {} -WindowsTextInputState::WindowsTextInputState(const WindowsTextInputState &previousState, const folly::dynamic &data) - : mostRecentEventCount(data.getDefault("mostRecentEventCount", previousState.mostRecentEventCount).getInt()), - cachedAttributedStringId(data.getDefault("opaqueCacheId", previousState.cachedAttributedStringId).getInt()), - attributedString(previousState.attributedString), - reactTreeAttributedString(previousState.reactTreeAttributedString), - paragraphAttributes(previousState.paragraphAttributes), - defaultThemePaddingStart( - data.getDefault("themePaddingStart", previousState.defaultThemePaddingStart).getDouble()), - defaultThemePaddingEnd(data.getDefault("themePaddingEnd", previousState.defaultThemePaddingEnd).getDouble()), - defaultThemePaddingTop(data.getDefault("themePaddingTop", previousState.defaultThemePaddingTop).getDouble()), - defaultThemePaddingBottom( - data.getDefault("themePaddingBottom", previousState.defaultThemePaddingBottom).getDouble()){}; - } // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.h index ae137bef264..0e8cd188df1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputState.h @@ -66,9 +66,6 @@ class WindowsTextInputState final { double defaultThemePaddingBottom); WindowsTextInputState() = default; - WindowsTextInputState(const WindowsTextInputState &previousState, const folly::dynamic &data); - folly::dynamic getDynamic() const; - MapBuffer getMapBuffer() const; }; } // namespace facebook::react diff --git a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp index 4d7259c1543..9d4e999e232 100644 --- a/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/FabricUIManagerModule.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -141,7 +142,7 @@ void FabricUIManager::startSurface( facebook::react::LayoutContext layoutContext; layoutContext.pointScaleFactor = rootView.ScaleFactor(); - layoutContext.fontSizeMultiplier = rootView.ScaleFactor(); + layoutContext.fontSizeMultiplier = rootView.FontSizeMultiplier(); m_surfaceManager->startSurface( surfaceId, diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/TextLayoutManager.cpp index 03f4a6e0414..846ff48d012 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -41,7 +41,10 @@ void TextLayoutManager::GetTextLayout( static_cast(DWRITE_FONT_WEIGHT_REGULAR))), style, DWRITE_FONT_STRETCH_NORMAL, - outerFragment.textAttributes.fontSize, + (outerFragment.textAttributes.allowFontScaling.value_or(true) && + !std::isnan(outerFragment.textAttributes.fontSizeMultiplier)) + ? (outerFragment.textAttributes.fontSizeMultiplier * outerFragment.textAttributes.fontSize) + : outerFragment.textAttributes.fontSize, L"", spTextFormat.put())); @@ -116,7 +119,11 @@ void TextLayoutManager::GetTextLayout( attributes.fontWeight.value_or(static_cast(DWRITE_FONT_WEIGHT_REGULAR))), range)); winrt::check_hresult(spTextLayout->SetFontStyle(fragmentStyle, range)); - winrt::check_hresult(spTextLayout->SetFontSize(attributes.fontSize, range)); + winrt::check_hresult(spTextLayout->SetFontSize( + (attributes.allowFontScaling.value_or(true) && !std::isnan(attributes.fontSizeMultiplier)) + ? (attributes.fontSizeMultiplier * attributes.fontSize) + : attributes.fontSize, + range)); if (!isnan(attributes.letterSpacing)) { winrt::check_hresult( diff --git a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp index 31962d77cc1..80f568e152f 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp +++ b/vnext/Microsoft.ReactNative/ReactNativeAppBuilder.cpp @@ -25,6 +25,7 @@ void UpdateRootViewSizeToAppWindow( if (window.Presenter().as().State() != winrt::Microsoft::UI::Windowing::OverlappedPresenterState::Minimized) { winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; constraints.MaximumSize = constraints.MinimumSize = size; rootView.Arrange(constraints, {0, 0}); } diff --git a/vnext/Microsoft.ReactNative/ReactNativeIsland.idl b/vnext/Microsoft.ReactNative/ReactNativeIsland.idl index 4df3ecd227d..5c1b3f1311f 100644 --- a/vnext/Microsoft.ReactNative/ReactNativeIsland.idl +++ b/vnext/Microsoft.ReactNative/ReactNativeIsland.idl @@ -98,6 +98,8 @@ namespace Microsoft.ReactNative DOC_STRING("ScaleFactor for this windows (DPI/96)") Single ScaleFactor {get; set;}; + Single FontSizeMultiplier { get; }; + DOC_STRING("Move focus to this @ReactNativeIsland") FocusNavigationResult NavigateFocus(FocusNavigationRequest request); diff --git a/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp b/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp index a91fac96c70..eed4ef509dc 100644 --- a/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp +++ b/vnext/templates/cpp-app/windows/MyApp/MyApp.cpp @@ -35,6 +35,7 @@ void UpdateRootViewSizeToAppWindow( if (window.Presenter().as().State() != winrt::Microsoft::UI::Windowing::OverlappedPresenterState::Minimized) { winrt::Microsoft::ReactNative::LayoutConstraints constraints; + constraints.LayoutDirection = winrt::Microsoft::ReactNative::LayoutDirection::Undefined; constraints.MaximumSize = constraints.MinimumSize = size; rootView.Arrange(constraints, {0,0}); }