From 083ec568b27bd1fa582d307630b00ab0df712e4b Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:29:56 -0700 Subject: [PATCH 1/8] Implement outline properties on view --- .../Fabric/Composition/BorderPrimitive.cpp | 26 +++-- .../Fabric/Composition/BorderPrimitive.h | 2 + .../CompositionViewComponentView.cpp | 96 +++++++++++++++++++ .../CompositionViewComponentView.h | 4 + 4 files changed, 114 insertions(+), 14 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp index fd25ebce122..d1b3f584134 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp @@ -40,22 +40,20 @@ void pixelRoundBorderRadii(facebook::react::BorderRadii &borderRadii, float scal }; } +float pixelRoundAndScaleBorderWidth(float width, float scaleFactor) noexcept { + if (width == 0) + return width = 0; + return std::max(1.f, std::round(width * scaleFactor)); +} + void scaleAndPixelRoundBorderWidths( facebook::react::LayoutMetrics const &layoutMetrics, facebook::react::BorderMetrics &borderMetrics, float scaleFactor) noexcept { - borderMetrics.borderWidths.left = (borderMetrics.borderWidths.left == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.left * scaleFactor)); - borderMetrics.borderWidths.top = (borderMetrics.borderWidths.top == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.top * scaleFactor)); - borderMetrics.borderWidths.right = (borderMetrics.borderWidths.right == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.right * scaleFactor)); - borderMetrics.borderWidths.bottom = (borderMetrics.borderWidths.bottom == 0) - ? 0.f - : std::max(1.f, std::round(borderMetrics.borderWidths.bottom * scaleFactor)); + borderMetrics.borderWidths.left = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.left, scaleFactor); + borderMetrics.borderWidths.top = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.top, scaleFactor); + borderMetrics.borderWidths.right = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.right, scaleFactor); + borderMetrics.borderWidths.bottom = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.bottom, scaleFactor); // If we rounded both sides of the borderWidths up, we may have made the borderWidths larger than the total if (layoutMetrics.frame.size.width * scaleFactor < @@ -740,9 +738,9 @@ bool BorderPrimitive::requiresBorder( auto borderStyle = borderMetrics.borderStyles.left; bool hasMeaningfulColor = - !borderMetrics.borderColors.isUniform() || !isColorMeaningful(borderMetrics.borderColors.left, theme); + !borderMetrics.borderColors.isUniform() || isColorMeaningful(borderMetrics.borderColors.left, theme); bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); - if (!hasMeaningfulColor && !hasMeaningfulWidth) { + if (!hasMeaningfulColor || !hasMeaningfulWidth) { return false; } return true; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h index 6661ee45299..47ad6578734 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h @@ -14,6 +14,8 @@ namespace winrt::Microsoft::ReactNative::Composition::implementation { struct ComponentView; +float pixelRoundAndScaleBorderWidth(float width, float scaleFactor) noexcept; + // Controls adding/removing appropriate visuals to a parent to render a specific border without requiring struct BorderPrimitive { static constexpr size_t SpecialBorderLayerCount = 8; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 792dfea6421..c74b5e8d32e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -41,6 +41,7 @@ constexpr float FOCUS_VISUAL_RADIUS = 3.0f; // ----- m_visual <-- Background / clip - Can be a custom visual depending on Component type // | // ----- Border Visuals x N (BorderPrimitive attached to m_visual) +// ----- Outline Visuals x N(BorderPrimitive) // ----- (default: directly in m_visual after border visuals) // ----- m_childrenContainer (created on demand when overflow:hidden, children moved here) // ------Focus Visual Container (created when hosting focus visuals) @@ -91,6 +92,9 @@ void ComponentView::onThemeChanged() noexcept { m_borderPrimitive->onThemeChanged( m_layoutMetrics, BorderPrimitive::resolveAndAlignBorderMetrics(m_layoutMetrics, *viewProps())); } + if (m_outlinePrimitive) { + m_outlinePrimitive->onThemeChanged(outlineLayoutMetrics(), outlineBorderMetrics()); + } if (m_componentHostingFocusVisual) { if (m_componentHostingFocusVisual->m_focusPrimitive->m_focusInnerPrimitive) { auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/); @@ -166,6 +170,16 @@ void ComponentView::updateProps( m_borderPrimitive->updateProps(oldViewProps, newViewProps); } + if (m_outlinePrimitive) { + if (oldViewProps.outlineOffset != newViewProps.outlineOffset || + oldViewProps.outlineWidth != newViewProps.outlineWidth || + oldViewProps.borderRadii != newViewProps.borderRadii || + oldViewProps.outlineColor != newViewProps.outlineColor || + oldViewProps.outlineStyle != newViewProps.outlineStyle) { + m_outlinePrimitive->markNeedsUpdate(); + } + } + if (m_componentHostingFocusVisual) { if (!newViewProps.enableFocusRing) { m_componentHostingFocusVisual->hostFocusVisual(false, get_strong()); @@ -218,6 +232,9 @@ void ComponentView::updateLayoutMetrics( if (m_borderPrimitive) { m_borderPrimitive->markNeedsUpdate(); } + if (m_outlinePrimitive) { + m_outlinePrimitive->markNeedsUpdate(); + } if (m_componentHostingFocusVisual) { m_componentHostingFocusVisual->updateFocusLayoutMetrics(); @@ -301,6 +318,21 @@ void ComponentView::FinalizeUpdates(winrt::Microsoft::ReactNative::ComponentView if (m_borderPrimitive) { m_borderPrimitive->finalize(m_layoutMetrics, borderMetrics); } + + auto outlineMetrics = outlineBorderMetrics(); + if (!m_outlinePrimitive && BorderPrimitive::requiresBorder(outlineMetrics, theme())) { + m_outlinePrimitive = std::make_shared(*this); + Visual().InsertAt(m_outlinePrimitive->RootVisual(), m_borderPrimitive ? m_borderPrimitive->numberOfVisuals() : 0); + } + + if (m_outlinePrimitive) { + auto offset = pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor) + + std::round(viewProps()->outlineOffset * m_layoutMetrics.pointScaleFactor); + m_outlinePrimitive->RootVisual().Offset({-offset, -offset, 0.0f}); + m_outlinePrimitive->RootVisual().RelativeSizeWithOffset({offset * 2, offset * 2}, {1.0f, 1.0f}); + + m_outlinePrimitive->finalize(outlineLayoutMetrics(), outlineMetrics); + } } if (m_componentHostingFocusVisual) { @@ -581,6 +613,67 @@ facebook::react::LayoutMetrics ComponentView::focusLayoutMetrics(bool inner) con return layoutMetrics; } +facebook::react::LayoutMetrics ComponentView::outlineLayoutMetrics() const noexcept { + auto &props = *viewProps(); + auto offset = (pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor) + + std::round(viewProps()->outlineOffset * m_layoutMetrics.pointScaleFactor)) / + m_layoutMetrics.pointScaleFactor; + facebook::react::LayoutMetrics layoutMetrics = m_layoutMetrics; + layoutMetrics.frame.origin.x -= offset; + layoutMetrics.frame.origin.y -= offset; + layoutMetrics.frame.size.height += offset * 2; + layoutMetrics.frame.size.width += offset * 2; + return layoutMetrics; +} + +facebook::react::BorderMetrics ComponentView::outlineBorderMetrics() const noexcept { + auto &props = *viewProps(); + + facebook::react::BorderMetrics metrics = BorderPrimitive::resolveAndAlignBorderMetrics(m_layoutMetrics, props); + metrics.borderColors.bottom = metrics.borderColors.left = metrics.borderColors.right = metrics.borderColors.top = + props.outlineColor; + + auto offset = pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor) + + std::round(viewProps()->outlineOffset * m_layoutMetrics.pointScaleFactor); + + if (metrics.borderRadii.bottomLeft.horizontal) + metrics.borderRadii.bottomLeft.horizontal = std::max(0.0f, metrics.borderRadii.bottomLeft.horizontal + offset); + if (metrics.borderRadii.bottomLeft.vertical) + metrics.borderRadii.bottomLeft.vertical = std::max(0.0f, metrics.borderRadii.bottomLeft.vertical + offset); + if (metrics.borderRadii.bottomRight.horizontal) + metrics.borderRadii.bottomRight.horizontal = std::max(0.0f, metrics.borderRadii.bottomRight.horizontal + offset); + if (metrics.borderRadii.bottomRight.vertical) + metrics.borderRadii.bottomRight.vertical = std::max(0.0f, metrics.borderRadii.bottomRight.vertical + offset); + if (metrics.borderRadii.topLeft.horizontal) + metrics.borderRadii.topLeft.horizontal = std::max(0.0f, metrics.borderRadii.topLeft.horizontal + offset); + if (metrics.borderRadii.topLeft.vertical) + metrics.borderRadii.topLeft.vertical = std::max(0.0f, metrics.borderRadii.topLeft.vertical + offset); + if (metrics.borderRadii.topRight.horizontal) + metrics.borderRadii.topRight.horizontal = std::max(0.0f, metrics.borderRadii.topRight.horizontal + offset); + if (metrics.borderRadii.topRight.vertical) + metrics.borderRadii.topRight.vertical = std::max(0.0f, metrics.borderRadii.topRight.vertical + offset); + + static_assert( + facebook::react::BorderStyle::Solid == + static_cast(facebook::react::OutlineStyle::Solid)); + static_assert( + facebook::react::BorderStyle::Dotted == + static_cast(facebook::react::OutlineStyle::Dotted)); + static_assert( + facebook::react::BorderStyle::Dashed == + static_cast(facebook::react::OutlineStyle::Dashed)); + assert( + props.outlineStyle == facebook::react::OutlineStyle::Solid || + props.outlineStyle == facebook::react::OutlineStyle::Dotted || + props.outlineStyle == facebook::react::OutlineStyle::Dashed); + metrics.borderStyles.bottom = metrics.borderStyles.left = metrics.borderStyles.right = metrics.borderStyles.top = + static_cast(props.outlineStyle); + + metrics.borderWidths.bottom = metrics.borderWidths.left = metrics.borderWidths.right = metrics.borderWidths.top = + pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor); + return metrics; +} + facebook::react::BorderMetrics ComponentView::focusBorderMetrics( bool inner, const facebook::react::LayoutMetrics &layoutMetrics) const noexcept { @@ -934,6 +1027,9 @@ void ComponentView::indexOffsetForBorder(uint32_t &index) const noexcept { if (m_borderPrimitive) { index += m_borderPrimitive->numberOfVisuals(); } + if (m_outlinePrimitive) { + index += 1; + } } void ComponentView::OnRenderingDeviceLost() noexcept {} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 242627ff558..9e12a017219 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -157,6 +157,9 @@ struct ComponentView : public ComponentViewT< facebook::react::BorderMetrics focusBorderMetrics(bool inner, const facebook::react::LayoutMetrics &layoutMetrics) const noexcept; + facebook::react::LayoutMetrics outlineLayoutMetrics() const noexcept; + facebook::react::BorderMetrics outlineBorderMetrics() const noexcept; + virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual visualToHostFocus() noexcept; virtual winrt::com_ptr focusVisualRoot(const facebook::react::Rect &focusRect) noexcept; @@ -168,6 +171,7 @@ struct ComponentView : public ComponentViewT< winrt::com_ptr m_componentHostingFocusVisual; // The component that we are showing our focus visuals within std::shared_ptr m_borderPrimitive; + std::shared_ptr m_outlinePrimitive; std::unique_ptr m_focusPrimitive{nullptr}; winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_outerVisual{nullptr}; winrt::event> m_themeChangedEvent; From 173a9688faa9aa5b0a69b9a2e1b912be88be0efb Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:30:12 -0700 Subject: [PATCH 2/8] Change files --- ...ative-windows-202bc094-b9c7-41dc-9154-c7e8236c58c6.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-202bc094-b9c7-41dc-9154-c7e8236c58c6.json diff --git a/change/react-native-windows-202bc094-b9c7-41dc-9154-c7e8236c58c6.json b/change/react-native-windows-202bc094-b9c7-41dc-9154-c7e8236c58c6.json new file mode 100644 index 00000000000..19b09733082 --- /dev/null +++ b/change/react-native-windows-202bc094-b9c7-41dc-9154-c7e8236c58c6.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement outline properties on view", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From f32230e33a4a7c601240e5bbd112c60786bcea7d Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:10:44 -0700 Subject: [PATCH 3/8] hostPlatformColorIsColorMeaningful impl --- .../Fabric/Composition/BorderPrimitive.cpp | 18 +++--------------- .../renderer/graphics/HostPlatformColor.h | 3 +++ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp index d1b3f584134..bbc912009e1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp @@ -9,18 +9,6 @@ namespace winrt::Microsoft::ReactNative::Composition::implementation { -// Ideally isColorMeaningful would be sufficient here. But it appears to detect platformColors as not meaningful -// https://github.com/microsoft/react-native-windows/issues/14006 -bool isColorMeaningful( - const facebook::react::SharedColor &color, - winrt::Microsoft::ReactNative::Composition::implementation::Theme *theme) noexcept { - if (!color) { - return false; - } - - return theme->Color(*color).A > 0; -} - // We don't want half pixel borders, or border radii - they lead to blurry borders // Also apply scale factor to the radii at this point void pixelRoundBorderRadii(facebook::react::BorderRadii &borderRadii, float scaleFactor) noexcept { @@ -354,7 +342,7 @@ void SetBorderLayerPropertiesCommon( // Clear with transparency pRT->Clear(); - if (!isColorMeaningful(borderColor, theme)) { + if (!facebook::react::isColorMeaningful(borderColor)) { return; } @@ -738,7 +726,7 @@ bool BorderPrimitive::requiresBorder( auto borderStyle = borderMetrics.borderStyles.left; bool hasMeaningfulColor = - !borderMetrics.borderColors.isUniform() || isColorMeaningful(borderMetrics.borderColors.left, theme); + !borderMetrics.borderColors.isUniform() || facebook::react::isColorMeaningful(borderMetrics.borderColors.left); bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); if (!hasMeaningfulColor || !hasMeaningfulWidth) { return false; @@ -828,7 +816,7 @@ bool BorderPrimitive::TryUpdateSpecialBorderLayers( auto borderStyle = borderMetrics.borderStyles.left; bool hasMeaningfulColor = - !borderMetrics.borderColors.isUniform() || !isColorMeaningful(borderMetrics.borderColors.left, theme); + !borderMetrics.borderColors.isUniform() || !facebook::react::isColorMeaningful(borderMetrics.borderColors.left); bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); if (!hasMeaningfulColor && !hasMeaningfulWidth) { return false; diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/graphics/HostPlatformColor.h b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/graphics/HostPlatformColor.h index 4fa717b94e6..163b0573258 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/graphics/HostPlatformColor.h +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/graphics/HostPlatformColor.h @@ -99,6 +99,9 @@ inline ColorComponents colorComponentsFromHostPlatformColor(Color color) { // windows inline bool hostPlatformColorIsColorMeaningful(Color color) noexcept { + if (color.m_platformColor.size()) + return true; + auto windowsColor = color.AsWindowsColor(); return windowsColor.A > 0; } From 0dc04ebdd072e292f580f38c3c58272799174611 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:10:52 -0700 Subject: [PATCH 4/8] update snapshots --- ...w-tests-views-can-have-outlines-2-snap.png | Bin 6128 -> 8632 bytes .../ViewComponentTest.test.ts.snap | 521 ++++++++++++++++++ 2 files changed, 521 insertions(+) diff --git a/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-views-can-have-outlines-2-snap.png b/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-views-can-have-outlines-2-snap.png index 43f3d987dadd1552121337698ba346d85b97878f..df187b8b0759ac5da3fc4c410e557ed4663f7a0b 100644 GIT binary patch literal 8632 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 diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap index 72cb791bd75..feb3d328762 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/ViewComponentTest.test.ts.snap @@ -3770,6 +3770,53 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, -16", "Visual Type": "SpriteVisual", }, + { + "Offset": "-8, -8, 0", + "Size": "16, 16", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "8, 0, 0", + "Size": "-16, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 0, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 8, 0", + "Size": "8, -16", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, -8, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "8, -8, 0", + "Size": "-16, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, -8, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 8, 0", + "Size": "8, -16", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -3856,6 +3903,85 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, -6", "Visual Type": "SpriteVisual", }, + { + "Offset": "-8, -8, 0", + "Size": "16, 16", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "0, 0, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "36, 0, 0", + "Size": "-6, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "-36, 0, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "-8, 36, 0", + "Size": "8, -6", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "-36, -36, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "36, -8, 0", + "Size": "-6, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "0, -36, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "0, 36, 0", + "Size": "8, -6", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -3942,6 +4068,85 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, 14", "Visual Type": "SpriteVisual", }, + { + "Offset": "-8, -8, 0", + "Size": "16, 16", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "0, 0, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "36, 0, 0", + "Size": "22, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "-8, 0, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "-8, 8, 0", + "Size": "8, 22", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "-36, -36, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "8, -8, 0", + "Size": "22, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "0, -8, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(128, 0, 128, 255)", + }, + "Offset": "0, 36, 0", + "Size": "8, 22", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -3996,6 +4201,53 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, -16", "Visual Type": "SpriteVisual", }, + { + "Offset": "-13, -13, 0", + "Size": "26, 26", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "8, 0, 0", + "Size": "-16, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 0, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 8, 0", + "Size": "8, -16", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, -8, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "8, -8, 0", + "Size": "-16, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, -8, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 8, 0", + "Size": "8, -16", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -4082,6 +4334,53 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, -6", "Visual Type": "SpriteVisual", }, + { + "Offset": "-8, -8, 0", + "Size": "16, 16", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "36, 0, 0", + "Size": "-72, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-36, 0, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 36, 0", + "Size": "8, -72", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-36, -36, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "36, -8, 0", + "Size": "-72, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, -36, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 36, 0", + "Size": "8, -72", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -4168,6 +4467,53 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, 14", "Visual Type": "SpriteVisual", }, + { + "Offset": "-8, -8, 0", + "Size": "16, 16", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "36, 0, 0", + "Size": "-44, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 0, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-8, 8, 0", + "Size": "8, -44", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-36, -36, 0", + "Size": "36, 36", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "8, -8, 0", + "Size": "-44, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, -8, 0", + "Size": "8, 8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 36, 0", + "Size": "8, -44", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -4254,6 +4600,85 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "8, -16", "Visual Type": "SpriteVisual", }, + { + "Offset": "-8, -8, 0", + "Size": "16, 16", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "0, 0, 0", + "Size": "66, 41", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "66, 0, 0", + "Size": "-16, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "-66, 0, 0", + "Size": "66, 41", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "-8, 41, 0", + "Size": "8, -16", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "-66, -41, 0", + "Size": "66, 41", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "66, -8, 0", + "Size": "-16, 8", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "0, -41, 0", + "Size": "66, 41", + "Visual Type": "SpriteVisual", + }, + { + "Brush": { + "Brush Type": "ColorBrush", + "Color": "rgba(255, 165, 0, 255)", + }, + "Offset": "0, 41, 0", + "Size": "8, -16", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -4308,6 +4733,53 @@ exports[`View Tests Views can have outlines 1`] = ` "Size": "12, -24", "Visual Type": "SpriteVisual", }, + { + "Offset": "4, 4, 0", + "Size": "-8, -8", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "4, 4", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "4, 0, 0", + "Size": "-8, 4", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-4, 0, 0", + "Size": "4, 4", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-4, 4, 0", + "Size": "4, -8", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-4, -4, 0", + "Size": "4, 4", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "4, -4, 0", + "Size": "-8, 4", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, -4, 0", + "Size": "4, 4", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 4, 0", + "Size": "4, -8", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, ], @@ -4321,6 +4793,55 @@ exports[`View Tests Views can have outlines 1`] = ` "Offset": "0, 0, 0", "Size": "50, 50", "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "-9, -9, 0", + "Size": "18, 18", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "9, 9", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "9, 0, 0", + "Size": "-18, 9", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-9, 0, 0", + "Size": "9, 9", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-9, 9, 0", + "Size": "9, -18", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "-9, -9, 0", + "Size": "9, 9", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "9, -9, 0", + "Size": "-18, 9", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, -9, 0", + "Size": "9, 9", + "Visual Type": "SpriteVisual", + }, + { + "Offset": "0, 9, 0", + "Size": "9, -18", + "Visual Type": "SpriteVisual", + }, + ], + }, + ], }, ], }, From 994d43266dda5b98004893b13f6bb2218ee3b73c Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:42:20 -0700 Subject: [PATCH 5/8] fix background clip --- .../CompositionViewComponentView.cpp | 55 ++++++++++++------- .../CompositionViewComponentView.h | 2 + .../Fabric/Composition/ImageComponentView.cpp | 5 ++ .../Fabric/Composition/ImageComponentView.h | 1 + 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index c74b5e8d32e..211a4459ec1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -38,10 +38,11 @@ constexpr float FOCUS_VISUAL_RADIUS = 3.0f; // m_outerVisual // | -// ----- m_visual <-- Background / clip - Can be a custom visual depending on Component type +// ----- m_visual - Can be a custom visual depending on Component type // | -// ----- Border Visuals x N (BorderPrimitive attached to m_visual) -// ----- Outline Visuals x N(BorderPrimitive) +// ----- m_backgroundVisual <-- Background / clip (ComponentViewFeatures::Background) +// ----- Border Visuals x N (BorderPrimitive attached to m_visual) (ComponentViewFeatures::NativeBorder) +// ----- Outline Visuals x N(BorderPrimitive) (ComponentViewFeatures::NativeBorder) // ----- (default: directly in m_visual after border visuals) // ----- m_childrenContainer (created on demand when overflow:hidden, children moved here) // ------Focus Visual Container (created when hosting focus visuals) @@ -80,13 +81,8 @@ facebook::react::Props::Shared ComponentView::props() noexcept { } void ComponentView::onThemeChanged() noexcept { - if ((m_flags & ComponentViewFeatures::Background) == ComponentViewFeatures::Background) { - if (viewProps()->backgroundColor) { - Visual().as().Brush(theme()->Brush(*viewProps()->backgroundColor)); - } else { - Visual().as().Brush(nullptr); - } - } + if (m_backgroundVisual) + m_backgroundVisual.Brush(theme()->Brush(*viewProps()->backgroundColor)); if (m_borderPrimitive) { m_borderPrimitive->onThemeChanged( @@ -158,10 +154,16 @@ void ComponentView::updateProps( if ((m_flags & ComponentViewFeatures::Background) == ComponentViewFeatures::Background) { if (oldViewProps.backgroundColor != newViewProps.backgroundColor) { - if (newViewProps.backgroundColor) { - Visual().as().Brush(theme()->Brush(*newViewProps.backgroundColor)); + if (facebook::react::isColorMeaningful(newViewProps.backgroundColor)) { + if (!m_backgroundVisual) { + m_backgroundVisual = m_compContext.CreateSpriteVisual(); + m_backgroundVisual.RelativeSizeWithOffset({0, 0}, {1.0f, 1.0f}); + Visual().InsertAt(m_backgroundVisual, 0); + } + m_backgroundVisual.Brush(theme()->Brush(*newViewProps.backgroundColor)); + // todo set clipping? } else { - Visual().as().Brush(nullptr); + m_backgroundVisual.Brush(nullptr); } } } @@ -214,8 +216,9 @@ void ComponentView::updateProps( void ComponentView::updateLayoutMetrics( facebook::react::LayoutMetrics const &layoutMetrics, facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept { + updateClippingPath(layoutMetrics, *viewProps()); + if ((m_flags & ComponentViewFeatures::NativeBorder) == ComponentViewFeatures::NativeBorder) { - updateClippingPath(layoutMetrics, *viewProps()); OuterVisual().Size( {layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor, layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor}); @@ -322,7 +325,9 @@ void ComponentView::FinalizeUpdates(winrt::Microsoft::ReactNative::ComponentView auto outlineMetrics = outlineBorderMetrics(); if (!m_outlinePrimitive && BorderPrimitive::requiresBorder(outlineMetrics, theme())) { m_outlinePrimitive = std::make_shared(*this); - Visual().InsertAt(m_outlinePrimitive->RootVisual(), m_borderPrimitive ? m_borderPrimitive->numberOfVisuals() : 0); + Visual().InsertAt( + m_outlinePrimitive->RootVisual(), + (m_backgroundVisual ? 1 : 0) + (m_borderPrimitive ? m_borderPrimitive->numberOfVisuals() : 0)); } if (m_outlinePrimitive) { @@ -747,7 +752,7 @@ void ComponentView::hostFocusVisual(bool show, winrt::com_ptr vie assert( view.get() == this); // When not using lifted comp, focus visuals should always host within their own component - OuterVisual().InsertAt(m_focusPrimitive->m_focusVisual, 1); + OuterVisual().InsertAt(m_focusPrimitive->m_focusVisual, (m_backgroundVisual ? 2 : 1)); } } @@ -991,9 +996,18 @@ void ComponentView::Toggle() noexcept { // no-op } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ComponentView::VisualToApplyBackgroundClipTo() const noexcept +{ + return m_backgroundVisual; +} + void ComponentView::updateClippingPath( facebook::react::LayoutMetrics const &layoutMetrics, const facebook::react::ViewProps &viewProps) noexcept { + auto clipTarget = VisualToApplyBackgroundClipTo(); + if (!clipTarget) + return; + auto borderMetrics = BorderPrimitive::resolveAndAlignBorderMetrics(layoutMetrics, viewProps); bool hasRoundedCorners = borderMetrics.borderRadii.topLeft.horizontal != 0 || @@ -1012,10 +1026,11 @@ void ComponentView::updateClippingPath( winrt::com_ptr pathGeometry = BorderPrimitive::GenerateRoundedRectPathGeometry( m_compContext, borderMetrics.borderRadii, {0, 0, 0, 0}, {0, 0, viewWidth, viewHeight}); - Visual().as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( + clipTarget.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( pathGeometry.get()); } else { - Visual().as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath(nullptr); + clipTarget.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( + nullptr); } } @@ -1180,7 +1195,7 @@ void ViewComponentView::ensureVisual() noexcept { } else { m_visual = createVisual(); } - OuterVisual().InsertAt(m_visual, 0); + OuterVisual().InsertAt(m_visual, m_backgroundVisual ? 1 : 0); } } @@ -1478,7 +1493,7 @@ void ViewComponentView::updateChildrenClippingPath( } // Insert m_childrenContainer after border visuals in m_visual - Visual().InsertAt(m_childrenContainer, borderCount); + Visual().InsertAt(m_childrenContainer, (m_backgroundVisual ? 1 : 0) + borderCount); // Use relative sizing so container automatically tracks parent's size m_childrenContainer.RelativeSizeWithOffset({0, 0}, {1, 1}); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 9e12a017219..4340bcb7b94 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -130,6 +130,7 @@ struct ComponentView : public ComponentViewT< void ThemeChanged(winrt::event_token const &token) noexcept; protected: + virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToApplyBackgroundClipTo() const noexcept; bool anyHitTestHelper( facebook::react::Tag &targetTag, facebook::react::Point &ptContent, @@ -141,6 +142,7 @@ struct ComponentView : public ComponentViewT< winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext; comp::CompositionPropertySet m_centerPropSet{nullptr}; facebook::react::SharedViewEventEmitter m_eventEmitter; + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual m_backgroundVisual { nullptr }; private: void updateFocusLayoutMetrics() noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp index cfdac7fe3f8..7c7c9970972 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp @@ -236,6 +236,11 @@ void ImageComponentView::onThemeChanged() noexcept { Super::onThemeChanged(); } +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ImageComponentView::VisualToApplyBackgroundClipTo() const noexcept +{ + return Visual(); +} + void ImageComponentView::ensureDrawingSurface() noexcept { assert(m_reactContext.UIDispatcher().HasThreadAccess()); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h index 1b89d302c36..422b5117c0a 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h @@ -52,6 +52,7 @@ struct ImageComponentView : ImageComponentViewT Date: Thu, 30 Apr 2026 09:16:04 -0700 Subject: [PATCH 6/8] default black borders should still be rendered. --- .../Fabric/Composition/BorderPrimitive.cpp | 3 ++- .../Fabric/Composition/CompositionViewComponentView.cpp | 7 +++---- .../Fabric/Composition/CompositionViewComponentView.h | 5 +++-- .../Fabric/Composition/ImageComponentView.cpp | 4 ++-- .../Fabric/Composition/ImageComponentView.h | 3 ++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp index bbc912009e1..8f97bb36adb 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp @@ -725,8 +725,9 @@ bool BorderPrimitive::requiresBorder( // We only handle a single borderStyle for now auto borderStyle = borderMetrics.borderStyles.left; + // A null border color will get replaced with black, so treat it as meaningful for this check bool hasMeaningfulColor = - !borderMetrics.borderColors.isUniform() || facebook::react::isColorMeaningful(borderMetrics.borderColors.left); + !borderMetrics.borderColors.isUniform() || borderMetrics.borderColors.left == nullptr || facebook::react::isColorMeaningful(borderMetrics.borderColors.left); bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); if (!hasMeaningfulColor || !hasMeaningfulWidth) { return false; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 211a4459ec1..0ac12db6c0d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -996,8 +996,8 @@ void ComponentView::Toggle() noexcept { // no-op } -winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ComponentView::VisualToApplyBackgroundClipTo() const noexcept -{ +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ComponentView::VisualToApplyBackgroundClipTo() + const noexcept { return m_backgroundVisual; } @@ -1029,8 +1029,7 @@ void ComponentView::updateClippingPath( clipTarget.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( pathGeometry.get()); } else { - clipTarget.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath( - nullptr); + clipTarget.as<::Microsoft::ReactNative::Composition::Experimental::IVisualInterop>()->SetClippingPath(nullptr); } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 4340bcb7b94..5597ea72d92 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -130,7 +130,8 @@ struct ComponentView : public ComponentViewT< void ThemeChanged(winrt::event_token const &token) noexcept; protected: - virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToApplyBackgroundClipTo() const noexcept; + virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual VisualToApplyBackgroundClipTo() + const noexcept; bool anyHitTestHelper( facebook::react::Tag &targetTag, facebook::react::Point &ptContent, @@ -142,7 +143,7 @@ struct ComponentView : public ComponentViewT< winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext m_compContext; comp::CompositionPropertySet m_centerPropSet{nullptr}; facebook::react::SharedViewEventEmitter m_eventEmitter; - winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual m_backgroundVisual { nullptr }; + winrt::Microsoft::ReactNative::Composition::Experimental::ISpriteVisual m_backgroundVisual{nullptr}; private: void updateFocusLayoutMetrics() noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp index 7c7c9970972..1eef1c4d5a0 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp @@ -236,8 +236,8 @@ void ImageComponentView::onThemeChanged() noexcept { Super::onThemeChanged(); } -winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ImageComponentView::VisualToApplyBackgroundClipTo() const noexcept -{ +winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ImageComponentView::VisualToApplyBackgroundClipTo() + const noexcept { return Visual(); } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h index 422b5117c0a..9a15475c04f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h @@ -52,7 +52,8 @@ struct ImageComponentView : ImageComponentViewT Date: Thu, 30 Apr 2026 10:03:01 -0700 Subject: [PATCH 7/8] fix border sometimes being created behind the background --- ...t-ts-view-tests-view-box-sizing-2-snap.png | Bin 2585 -> 2670 bytes ...w-tests-views-can-have-outlines-2-snap.png | Bin 8632 -> 11967 bytes .../ButtonComponentTest.test.ts.snap | 182 ++- .../FlatListComponentTest.test.ts.snap | 1336 ++++++++++++----- .../PointerButtonComponentTest.test.ts.snap | 30 +- .../PressableComponentTest.test.ts.snap | 295 ++-- .../TextComponentTest.test.ts.snap | 60 +- .../ViewComponentTest.test.ts.snap | 666 +++++--- .../Fabric/Composition/BorderPrimitive.cpp | 12 +- .../Fabric/Composition/BorderPrimitive.h | 3 +- .../CompositionViewComponentView.cpp | 11 +- .../CompositionViewComponentView.h | 3 + 12 files changed, 1846 insertions(+), 752 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-view-box-sizing-2-snap.png b/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-view-box-sizing-2-snap.png index b573d2d449f68f1389ffdccf47d8e37be8fa91a2..f40f56c19c33dbacb5455a4e54cceaea10d7fe35 100644 GIT binary patch literal 2670 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 diff --git a/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-views-can-have-outlines-2-snap.png b/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-views-can-have-outlines-2-snap.png index df187b8b0759ac5da3fc4c410e557ed4663f7a0b..2273cc73ba05ccf685d620c097c465eb27a84e5b 100644 GIT binary patch literal 11967 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 iY!hG7V7ah!cq4=>Gu~mrL~k literal 8632 zcmcIq2{@E{`R!@SpP=6Qb4a^KJL25D(rqoHJ_BqAcBQB_gWAtEC2 z2HsyiOb+}`wBIrZ{v2}Fxu!sb?qHt-E=aB9!SX~zg`reCW@Nzi5eF50XCfln2Eyx5 z6D<1<5z#5Is*?P5cgR8_GK_KhY2Y>U8#@ipOI`Ecojak@n_H@FE_$)@NnTjCHGg>6 z{p_6Xhe2;hZ6k?=|G3I=|H&aT@)LLN z1jH6Qqmfk|(!22pSSPY-y9S?>HJ?*dJ%aRFVyWCl;yU1BVP+>5%d7B_ms_o@m&g?=O^L7YLI(HYi_2+xMB`2O=`!8;(8NgCF zRcHW#)RmUl;s7)8(^BbOx3f7#9zT>56R@fA@jNL4=qEHAr{BpMF#h=SV}(blp1nfn ziAPE@*2w35dI8;Er9dsEp(g?^zrIW<5HPRZkbM~56OLuerqKEED1WK@Zv?CSK~oct z^|rR_F0mdf3KW$o2^g)~6C83H6d&@KsVLdMOk!qv2G+X$x%S@UU@DW$k$5+XYx$7# z3y+*9eeW9R$TbWE#2xkZJPGzr3U$-c?185-gVN9zR^5tJnyDa-J?gf{Qcm4Z;2k3$ zyOxsoq({CCag?~?4CY7cE{l6Iu3#+FP#UdknN=@AP$uM4bo`fmIA)5RRie-*B)&^7 zFt|Zl^>t;6Iora%DWg5Y?T`p8MiG=>5d`sCyktyYU%dXO?jmeA;SY~$XuesOY$2ph&ozgqZ5=C+(f$4=4jyZ zOTM6Xe~1Z|I zmYkXhTRN<>ag7NXWN*|3R-=@QxNmzL@Kjz{$D)S8=4i{Xm@99DT4!yc&yXjx_UgNf zO1M3xk}N)zajxNLy3s8*%*13i6hZ>7*o{!;XVtrTQ($B}2eGYIvX;0#TYsa!i=}vv zEu@p;3Y*nbZbEFa-UaJ>eb)S4BD#hbQVMNo8XT^=Url`*c3dOmg_f{r4%FagH5d^p zOJ=F6h#pxUu99a(y{iOSurUMSOWbuPmDL}}jti!(NdoYR5(cxV45{1qNP^?K5gZkH zK8>AMqRL9Q@%E;*pahqpUUG>UPpPQfymOweb*czaX@wWqVWs`;SE1wX&*eKh%m<2X z9Pg_4k+N$3#(MJ)(IJAiNmb#q+3BVs*ExiFqx~;i1nVLBn#xn+qmFx@JJQEXx+XWo zMSD-Zvj)%3A+pIEa~ezmW{GT_psHQXWUu)+xzM}Sfm{vrKW(~a@HsBfE1d0gXrCM0 z)m{2am&&5J_oQ+zW7j~*_jFgGKgci9*AD_Up?&pn%{i_484EQvDXw*UaJi57 z`x)qhr$R`?A#V&01%v;DstgAwBmcBQ)eC3wxZo7D=FFvM>LROTtOx#IB~3LT_)zdD*|7lJwkcB`ZHtID)7Y9X2}gzA`2?r!LLqZ$#A|`%ESmio`QGU!hD?{*ccpK1 zPghJ5g)kT1lr!)7j+>|W64=#{hb$Nf5`DPrd~Mg~KGZ8kn`o4{U)0uR&_*B}GA@0v z_aK18MWNBEd%ouRL?=dRFyWWhdg~#*2p5kl=9MHQLQt|6${Mjpe06fKNzQa zG^iqh33f{zEE$)LL~ZT|xEqcJ;-}526Qqt}XIt|SFUha{!RhsZs--LX>FQa*>{?+% z&)M|$M|1sD5r~x5)P9aXnua~86EEVaD`!(5S$}$1*jxVWiJ;fU?YQNM$b`!#iLr*S zirLCwhl{iq>sI!YUpB)~bLr7DA@xnHYwxm$3oFR`68_wqLZSp``SlLKmD%9&8QYeDi?n1pD{dY zi@4>Sl6k~>bg-KY6;7F5MrsgNvN3S_+Qd+cWk-V0=xQcNi^=9&f|2HdQtWg9FfXA_ z8rk5_&EoLt{H#=mnJ^Dckfp~)L4+&ZjBrh4h_itpr zge>o8AkYEKdLJO*y#kkon_C-GJ*G9p*FtI6RSu#3FCD&e*-DcVw4EmJk#WF4a+*g= zYQ2k8Pi0ed(|@LF*G%%bl*=;>lj`<`rc!%cQ=jMPpO}2$TFv@toRjO|b2i;8^;D}_ zOEV&C7$Hqz`-vg-OrMh=|OZ#B&$ z-eg0UjK}dl5{i~SRlV?t)FQl*45(Cyq~*H^?jYo|S^+?m1yPZ8gDPz3prV2uv4;z8 zYa#XvF{V)b#t`$7mNVc-y8@w@2NjBD0k^R&Y5Ab@#?(%l6)$a4aX6^z)Jo)oUYA%k z4l0A6X(bb|#SwtIcg>nQ(f3OgHev6JHr5);%ED0$<+!Y9a<;0b_q8fog1>s5G@8Gb z6x-5@dLQ*R|3j%u3FB`3t8p<=ExfLxU*1Sh`P4m$DFdVU_U9n_NVPcfZG&ixS$`8k zj#Boz|Ncxd6EhN7Z$5o-Wk6eVD;&`(twCaJXrjiQoTZe3ZZ(>})%7#<@e{D*LF2h# zwM9K%*sR+$1?n3YKi7gh{!ZP)+jO5mn{Y+10@a%i_K|!` z(dXFJjjQ%q>=wc}P+98;888XD!DWztJDg%V{9OAefHaT9=zL3`td)Q6S+zRlzAE77 zT%*BitoILXPx+rWuCAX33(`hJGtLDBvDTQAWTp?&E`l~??7enpX+th$|gg5<73t<(R!??w= zwffQK6zKdva=7Clyg|A^gSW0z5Gqd6!c@LFUq{APnSOrjb&B1ukaf&5Kb8hJHH{y% zps|u0TwbnvpHS?1+*pXE-yAthr{XEmTv13rgh>eO&>8n0CC?lYj^E6 z*R*Hy@yevyC?y%qAQj{2ir&H$pel>U3Q=3#w>oY>tvWt+Yo3ZX{%A0mH5xmn?hV+@ zVn)A#MXT%;Rt-%n3cq$^)n9(_*Bs{FaUA3_ORV0xw30hc&1*P%A-Vd}KsU!oCpBp6 z(mk$&<7Bw+GL~Vzy)9)~+tCj1BC^FRNTq^Q$>(Vx z>XviAkRH=`H`!RA%d+UgyS=fa1jEr*L2nggN@4|NXs|6fMUxYNM@dKGnV+sLuM}RG zFu~yxgE}=c{2Dn)w_~FbV>O$prgSf74YmyEH>J*UyBN>tH2MBq$C~E=P^=&f#FRv+ z3@!FZ`xaj`H6V>xk^;py(8X~B!%S`w@e8m#`n;|Gmg~cK16BDIl9nhul^E(2U^+2T zFV4Sodz4gp6dMjHoO);qJuRpw?ZWseXLXbZL|x|k2%VmP_DQEMw@$x>;BN?}k-V3; z3ou{~REvh7>EDFcRgKp!%LKr4X;)U>+Y2RR`g{UiQlLu)kGkZkfq_|2R+|*r`pp(u zs(vNg?0*#Fg@;5aR|#LldR>taUBZI1&(4y~9`VHj%J2cznZf_b#dZaiYd{DL zjUHTUymO0ZslK1nj_#f#Q60&O?xYMEBEhYRZIR0>13LRZ5>EP6t6q^N@T>2KPdocI zd1td-e~h|J`24h_n-|5p_!2U7?!j0;a<0ykCiV2MoSuH)Y?D2=vd}UkaZ|Hu&o0aUqb2QIS5u)XL z4J?lXJWXiR1c<-h`0o8)kxVA$R149nTa`{Pj zKMmh5dLNKJ(@S`qsjhIIL$vpOVO08K7Vh3`#=N2!Vm+6R1skjOCc!>`|>OIWCnn&@bS` zHM*~OiwD*d@~Kk*;F@U;*Zu-iHMbNA9WLI!=uEmKcDuQ#MKINzZa>rhnv(0p3WJ~Y zW2e~ViQ^L1pX3XYJu2`!msN<3)qYd4So<2Kd}j}X{1_tKeL)EOH_O@WPWROZ&>nK3 zT=hA#MHpB803$JVJC^{UoS+#@HUtdaikN25BR8xh>3gyzS6IvLWKXhS=y|mu6T#_u z?@aCs4hbt>7q|;4f(41`8@aKZ3i9BZT|nymmfVyXrij0_pp!*&0aa#+U9BLxZ{&M- zZ?fb!o?Xr5RBqCIRXWfbeFt^dkvvjhX^J@lbs_*u@o;VWaowury~SbER#Iz2wYnHt zb}V-J^|R6%kpWDX1FXe($FZ{lubPUR`wiTK!>9-Zb5N76?1JhgQvE0xTwKTut%&+L z#gAH1+10KLnwk@0&l0TjOYGgQ_Fw?VKZF+U;9YhWlx{jK_(V{=w)4=djoxVxD$&<{ z4!Vp$JuC4tDsZ_3_bQL0t=iMNWg=(vdNgjdMYvqFf?z@wuE@!sG@RVf@*#UM$Oh##l_}Q@SjQ zZeARo7~+k9!#<9hLV;lH&gH{KE9Tp$(0yFD94_M?khn4=CL@1gE78B~AFWgc!r}lA8 zp4gafmiAIJMVSPyp*yn{LJ>xDRb6+GjH2eVD8=KT(Y4NgGDo_Gj+6o`-h6SxUs;Mv9S zSC3DcNFgz|k<--R*^SZ6aJ%a$5P53pohl9d!?<}+S>5qZ<4o6e;}|oyE={kG+J&C! zkG>6uAp^XMXN_WButRMw-fB(*f$-soxN{PIwNbO3#YNZAx3YgOwbO!}2#j&il3GkA zFcv`Gj5kE=x@gI`vWz(tR+kQD;k3Hc?yssIPxskN=kj2BY*>%oeRACYCIH;;N53 zwz<@4*#X~%YsoOl4rII;E1RCql^h#o`IJDEV(DY^#Pt_LQ&%D?=Mt(O?F*Pf_O^>- z^E=>M-#(te!wr$hbW_(%P$=Q4i0)0V>@MMb2_iv5Q)aF-<1>vm;$KMK+@ZBx2n~d| z@g{mURKzv(X^!1Q@x;*Wq2Hd!|p>bcLHA z$yw5DeN&w~Bfq3(gaD%KRUEQ;4F0)lcEf)PnE%~IUVis}S*9AqmEQY;GovCfp#(@B z&+B?|$9ET6)8^_sCB|#tq(MOoqvc^GW3NmjMYl>V-DYR%Hrz8Xv)J-4QhJ_O7hOWqhM*g;K&`v(cO9bv9ZPjo{f- zrhSepZUS4UAYsGDGw_c)j7#qB+QX;bG?=Y>Mj?vk`+}6XV03X$1thqtliaP(xUs+b z!u?}6r_3;83v49}_>4J>d!2B81!Z()J8Shujc*Q>BC-14;^o_PyLmT%j1Ic}VMlC+ zf-i!sZ+#FLrM!A!X;icg9@<)$hVM?+`I=|*H1V@2FNw9MCmH8cw0|K8{>MZiG@tljio1U0GtKOFKq)ltAxZ!#4t=jpLACH}x6*zipSJE~CE0+%2 zn|eFJt2U&8urN3(6u>7-@dwzp3g_LEVJ!v4D zWW86{6(5`|b%%6XwLxB&eA-Jy1gu3n)|Lk97<}n3x?@frT)lE#ywM=+0717xI%xBG zI9Z!HC>*LE{4|vyt)x^okEjgbqAtd7=$b|ePYnghekMCqv~lP5cQ*ejon92gZ6NN= zne4N2W+A9YBZDx&HtQy__|lxa70xc*TQhlMXm9bJL6IjrP4c$A_@Zst!R&BE(xf#w z&p>W6=c>66I(F#Cp_etiaWF7_kMK4*R_M4`y!u67);l)5U}d$~d8zpQ=6c9U)clT5~&OqX;8 z-!?4CRy?m}d#^fC;$>dO%u80A#^tdYFC_h1!?(@UU^GSLDur;ZtuWJUT+WXZ*oWtO z`hZo8uLOI&1OV|!q$e_8&G7!#I%7Ih$@kRAG! zwcr|kt}6l73n$V-uqn2pDUbC%w-U2HdUh^BbJb*QQ%T;NJX;5c0>k6>XFNAU(;Y)0 zD*Ga9tpz5k?@G(h_0Lcz_+Q3ci}wrbqx<_^pK`=JA*(K4H3siGpNTrz_!3*4MPnO9OS;t4 zJRGsxOt8;uH1G);aTH`U3O|m7LDr%*A!c1=GUA@=DQ&7TOZ{O%G{U!aNOoO^>M#-b zeV$UO-4P>LMH4WKl0Q#^>8T=mzh8fTXnm2`c2p=30&!-@gLtq=SFTzSmb0_#$_=v< zgC@^bo^gQ{VY_w%h&|XIYKH~Z@D(-vLf GetGeometryForRoundedBorder( BorderPrimitive::BorderPrimitive( winrt::Microsoft::ReactNative::Composition::implementation::ComponentView &outer, const winrt::Microsoft::ReactNative::Composition::Experimental::IVisual &rootVisual) - : m_outer(&outer), m_rootVisual(rootVisual) {} + : m_outer(&outer), m_rootVisual(rootVisual), m_ownsRootVisual(false) {} BorderPrimitive::BorderPrimitive(winrt::Microsoft::ReactNative::Composition::implementation::ComponentView &outer) : m_outer(&outer), m_rootVisual(outer.CompositionContext().CreateSpriteVisual()) {} @@ -725,9 +725,8 @@ bool BorderPrimitive::requiresBorder( // We only handle a single borderStyle for now auto borderStyle = borderMetrics.borderStyles.left; - // A null border color will get replaced with black, so treat it as meaningful for this check bool hasMeaningfulColor = - !borderMetrics.borderColors.isUniform() || borderMetrics.borderColors.left == nullptr || facebook::react::isColorMeaningful(borderMetrics.borderColors.left); + !borderMetrics.borderColors.isUniform() || facebook::react::isColorMeaningful(borderMetrics.borderColors.left); bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0); if (!hasMeaningfulColor || !hasMeaningfulWidth) { return false; @@ -789,8 +788,10 @@ BorderPrimitive::FindSpecialBorderLayers() const noexcept { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; if (m_numBorderVisuals) { + auto borderInsertAtIndex = m_ownsRootVisual ? 0 : m_outer->borderInsertAtIndex(); + for (uint8_t i = 0; i < m_numBorderVisuals; i++) { - auto visual = m_rootVisual.GetAt(i); + auto visual = m_rootVisual.GetAt(i + borderInsertAtIndex); layers[i] = visual.as(); } } @@ -825,9 +826,10 @@ bool BorderPrimitive::TryUpdateSpecialBorderLayers( // Create the special border layers if they don't exist yet if (!spBorderVisuals[0]) { + auto borderInsertAtIndex = m_ownsRootVisual ? 0 : m_outer->borderInsertAtIndex(); for (uint8_t i = 0; i < SpecialBorderLayerCount; i++) { auto visual = m_outer->CompositionContext().CreateSpriteVisual(); - m_rootVisual.InsertAt(visual, i); + m_rootVisual.InsertAt(visual, i + borderInsertAtIndex); spBorderVisuals[i] = std::move(visual); m_numBorderVisuals++; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h index 47ad6578734..ff262ebe224 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h @@ -76,7 +76,8 @@ struct BorderPrimitive { uint8_t m_numBorderVisuals{0}; winrt::Microsoft::ReactNative::Composition::implementation::ComponentView *m_outer; winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_rootVisual{nullptr}; - bool m_needsUpdate{true}; + bool m_needsUpdate : 1 {true}; + bool m_ownsRootVisual : 1 {false}; }; } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 0ac12db6c0d..727a3cb4fa6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -161,9 +161,11 @@ void ComponentView::updateProps( Visual().InsertAt(m_backgroundVisual, 0); } m_backgroundVisual.Brush(theme()->Brush(*newViewProps.backgroundColor)); - // todo set clipping? + updateClippingPath(m_layoutMetrics, *viewProps()); } else { - m_backgroundVisual.Brush(nullptr); + if (m_backgroundVisual) { + m_backgroundVisual.Brush(nullptr); + } } } } @@ -996,6 +998,11 @@ void ComponentView::Toggle() noexcept { // no-op } +// This offset ensures that the m_borderPrimitive inserts its layers above the background +int32_t ComponentView::borderInsertAtIndex() const noexcept { + return m_backgroundVisual ? 1 : 0; +} + winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ComponentView::VisualToApplyBackgroundClipTo() const noexcept { return m_backgroundVisual; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index 5597ea72d92..b7fc0d4cb4c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -111,6 +111,9 @@ struct ComponentView : public ComponentViewT< bool getAcccessiblityIsReadOnly() noexcept override; ToggleState getToggleState() noexcept override; void Toggle() noexcept override; + + int32_t borderInsertAtIndex() const noexcept; + virtual winrt::Microsoft::ReactNative::implementation::ClipState getClipState() noexcept; virtual std::pair cursor() const noexcept; From e60aefb46ca58378b6f0f5637c5f72edc187142a Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:34:22 -0700 Subject: [PATCH 8/8] snapshots --- ...t-ts-view-tests-view-box-sizing-2-snap.png | Bin 2670 -> 2768 bytes .../AccessibilityTest.test.ts.snap | 136 ++++++--- .../ButtonComponentTest.test.ts.snap | 252 ++++++++-------- .../CustomAccessibilityTest.test.ts.snap | 15 +- .../FlatListComponentTest.test.ts.snap | 270 +++++++++--------- .../LegacyImageTest.test.ts.snap | 54 ++-- .../PressableComponentTest.test.ts.snap | 66 ++--- .../ScrollViewComponentTest.test.ts.snap | 245 +++++++++++----- .../TouchableComponentTest.test.ts.snap | 21 +- .../ViewComponentTest.test.ts.snap | 90 +++--- 10 files changed, 670 insertions(+), 479 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-view-box-sizing-2-snap.png b/packages/e2e-test-app-fabric/test/__image_snapshots__/view-component-test-test-ts-view-tests-view-box-sizing-2-snap.png index f40f56c19c33dbacb5455a4e54cceaea10d7fe35..37d496e964cc1888efdbf7488d461881afb50448 100644 GIT binary patch literal 2768 zcmeHJYgAKL7QPnh6I31&60D4}L~sPeRD%zoQ68d#i5MO&0W~U0j2Z(32>}bMMKF-% z7|NsJv7{&snv|gkgh%K!B$lGLDlrO%C>Ww3iGc}8Ai$j9>Yrg|e))6GT6dp)&pmga z?|kRm_wGDgM5yg@)N%-dY{T{k9f2S#4)AX*T@0+QKZ*jnMYJQKdm;YAwQs?Vbs{z# z3qkel73Slgg8O9`_s7y8Xyvb#YmxLq?MVn)ekv>odo(S6tWSY=tGB;`SMS~GvLg0H z^oHkOT+NATeiBzUBBOj?f9@DPIie_O?2$Qa`kzHR+dqMzPHXHkU>z4&PGI)9PN&l+ z?_#oyvx4;W^r?G>xtVE$>$KC5N~K~>>c?p1jLgi;%9eL>K|)qsbeI1~9IM>`P&VhX z59Iv?Y4xrG>qK>YmEu#W(C*F|Z%C9q``u_h#7&SR;55+YCPmZYbzoO!PSfA>w&oo( z9SB|3O`Dc~qwI-)E#P|c=lJ?X5R@E2s^y|*pAS2SQO}vtJxAGH_XC#{c#IV08Z*ac zcsAWe4D6ybJuVxRccRWwE2mZ*FM{7D(=KmOda0(9vxH?dPySu)5(s*)h*0>PsrF7D zEJJDG{_3_f+o0G(B13<$wlCoCRrG`MuNkDAtNimwY3!%aPK z1wrLMZr1dnIO`v32QaGLaep&_T^qe`qhi%T=Hz;NyGX08fw@;}bqEP6O6Sit zaMU@}-{9MJ!rAZprVE5stO>Mm@{T7wcP=YLKcHZU{YM1?U60=;rB?ytV%VO?0F4R`;mwJ||FEz2+oIs%6W_+04#+2SKe#3R+bKJ%C2N%T%;E8QhmwFSPja)ts z&+wY*ZKM}y*u`q>ahYU-#19bV59tGt?6VmWC~c{_OXM3Z`0cv(sC>=7LqMU*F*AG~ zkah9vDnR)lF;<2 zD$B{`fkmiy4&dU^W&Xg6DXMBa{W$}8KW)ZdesE?Ic*h9Y<7JQD0mXLmH0wD1;`Jd- z_s*{ZPof%IuL?^>Zj!(E6Tf--bZmY~Dguv)BqmcFa-9`@!ug;w&4!T^ylZ^Q;OgqH zzG?z6pt?R{5b;pTqsH*HCz8pdAJ%ETR(tJ7Z{fYXeXR2o#XPbkx?PH|ELgsSdiBxW zyZr$96~dVJP48R6H+}9aeT9uF^I&<3;NGsI2;%?2eKh8hsz>ivKc6RjSvNVPVgA(Y zIdazN`J>6MKEK%qzqE*0x29Y5?t>J2op-sg?FIgiG*yQ|4KNj;Z+ zeUR&iQIEFP_(+3XHi zZ0VylB%9I_fpo(TvRNFL;p*0~I8H*4^ro>(;E5q_$u*Mt^~}i5?e?u{F7oS^Fa0Iv z&ZKivf2pyM-mMmkOf9=HvWdNXA8AcH2x?sBijB?L7@!uUAnDbpP?F85Dp{uu6}D+J z?vW4z)GyF=Y0|XZ&HUdD>y)0XSI#h-5x2Y@Ki~#y`33YkxNxG?5N=vo{MwS|vrZJ; zw=5CrEWtW5J~A+uCNdUak!qG3-`~))xdO9z$eBaDqC!8eC~=V`(qSY!_+lX(PR%&n zBiSkM*aV1;?pwpL^RI4p-*?8QmWD-2lW@aoyzQ^Fq~017(HHZOtu0bZ8zzH@JcDMy zO%p43f>|^`)>`d`1_u}^04gAQJaaxzCcIe!y8O56Ud2GcuvaVPJwO`C;X^28PDC=GZq#~jai9|8Q zA#j_J{&oOqS@jt)hR98^YYI2te=|_SNCdmnkm$!#s854mEDY#E&3ba?!;Ak5?k|>% uV=g@ag56qZJ+}Pk@r5%NjJUUT%p!cPH#$yDxw(TVP*`w85P$En?0*4s5V$-5 literal 2670 zcmeHJYfw{X8a}L+QV|z~HVrp-VI4t~f|vmY2*pJya*>+?VgQYj)mStoNFWd#sV)tH ztV#<)E{s)fLQ)oKLIgqwl4yja1w!N|C0WQqFxdn!;S#f7c4lYV*+1Q`KljX>=Ul!y z&-=dL`+U!LzDS6VvfJUj1A-vC=uaY(AZS}PxW8*-4Ny0A>QivB%1eqm0-ixovNQv$y5c#U#H9KIl*Re7Pj2z125nvNS+HnyOf}#I)VM7huePBFR=P5{WpY zi>tx9H_Z$NL-Xw$l}a_+$ElUe<3$5y^${FodGiGIlVN(lgyL;vB#`2 zweA!f1EntV;)^GA*lfFRxhG6FzY(kOJoI`x>)?D5FB~b_|Y1EL{47MB`mW^$#Rt6@++HDo5g6rn}PD_qeGj z^QmcmtmHQkG(C`#Q04C^d`gxhB*M+A4kxY1hWz96{w+X)(-x7+H!nPpEPFH8Wu2`( zQH)tsM%kgk^_ik%*4&+*cOj_1mv-qq-BC9xI35{m*I(@BabKAAAvR60$<3iro)S#- z%}U{)Y|#m%t|mXnystNwHNi?u2UXYYjpRR;I;n~a=%9IWf;rgNL!I14KguCYuB7` zj=B-koe%Vc%7*e9=i}%tywwZS{sKvXHjs=~)+tMF%@w?^Rxy&5a~$AT zZwq7JonK_$y$G1IQ*Y?SRuJ^!%Z8rMK!|I5z1Xgx6x4fSA22t+bXYcArhm0MTbp}j z29PXY;+6Aiml`WNJ>xl?TnjA%6^V+?9~DeEG)-j$f;Ug=m(I?i8@s{O)%;H)__u@}mV^GLd3b?lyfxIj zSMy1xq7ZsKfA|Ce9K^!HbwUBXsi}zrteRR7u3leX&zhC0)oRfX9D{uA#j?9d+e;>n zq5U}<8jw-$T!5G7_er0hF2071vJgl;d74iD8F&^T3u*n*&*a} zrN6i2i@X&ADuXQ5cNdm~vYpLNUGCIPd3MF-AckHX-xTr{bAB?3)l{+M@1>mNA%XPy z!&!f(t7rIc3Phio)nG|s6h;sC>VDD8M05CDs8#_LNLXph9ZU2 zUWS@vbciNcSkUIpbTluhHPb0l+ipHBW)^Ih{mg&Bs}X66{5Pj-aDlD3!@>mX({@Al zhN1onIr_|eo+PZ3j1W4va((4tEN{x(hf*r>%#Pl~m!48BtFlXm5&hJ!XYbpI|DsFv zY$s=nu}kW!8W1N~;qQscKU1gkhVoC=u(^h8k z!Je`8prwKGcenrnKZ5?4ZYwsnSzqXLm-&S@9YeML@V-c>RPyv&gz?z1heg_4H=LBm zA>`2RVqqJAF!B&SH z@I&sQQn6&E1HMobyjG{{gSV_B8Cmj7e!VeN4r5l%rk`ZqlN#e!EqOhbHc+ykEuQm5 zK2oJ1>RO9Pw@%sha9NDI4)3e**k(l`j)dCe-lFhzZ_b8>SJ8