Skip to content

Commit 083ec56

Browse files
committed
Implement outline properties on view
1 parent ea0dd89 commit 083ec56

4 files changed

Lines changed: 114 additions & 14 deletions

File tree

vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.cpp

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,20 @@ void pixelRoundBorderRadii(facebook::react::BorderRadii &borderRadii, float scal
4040
};
4141
}
4242

43+
float pixelRoundAndScaleBorderWidth(float width, float scaleFactor) noexcept {
44+
if (width == 0)
45+
return width = 0;
46+
return std::max(1.f, std::round(width * scaleFactor));
47+
}
48+
4349
void scaleAndPixelRoundBorderWidths(
4450
facebook::react::LayoutMetrics const &layoutMetrics,
4551
facebook::react::BorderMetrics &borderMetrics,
4652
float scaleFactor) noexcept {
47-
borderMetrics.borderWidths.left = (borderMetrics.borderWidths.left == 0)
48-
? 0.f
49-
: std::max(1.f, std::round(borderMetrics.borderWidths.left * scaleFactor));
50-
borderMetrics.borderWidths.top = (borderMetrics.borderWidths.top == 0)
51-
? 0.f
52-
: std::max(1.f, std::round(borderMetrics.borderWidths.top * scaleFactor));
53-
borderMetrics.borderWidths.right = (borderMetrics.borderWidths.right == 0)
54-
? 0.f
55-
: std::max(1.f, std::round(borderMetrics.borderWidths.right * scaleFactor));
56-
borderMetrics.borderWidths.bottom = (borderMetrics.borderWidths.bottom == 0)
57-
? 0.f
58-
: std::max(1.f, std::round(borderMetrics.borderWidths.bottom * scaleFactor));
53+
borderMetrics.borderWidths.left = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.left, scaleFactor);
54+
borderMetrics.borderWidths.top = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.top, scaleFactor);
55+
borderMetrics.borderWidths.right = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.right, scaleFactor);
56+
borderMetrics.borderWidths.bottom = pixelRoundAndScaleBorderWidth(borderMetrics.borderWidths.bottom, scaleFactor);
5957

6058
// If we rounded both sides of the borderWidths up, we may have made the borderWidths larger than the total
6159
if (layoutMetrics.frame.size.width * scaleFactor <
@@ -740,9 +738,9 @@ bool BorderPrimitive::requiresBorder(
740738
auto borderStyle = borderMetrics.borderStyles.left;
741739

742740
bool hasMeaningfulColor =
743-
!borderMetrics.borderColors.isUniform() || !isColorMeaningful(borderMetrics.borderColors.left, theme);
741+
!borderMetrics.borderColors.isUniform() || isColorMeaningful(borderMetrics.borderColors.left, theme);
744742
bool hasMeaningfulWidth = !borderMetrics.borderWidths.isUniform() || (borderMetrics.borderWidths.left != 0);
745-
if (!hasMeaningfulColor && !hasMeaningfulWidth) {
743+
if (!hasMeaningfulColor || !hasMeaningfulWidth) {
746744
return false;
747745
}
748746
return true;

vnext/Microsoft.ReactNative/Fabric/Composition/BorderPrimitive.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace winrt::Microsoft::ReactNative::Composition::implementation {
1414

1515
struct ComponentView;
1616

17+
float pixelRoundAndScaleBorderWidth(float width, float scaleFactor) noexcept;
18+
1719
// Controls adding/removing appropriate visuals to a parent to render a specific border without requiring
1820
struct BorderPrimitive {
1921
static constexpr size_t SpecialBorderLayerCount = 8;

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ constexpr float FOCUS_VISUAL_RADIUS = 3.0f;
4141
// ----- m_visual <-- Background / clip - Can be a custom visual depending on Component type
4242
// |
4343
// ----- Border Visuals x N (BorderPrimitive attached to m_visual)
44+
// ----- Outline Visuals x N(BorderPrimitive)
4445
// ----- <children> (default: directly in m_visual after border visuals)
4546
// ----- m_childrenContainer (created on demand when overflow:hidden, children moved here)
4647
// ------Focus Visual Container (created when hosting focus visuals)
@@ -91,6 +92,9 @@ void ComponentView::onThemeChanged() noexcept {
9192
m_borderPrimitive->onThemeChanged(
9293
m_layoutMetrics, BorderPrimitive::resolveAndAlignBorderMetrics(m_layoutMetrics, *viewProps()));
9394
}
95+
if (m_outlinePrimitive) {
96+
m_outlinePrimitive->onThemeChanged(outlineLayoutMetrics(), outlineBorderMetrics());
97+
}
9498
if (m_componentHostingFocusVisual) {
9599
if (m_componentHostingFocusVisual->m_focusPrimitive->m_focusInnerPrimitive) {
96100
auto innerFocusMetrics = focusLayoutMetrics(true /*inner*/);
@@ -166,6 +170,16 @@ void ComponentView::updateProps(
166170
m_borderPrimitive->updateProps(oldViewProps, newViewProps);
167171
}
168172

173+
if (m_outlinePrimitive) {
174+
if (oldViewProps.outlineOffset != newViewProps.outlineOffset ||
175+
oldViewProps.outlineWidth != newViewProps.outlineWidth ||
176+
oldViewProps.borderRadii != newViewProps.borderRadii ||
177+
oldViewProps.outlineColor != newViewProps.outlineColor ||
178+
oldViewProps.outlineStyle != newViewProps.outlineStyle) {
179+
m_outlinePrimitive->markNeedsUpdate();
180+
}
181+
}
182+
169183
if (m_componentHostingFocusVisual) {
170184
if (!newViewProps.enableFocusRing) {
171185
m_componentHostingFocusVisual->hostFocusVisual(false, get_strong());
@@ -218,6 +232,9 @@ void ComponentView::updateLayoutMetrics(
218232
if (m_borderPrimitive) {
219233
m_borderPrimitive->markNeedsUpdate();
220234
}
235+
if (m_outlinePrimitive) {
236+
m_outlinePrimitive->markNeedsUpdate();
237+
}
221238

222239
if (m_componentHostingFocusVisual) {
223240
m_componentHostingFocusVisual->updateFocusLayoutMetrics();
@@ -301,6 +318,21 @@ void ComponentView::FinalizeUpdates(winrt::Microsoft::ReactNative::ComponentView
301318
if (m_borderPrimitive) {
302319
m_borderPrimitive->finalize(m_layoutMetrics, borderMetrics);
303320
}
321+
322+
auto outlineMetrics = outlineBorderMetrics();
323+
if (!m_outlinePrimitive && BorderPrimitive::requiresBorder(outlineMetrics, theme())) {
324+
m_outlinePrimitive = std::make_shared<BorderPrimitive>(*this);
325+
Visual().InsertAt(m_outlinePrimitive->RootVisual(), m_borderPrimitive ? m_borderPrimitive->numberOfVisuals() : 0);
326+
}
327+
328+
if (m_outlinePrimitive) {
329+
auto offset = pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor) +
330+
std::round(viewProps()->outlineOffset * m_layoutMetrics.pointScaleFactor);
331+
m_outlinePrimitive->RootVisual().Offset({-offset, -offset, 0.0f});
332+
m_outlinePrimitive->RootVisual().RelativeSizeWithOffset({offset * 2, offset * 2}, {1.0f, 1.0f});
333+
334+
m_outlinePrimitive->finalize(outlineLayoutMetrics(), outlineMetrics);
335+
}
304336
}
305337

306338
if (m_componentHostingFocusVisual) {
@@ -581,6 +613,67 @@ facebook::react::LayoutMetrics ComponentView::focusLayoutMetrics(bool inner) con
581613
return layoutMetrics;
582614
}
583615

616+
facebook::react::LayoutMetrics ComponentView::outlineLayoutMetrics() const noexcept {
617+
auto &props = *viewProps();
618+
auto offset = (pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor) +
619+
std::round(viewProps()->outlineOffset * m_layoutMetrics.pointScaleFactor)) /
620+
m_layoutMetrics.pointScaleFactor;
621+
facebook::react::LayoutMetrics layoutMetrics = m_layoutMetrics;
622+
layoutMetrics.frame.origin.x -= offset;
623+
layoutMetrics.frame.origin.y -= offset;
624+
layoutMetrics.frame.size.height += offset * 2;
625+
layoutMetrics.frame.size.width += offset * 2;
626+
return layoutMetrics;
627+
}
628+
629+
facebook::react::BorderMetrics ComponentView::outlineBorderMetrics() const noexcept {
630+
auto &props = *viewProps();
631+
632+
facebook::react::BorderMetrics metrics = BorderPrimitive::resolveAndAlignBorderMetrics(m_layoutMetrics, props);
633+
metrics.borderColors.bottom = metrics.borderColors.left = metrics.borderColors.right = metrics.borderColors.top =
634+
props.outlineColor;
635+
636+
auto offset = pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor) +
637+
std::round(viewProps()->outlineOffset * m_layoutMetrics.pointScaleFactor);
638+
639+
if (metrics.borderRadii.bottomLeft.horizontal)
640+
metrics.borderRadii.bottomLeft.horizontal = std::max(0.0f, metrics.borderRadii.bottomLeft.horizontal + offset);
641+
if (metrics.borderRadii.bottomLeft.vertical)
642+
metrics.borderRadii.bottomLeft.vertical = std::max(0.0f, metrics.borderRadii.bottomLeft.vertical + offset);
643+
if (metrics.borderRadii.bottomRight.horizontal)
644+
metrics.borderRadii.bottomRight.horizontal = std::max(0.0f, metrics.borderRadii.bottomRight.horizontal + offset);
645+
if (metrics.borderRadii.bottomRight.vertical)
646+
metrics.borderRadii.bottomRight.vertical = std::max(0.0f, metrics.borderRadii.bottomRight.vertical + offset);
647+
if (metrics.borderRadii.topLeft.horizontal)
648+
metrics.borderRadii.topLeft.horizontal = std::max(0.0f, metrics.borderRadii.topLeft.horizontal + offset);
649+
if (metrics.borderRadii.topLeft.vertical)
650+
metrics.borderRadii.topLeft.vertical = std::max(0.0f, metrics.borderRadii.topLeft.vertical + offset);
651+
if (metrics.borderRadii.topRight.horizontal)
652+
metrics.borderRadii.topRight.horizontal = std::max(0.0f, metrics.borderRadii.topRight.horizontal + offset);
653+
if (metrics.borderRadii.topRight.vertical)
654+
metrics.borderRadii.topRight.vertical = std::max(0.0f, metrics.borderRadii.topRight.vertical + offset);
655+
656+
static_assert(
657+
facebook::react::BorderStyle::Solid ==
658+
static_cast<facebook::react::BorderStyle>(facebook::react::OutlineStyle::Solid));
659+
static_assert(
660+
facebook::react::BorderStyle::Dotted ==
661+
static_cast<facebook::react::BorderStyle>(facebook::react::OutlineStyle::Dotted));
662+
static_assert(
663+
facebook::react::BorderStyle::Dashed ==
664+
static_cast<facebook::react::BorderStyle>(facebook::react::OutlineStyle::Dashed));
665+
assert(
666+
props.outlineStyle == facebook::react::OutlineStyle::Solid ||
667+
props.outlineStyle == facebook::react::OutlineStyle::Dotted ||
668+
props.outlineStyle == facebook::react::OutlineStyle::Dashed);
669+
metrics.borderStyles.bottom = metrics.borderStyles.left = metrics.borderStyles.right = metrics.borderStyles.top =
670+
static_cast<facebook::react::BorderStyle>(props.outlineStyle);
671+
672+
metrics.borderWidths.bottom = metrics.borderWidths.left = metrics.borderWidths.right = metrics.borderWidths.top =
673+
pixelRoundAndScaleBorderWidth(viewProps()->outlineWidth, m_layoutMetrics.pointScaleFactor);
674+
return metrics;
675+
}
676+
584677
facebook::react::BorderMetrics ComponentView::focusBorderMetrics(
585678
bool inner,
586679
const facebook::react::LayoutMetrics &layoutMetrics) const noexcept {
@@ -934,6 +1027,9 @@ void ComponentView::indexOffsetForBorder(uint32_t &index) const noexcept {
9341027
if (m_borderPrimitive) {
9351028
index += m_borderPrimitive->numberOfVisuals();
9361029
}
1030+
if (m_outlinePrimitive) {
1031+
index += 1;
1032+
}
9371033
}
9381034

9391035
void ComponentView::OnRenderingDeviceLost() noexcept {}

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ struct ComponentView : public ComponentViewT<
157157
facebook::react::BorderMetrics focusBorderMetrics(bool inner, const facebook::react::LayoutMetrics &layoutMetrics)
158158
const noexcept;
159159

160+
facebook::react::LayoutMetrics outlineLayoutMetrics() const noexcept;
161+
facebook::react::BorderMetrics outlineBorderMetrics() const noexcept;
162+
160163
virtual winrt::Microsoft::ReactNative::Composition::Experimental::IVisual visualToHostFocus() noexcept;
161164
virtual winrt::com_ptr<ComponentView> focusVisualRoot(const facebook::react::Rect &focusRect) noexcept;
162165

@@ -168,6 +171,7 @@ struct ComponentView : public ComponentViewT<
168171
winrt::com_ptr<ComponentView>
169172
m_componentHostingFocusVisual; // The component that we are showing our focus visuals within
170173
std::shared_ptr<BorderPrimitive> m_borderPrimitive;
174+
std::shared_ptr<BorderPrimitive> m_outlinePrimitive;
171175
std::unique_ptr<FocusPrimitive> m_focusPrimitive{nullptr};
172176
winrt::Microsoft::ReactNative::Composition::Experimental::IVisual m_outerVisual{nullptr};
173177
winrt::event<winrt::Windows::Foundation::EventHandler<winrt::IInspectable>> m_themeChangedEvent;

0 commit comments

Comments
 (0)