From 9f5cfd4c4a11f3c97b265a7bc36637ba8d11315e Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 23 Feb 2023 09:51:34 -0800 Subject: [PATCH 1/5] [Fabric] Implement onFocus and onBlur --- .../CompositionViewComponentView.cpp | 12 +- .../WindowsTextInputComponentView.cpp | 8 -- .../TextInput/WindowsTextInputComponentView.h | 1 - .../components/view/ViewEventEmitter.cpp | 127 ++++++++++++++++++ .../components/view/ViewEventEmitter.h | 82 +++++++++++ .../renderer/components/view/ViewProps.cpp | 15 +++ .../renderer/components/view/ViewProps.h | 2 + .../components/view/ViewShadowNode.cpp | 2 + .../renderer/components/view/primitives.h | 27 ++++ vnext/overrides.json | 12 ++ 10 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.cpp create mode 100644 vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.h diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index d771ab7a059..571c6682594 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -13,6 +13,7 @@ #include "CompositionContextHelper.h" #include "CompositionHelpers.h" #include "d2d1helper.h" +#include "RootComponentView.h" namespace Microsoft::ReactNative { @@ -38,6 +39,13 @@ const std::vector &CompositionBaseComponentView::children() co } void CompositionBaseComponentView::parent(IComponentView *parent) noexcept { + if (!parent) { + auto root = rootComponentView(); + if (root->GetFocusedComponent() == this) { + root->SetFocusedComponent(nullptr); // TODO need move focus logic - where should focus go? + } + } + m_parent = parent; } @@ -60,9 +68,9 @@ bool CompositionBaseComponentView::runOnChildren(bool forward, Mso::FunctoronBlur();} -void CompositionBaseComponentView::onFocusGained() noexcept {} +void CompositionBaseComponentView::onFocusGained() noexcept {m_eventEmitter->onFocus();} void CompositionBaseComponentView::updateEventEmitter( facebook::react::EventEmitter::Shared const &eventEmitter) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index bab7590ef8d..e774ef50da4 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -558,14 +558,6 @@ WindowsTextInputComponentView::supplementalComponentDescriptorProviders() noexce return {}; } -void WindowsTextInputComponentView::parent(IComponentView *parent) noexcept { - if (!parent && rootComponentView()->GetFocusedComponent() == this) { - rootComponentView()->SetFocusedComponent(nullptr); // TODO need move focus logic - where should focus go? - } - - Super::parent(parent); -} - void WindowsTextInputComponentView::mountChildComponentView( IComponentView &childComponentView, uint32_t index) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index ab84a821ebd..c2b90929e7d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -46,7 +46,6 @@ struct WindowsTextInputComponentView : CompositionBaseComponentView { void handleCommand(std::string const &commandName, folly::dynamic const &arg) noexcept override; int64_t sendMessage(uint32_t msg, uint64_t wParam, int64_t lParam) noexcept override; facebook::react::Tag hitTest(facebook::react::Point pt, facebook::react::Point &localPt) const noexcept override; - void parent(IComponentView *parent) noexcept override; void OnRenderingDeviceLost() noexcept override; winrt::Microsoft::ReactNative::Composition::IVisual Visual() const noexcept override; void onFocusLost() noexcept override; diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.cpp new file mode 100644 index 00000000000..973f4e68e9d --- /dev/null +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ViewEventEmitter.h" + +namespace facebook::react { + +#pragma mark - Accessibility + +void ViewEventEmitter::onAccessibilityAction(std::string const &name) const { + dispatchEvent("accessibilityAction", [name](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "actionName", name); + return payload; + }); +} + +void ViewEventEmitter::onAccessibilityTap() const { + dispatchEvent("accessibilityTap"); +} + +void ViewEventEmitter::onAccessibilityMagicTap() const { + dispatchEvent("magicTap"); +} + +void ViewEventEmitter::onAccessibilityEscape() const { + dispatchEvent("accessibilityEscape"); +} + +#pragma mark - Layout + +void ViewEventEmitter::onLayout(const LayoutMetrics &layoutMetrics) const { + // A copy of a shared pointer (`layoutEventState_`) establishes shared + // ownership that will be captured by lambda. + auto layoutEventState = layoutEventState_; + + // Dispatched `frame` values to JavaScript thread are throttled here. + // Basic ideas: + // - Scheduling a lambda with some value that already was dispatched, does + // nothing. + // - If some lambda is already in flight, we don't schedule another; + // - When a lambda is being executed on the JavaScript thread, the *most + // recent* `frame` value is used (not the value that was current at the + // moment of scheduling the lambda). + // + // This implies the following caveats: + // - Some events can be skipped; + // - When values change rapidly, even events with different values + // can be skipped (only the very last will be delivered). + // - Ordering is preserved. + + { + std::lock_guard guard(layoutEventState->mutex); + + // If a *particular* `frame` was already dispatched to the JavaScript side, + // no other work is required. + if (layoutEventState->frame == layoutMetrics.frame && + layoutEventState->wasDispatched) { + return; + } + + // If the *particular* `frame` was not already dispatched *or* + // some *other* `frame` was dispatched before, + // we need to schedule the dispatching. + layoutEventState->wasDispatched = false; + layoutEventState->frame = layoutMetrics.frame; + + // Something is already in flight, dispatching another event is not + // required. + if (layoutEventState->isDispatching) { + return; + } + + layoutEventState->isDispatching = true; + } + + dispatchEvent( + "layout", + [layoutEventState](jsi::Runtime &runtime) { + auto frame = Rect{}; + + { + std::lock_guard guard(layoutEventState->mutex); + + layoutEventState->isDispatching = false; + + // If some *particular* `frame` was already dispatched before, + // and since then there were no other new values of the `frame` + // observed, do nothing. + if (layoutEventState->wasDispatched) { + return jsi::Value::null(); + } + + frame = layoutEventState->frame; + + // If some *particular* `frame` was *not* already dispatched before, + // it's time to dispatch it and mark as dispatched. + layoutEventState->wasDispatched = true; + } + + auto layout = jsi::Object(runtime); + layout.setProperty(runtime, "x", frame.origin.x); + layout.setProperty(runtime, "y", frame.origin.y); + layout.setProperty(runtime, "width", frame.size.width); + layout.setProperty(runtime, "height", frame.size.height); + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "layout", std::move(layout)); + return jsi::Value(std::move(payload)); + }, + EventPriority::AsynchronousUnbatched); +} + +// [Windows] +void ViewEventEmitter::onFocus() const { + dispatchEvent("focus"); +} + +// [Windows] +void ViewEventEmitter::onBlur() const { + dispatchEvent("blur"); +} + +} // namespace facebook::react diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.h new file mode 100644 index 00000000000..77a8bd41e8f --- /dev/null +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include +#include + +#include "TouchEventEmitter.h" + +namespace facebook { +namespace react { + +struct FocusEvent { + int target; +}; + +class ViewEventEmitter; + +using SharedViewEventEmitter = std::shared_ptr; + +class ViewEventEmitter : public TouchEventEmitter { + public: + using TouchEventEmitter::TouchEventEmitter; + +#pragma mark - Accessibility + + void onAccessibilityAction(std::string const &name) const; + void onAccessibilityTap() const; + void onAccessibilityMagicTap() const; + void onAccessibilityEscape() const; + +#pragma mark - Layout + + void onLayout(const LayoutMetrics &layoutMetrics) const; + +#pragma mark - NativeHostPlatform + + void onFocus() const; // [Windows] + void onBlur() const; // [Windows] + + private: + /* + * Contains the most recent `frame` and a `mutex` protecting access to it. + */ + struct LayoutEventState { + /* + * Protects an access to other fields of the struct. + */ + std::mutex mutex; + + /* + * Last dispatched `frame` value or value that's being dispatched right now. + */ + Rect frame{}; + + /* + * Indicates that the `frame` value was already dispatched (and dispatching + * of the *same* value is not needed). + */ + bool wasDispatched{false}; + + /* + * Indicates that some lambda is already being dispatching (and dispatching + * another one is not needed). + */ + bool isDispatching{false}; + }; + + mutable std::shared_ptr layoutEventState_{ + std::make_shared()}; +}; + +} // namespace react +} // namespace facebook diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.cpp index 910d648d082..8630e48c44b 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.cpp +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.cpp @@ -283,6 +283,19 @@ ViewProps::ViewProps( return; \ } +// [Windows] +#define HOST_PLATFORM_VIEW_EVENT_CASE(eventType) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ + const auto offset = HostPlatformViewEvents::Offset::eventType; \ + HostPlatformViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[offset]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + platformEvents[offset] = res; \ + return; \ + } + void ViewProps::setProp( const PropsParserContext &context, RawPropsPropNameHash hash, @@ -339,6 +352,8 @@ void ViewProps::setProp( VIEW_EVENT_CASE(TouchCancel); VIEW_EVENT_CASE(MouseEnter); // [Windows] VIEW_EVENT_CASE(MouseLeave); // [Windows] + HOST_PLATFORM_VIEW_EVENT_CASE(Focus); // [Windows] + HOST_PLATFORM_VIEW_EVENT_CASE(Blur); // [Windows] #ifdef ANDROID RAW_SET_PROP_SWITCH_CASE_BASIC(elevation, {}); diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.h index 7c1b7a301b7..cc3eac1d92f 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.h +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.h @@ -77,6 +77,8 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps { ViewEvents events{}; + HostPlatformViewEvents platformEvents{}; // [Windows] + bool collapsable{true}; bool removeClippedSubviews{false}; diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewShadowNode.cpp b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewShadowNode.cpp index 0567a424fe4..fb1831c1a3f 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewShadowNode.cpp +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewShadowNode.cpp @@ -56,6 +56,8 @@ void ViewShadowNode::initialize() noexcept { viewProps.importantForAccessibility != ImportantForAccessibility::Auto || viewProps.removeClippedSubviews; + formsStackingContext = formsStackingContext || viewProps.platformEvents.bits.any(); // [Windows] + #ifdef ANDROID formsStackingContext = formsStackingContext || viewProps.elevation != 0; #endif diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h index d81613fd065..c163239e92b 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h @@ -82,6 +82,33 @@ inline static bool operator!=(ViewEvents const &lhs, ViewEvents const &rhs) { return lhs.bits != rhs.bits; } +// [Windows] +struct HostPlatformViewEvents { + std::bitset<32> bits{}; + + enum class Offset : std::size_t { + Focus = 0, + Blur = 0, + }; + + constexpr bool operator[](const Offset offset) const { + return bits[static_cast(offset)]; + } + + std::bitset<32>::reference operator[](const Offset offset) { + return bits[static_cast(offset)]; + } + }; + + +inline static bool operator==(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) { + return lhs.bits == rhs.bits; +} + +inline static bool operator!=(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) { + return lhs.bits != rhs.bits; +} + enum class BackfaceVisibility : uint8_t { Auto, Visible, Hidden }; enum class BorderCurve : uint8_t { Circular, Continuous }; diff --git a/vnext/overrides.json b/vnext/overrides.json index decbf426e36..555900169d7 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -98,6 +98,18 @@ "baseFile": "ReactCommon/react/renderer/components/view/TouchEventEmitter.h", "baseHash": "e8e2327d4c67082922c849a3726f73543e314eca" }, + { + "type": "derived", + "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.cpp", + "baseFile": "ReactCommon/react/renderer/components/view/ViewEventEmitter.cpp", + "baseHash": "0c19260b4d84d078e6991f67d94df14a32e71017" + }, + { + "type": "derived", + "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewEventEmitter.h", + "baseFile": "ReactCommon/react/renderer/components/view/ViewEventEmitter.h", + "baseHash": "a373fca9d1a593bf6cb5717c394ffba91baddf40" + }, { "type": "derived", "file": "ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/ViewProps.cpp", From 8a11fb428a34236de32fdc0d551eb777358254b8 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 23 Feb 2023 09:51:46 -0800 Subject: [PATCH 2/5] Change files --- ...ative-windows-0d249a84-b206-4efd-bdcb-1888c83a3b3d.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-0d249a84-b206-4efd-bdcb-1888c83a3b3d.json diff --git a/change/react-native-windows-0d249a84-b206-4efd-bdcb-1888c83a3b3d.json b/change/react-native-windows-0d249a84-b206-4efd-bdcb-1888c83a3b3d.json new file mode 100644 index 00000000000..99a652726af --- /dev/null +++ b/change/react-native-windows-0d249a84-b206-4efd-bdcb-1888c83a3b3d.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "[Fabric] Implement onFocus and onBlur", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From 6b1369b3172a37f8dee83272d459040e6c0343f1 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 23 Feb 2023 10:13:56 -0800 Subject: [PATCH 3/5] fix issue where updating borderColor does not update the border --- .../Composition/CompositionViewComponentView.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 571c6682594..c4874ed0dad 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -979,9 +979,9 @@ void CompositionBaseComponentView::updateBorderLayoutMetrics( Visual().as()->SetClippingPath(pathGeometry.get()); } - if (m_needsBorderUpdate || m_layoutMetrics != layoutMetrics) { - m_needsBorderUpdate = false; - UpdateSpecialBorderLayers(layoutMetrics, viewProps); + if (m_layoutMetrics != layoutMetrics) { + m_needsBorderUpdate = true; + } } @@ -1227,7 +1227,12 @@ void CompositionViewComponentView::updateLayoutMetrics( }); } -void CompositionViewComponentView::finalizeUpdates(RNComponentViewUpdateMask updateMask) noexcept {} +void CompositionViewComponentView::finalizeUpdates(RNComponentViewUpdateMask updateMask) noexcept { + if (m_needsBorderUpdate) { + m_needsBorderUpdate = false; + UpdateSpecialBorderLayers(m_layoutMetrics, *m_props); + } +} void CompositionViewComponentView::prepareForRecycle() noexcept {} facebook::react::Props::Shared CompositionViewComponentView::props() noexcept { From d2d7d2bef5dd69fe9da1a7005fb57a33e6aa9769 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 23 Feb 2023 10:44:06 -0800 Subject: [PATCH 4/5] formatting --- .../Composition/CompositionViewComponentView.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index c4874ed0dad..fdcbbc0d339 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -12,8 +12,8 @@ #include #include "CompositionContextHelper.h" #include "CompositionHelpers.h" -#include "d2d1helper.h" #include "RootComponentView.h" +#include "d2d1helper.h" namespace Microsoft::ReactNative { @@ -68,9 +68,13 @@ bool CompositionBaseComponentView::runOnChildren(bool forward, Mso::FunctoronBlur();} +void CompositionBaseComponentView::onFocusLost() noexcept { + m_eventEmitter->onBlur(); +} -void CompositionBaseComponentView::onFocusGained() noexcept {m_eventEmitter->onFocus();} +void CompositionBaseComponentView::onFocusGained() noexcept { + m_eventEmitter->onFocus(); +} void CompositionBaseComponentView::updateEventEmitter( facebook::react::EventEmitter::Shared const &eventEmitter) noexcept { @@ -981,7 +985,6 @@ void CompositionBaseComponentView::updateBorderLayoutMetrics( if (m_layoutMetrics != layoutMetrics) { m_needsBorderUpdate = true; - } } From c8203cfd8033d9da66539fe6383927c727815834 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:12:06 -0800 Subject: [PATCH 5/5] fix --- .../react/renderer/components/view/primitives.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h index c163239e92b..81cdaae343a 100644 --- a/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h +++ b/vnext/ReactCommon/TEMP_UntilReactCommonUpdate/react/renderer/components/view/primitives.h @@ -88,7 +88,7 @@ struct HostPlatformViewEvents { enum class Offset : std::size_t { Focus = 0, - Blur = 0, + Blur = 1, }; constexpr bool operator[](const Offset offset) const {