Files
react-native-svg/windows/RNSVG/Fabric/RenderableView.h
Andrew Coates d02c997352 fix: react-native-windows implementation for new architecture (#2527)
# Summary
There are two main things going on with the PR. Both involve a reworking
of the new arch implementation of rn-svg for windows.
The current implementation attempts to implement a svg renderer from
scratch. There are numerous edge cases that it wasn't handling
correctly, and the performance had some serious issues. This
implementation switches to use the svg rendering path built into
Direct2D. This brings significant performance improvements.

The 2nd issue is there have been various breaking changes in
react-native-windows for how new arch native components are implemented.
This brings the rn-svg implementation in line with those latest changes.

## Test Plan

Primary testing right now is loading up the example app in the repo.
New arch on react-native-windows is still in somewhat early days - so
there are not really current users of this code. I am integrating this
code into Microsoft Office, where I have tested some scenarios. But we
will get expanded testing as we roll out the new arch. I expect there to
be some follow-ups as we expand our usage. The version of rn-svg before
this PR doesn't build with the latest new arch react-native-windows
versions. - So its hard to get worse than that.

### What's required for testing (prerequisites)?

### What are the steps to reproduce (after prerequisites)?

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    N/A  |
| MacOS   |    N/A      |
| Android |    N/A      |
| Web     |          |

## Checklist

- [x] I have tested this on a device and a simulator
- [ ] I added documentation in `README.md`
- [ ] I updated the typed files (typescript)
- [ ] I added a test for the API in the `__tests__` folder
2024-11-15 12:17:12 +01:00

303 lines
14 KiB
C++

#pragma once
#include "SvgView.h"
#include <JSValueComposition.h>
#include <NativeModules.h>
#include "D2DHelpers.h"
#include "SvgStrings.h"
namespace winrt::Microsoft::ReactNative {
void WriteValue(IJSValueWriter const &writer, const D2D1_SVG_LENGTH &value) noexcept;
void ReadValue(IJSValueReader const &reader, /*out*/ D2D1_SVG_LENGTH &value) noexcept;
} // namespace winrt::Microsoft::ReactNative
namespace winrt::RNSVG::implementation {
REACT_STRUCT(ColorStruct)
struct ColorStruct {
REACT_FIELD(type)
int32_t type{-1};
REACT_FIELD(payload)
winrt::Microsoft::ReactNative::Color payload{nullptr};
REACT_FIELD(brushRef)
std::wstring brushRef;
bool operator==(const ColorStruct &rhs) const {
if (type != rhs.type || brushRef != rhs.brushRef)
return false;
// When we move to a RNW version that provides Color::Equals switch to that for the payload comparison
auto writer = winrt::Microsoft::ReactNative::MakeJSValueTreeWriter();
winrt::Microsoft::ReactNative::WriteValue(writer, payload);
auto rhsWriter = winrt::Microsoft::ReactNative::MakeJSValueTreeWriter();
winrt::Microsoft::ReactNative::WriteValue(rhsWriter, rhs.payload);
return winrt::Microsoft::ReactNative::TakeJSValue(writer).Equals(
winrt::Microsoft::ReactNative::TakeJSValue(rhsWriter));
}
bool operator!=(const ColorStruct &rhs) const {
return !(*this == rhs);
}
};
HRESULT SetColorMode(
const SvgView &svgView,
ID2D1SvgElement &element,
const std::wstring &attribute,
const ColorStruct &colorProp) noexcept;
// Currently no good way to do inheritance in REACT_STRUCTS
#define REACT_SVG_RENDERABLE_COMMON_PROPS \
REACT_FIELD(name) \
std::optional<std::wstring> name; \
REACT_FIELD(opacity) \
std::optional<float> opacity; \
REACT_FIELD(matrix) \
std::optional<std::vector<float>> matrix; \
REACT_FIELD(clipPath) \
std::optional<std::wstring> clipPath; \
REACT_FIELD(clipRule) \
std::optional<D2D1_FILL_MODE> clipRule; \
REACT_FIELD(fill) \
std::optional<ColorStruct> fill; \
REACT_FIELD(fillOpacity) \
std::optional<float> fillOpacity; \
REACT_FIELD(fillRule) \
std::optional<D2D1_FILL_MODE> fillRule; \
REACT_FIELD(stroke) \
std::optional<ColorStruct> stroke; \
REACT_FIELD(strokeOpacity) \
std::optional<float> strokeOpacity; \
REACT_FIELD(strokeWidth) \
std::optional<D2D1_SVG_LENGTH> strokeWidth; \
REACT_FIELD(strokeLinecap) \
std::optional<uint32_t> strokeLinecap; \
REACT_FIELD(strokeLinejoin) \
std::optional<D2D1_SVG_LINE_JOIN> strokeLinejoin; \
REACT_FIELD(strokeDasharray) \
std::optional<std::vector<D2D1_SVG_LENGTH>> strokeDasharray; \
REACT_FIELD(strokeDashoffset) \
std::optional<float> strokeDashoffset; \
REACT_FIELD(strokeMiterlimit) \
std::optional<float> strokeMiterlimit; \
REACT_FIELD(propList) \
std::optional<std::vector<std::string>> propList; \
std::optional<winrt::Microsoft::ReactNative::Color> color; \
winrt::Microsoft::ReactNative::ViewProps m_props{nullptr};
#define REACT_SVG_RENDERABLE_COMMON_PROPS_INIT \
: m_props(props)
#define REACT_BEGIN_SVG_RENDERABLE_COMMON_PROPS_CLONE(TProps) \
if (cloneFrom) { \
auto cloneFromProps = cloneFrom.as<TProps>(); \
name = cloneFromProps->name; \
opacity = cloneFromProps->opacity; \
matrix = cloneFromProps->matrix; \
clipPath = cloneFromProps->clipPath; \
clipRule = cloneFromProps->clipRule; \
fill = cloneFromProps->fill; \
fillOpacity = cloneFromProps->fillOpacity; \
fillRule = cloneFromProps->fillRule; \
stroke = cloneFromProps->stroke; \
strokeOpacity = cloneFromProps->strokeOpacity; \
strokeWidth = cloneFromProps->strokeWidth; \
strokeLinecap = cloneFromProps->strokeLinecap; \
strokeLinejoin = cloneFromProps->strokeLinejoin; \
strokeDasharray = cloneFromProps->strokeDasharray; \
strokeMiterlimit = cloneFromProps->strokeMiterlimit; \
propList = cloneFromProps->propList; \
color = cloneFromProps->color;
#define REACT_END_SVG_RENDERABLE_COMMON_PROPS_CLONE \
}
struct __declspec(uuid("a03986c0-b06e-4fb8-a86e-16fcc47b2f31")) RenderableView : public ::IUnknown {
public:
RenderableView() = default;
virtual const wchar_t *GetSvgElementName() noexcept = 0;
// ComponentView
void MountChildComponentView(
const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &args) noexcept;
void UnmountChildComponentView(
const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept;
virtual void UpdateProps(
const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
const winrt::Microsoft::ReactNative::IComponentProps &props,
const winrt::Microsoft::ReactNative::IComponentProps &oldProps) noexcept;
virtual void FinalizeUpates(
const winrt::Microsoft::ReactNative::ComponentView & /*view*/,
winrt::Microsoft::ReactNative::ComponentViewUpdateMask mask) noexcept;
ID2D1SvgElement &Render(const SvgView &svgView, ID2D1SvgDocument &document, ID2D1SvgElement &svgElement) noexcept;
virtual void OnRender(const SvgView &svgView, ID2D1SvgDocument &document, ID2D1SvgElement & /*svgElement*/) noexcept;
virtual bool IsSupported() const noexcept;
void Invalidate(const winrt::Microsoft::ReactNative::ComponentView &view);
protected:
winrt::Microsoft::ReactNative::IComponentProps m_props;
private:
winrt::com_ptr<ID2D1SvgElement> m_spD2DSvgElement;
};
template <typename TProps>
void SetCommonSvgProps(
const SvgView &svgView,
ID2D1SvgDocument &document,
ID2D1SvgElement &element,
const TProps &commonProps) noexcept {
HRESULT hr = S_OK;
if (commonProps.color != std::nullopt) {
auto color = commonProps.color.value().AsWindowsColor(svgView.Theme());
hr |= element.SetAttributeValue(SvgStrings::colorAttributeName, D2DHelpers::AsD2DColor(color));
}
if (commonProps.propList) {
for (auto &prop : commonProps.propList.value()) {
if (prop == "fill") {
if (commonProps.fill != std::nullopt)
hr |= SetColorMode(svgView, element, SvgStrings::fillAttributeName, commonProps.fill.value());
else
hr |= element.SetAttributeValue(
SvgStrings::fillAttributeName,
D2D1_SVG_ATTRIBUTE_STRING_TYPE::D2D1_SVG_ATTRIBUTE_STRING_TYPE_SVG,
SvgStrings::noneAttributeValue);
} else if (prop == "fillOpacity") {
if (commonProps.fillOpacity != std::nullopt)
hr |= element.SetAttributeValue(SvgStrings::fillOpacityAttributeName, commonProps.fillOpacity.value());
} else if (prop == "fillRule") {
if (commonProps.fillRule != std::nullopt) {
hr |= element.SetAttributeValue(SvgStrings::fillRuleAttributeName, commonProps.fillRule.value());
}
} else if (prop == "stroke") {
if (commonProps.stroke != std::nullopt)
hr |= SetColorMode(svgView, element, SvgStrings::strokeAttributeName, commonProps.stroke.value());
else
hr |= element.SetAttributeValue(
SvgStrings::strokeAttributeName,
D2D1_SVG_ATTRIBUTE_STRING_TYPE::D2D1_SVG_ATTRIBUTE_STRING_TYPE_SVG,
SvgStrings::noneAttributeValue);
} else if (prop == "strokeWidth") {
if (commonProps.strokeWidth != std::nullopt)
hr |= element.SetAttributeValue(SvgStrings::strokeWidthAttributeName, commonProps.strokeWidth.value());
} else if (prop == "strokeOpacity") {
if (commonProps.strokeOpacity != std::nullopt)
hr |= element.SetAttributeValue(SvgStrings::strokeOpacityAttributeName, commonProps.strokeOpacity.value());
} else if (prop == "strokeDasharray") {
if (commonProps.strokeDasharray != std::nullopt && !commonProps.strokeDasharray->empty()) {
winrt::com_ptr<ID2D1SvgStrokeDashArray> dashArray;
document.CreateStrokeDashArray(
&commonProps.strokeDasharray.value()[0],
static_cast<UINT32>(commonProps.strokeDasharray.value().size()),
dashArray.put());
hr |= element.SetAttributeValue(SvgStrings::strokeDashArrayAttributeName, dashArray.get());
}
} else if (prop == "strokeDashoffset") {
if (commonProps.strokeDashoffset != std::nullopt) {
hr |= element.SetAttributeValue(
SvgStrings::strokeDashOffsetAttributeName, commonProps.strokeDashoffset.value());
}
} else if (prop == "strokeLinecap") {
if (commonProps.strokeLinecap != std::nullopt) {
static D2D1_SVG_LINE_CAP supportedCaps[] = {
D2D1_SVG_LINE_CAP_BUTT, D2D1_SVG_LINE_CAP_ROUND, D2D1_SVG_LINE_CAP_SQUARE};
hr |= element.SetAttributeValue(
SvgStrings::strokeLinecapAttributeName, supportedCaps[commonProps.strokeLinecap.value()]);
}
} else if (prop == "strokeLinejoin") {
if (commonProps.strokeLinejoin != std::nullopt) {
static D2D1_SVG_LINE_JOIN supportedJoins[] = {
D2D1_SVG_LINE_JOIN_MITER, D2D1_SVG_LINE_JOIN_ROUND, D2D1_SVG_LINE_JOIN_BEVEL};
hr |= element.SetAttributeValue(
SvgStrings::strokeLinejoinAttributeName, supportedJoins[commonProps.strokeLinejoin.value()]);
}
} else if (prop == "strokeMiterlimit") {
if (commonProps.strokeMiterlimit != std::nullopt) {
hr |= element.SetAttributeValue(
SvgStrings::strokeMiterLimitAttributeName, commonProps.strokeMiterlimit.value());
}
}
}
}
if (commonProps.clipPath != std::nullopt) {
std::wstring namedRefStr = L"url(#" + commonProps.clipPath.value() + L")";
hr |= element.SetAttributeValue(
SvgStrings::clipPathAttributeName,
D2D1_SVG_ATTRIBUTE_STRING_TYPE::D2D1_SVG_ATTRIBUTE_STRING_TYPE_SVG,
namedRefStr.c_str());
}
if (commonProps.clipRule != std::nullopt) {
hr |= element.SetAttributeValue(SvgStrings::clipRuleAttributeName, commonProps.clipRule.value());
}
if (commonProps.name != std::nullopt)
hr |= element.SetAttributeValue(
SvgStrings::idAttributeName,
D2D1_SVG_ATTRIBUTE_STRING_TYPE::D2D1_SVG_ATTRIBUTE_STRING_TYPE_SVG,
commonProps.name.value().c_str());
if (commonProps.opacity != std::nullopt)
hr |= element.SetAttributeValue(SvgStrings::opacityAttributeName, commonProps.opacity.value());
if (commonProps.matrix != std::nullopt) {
auto &matrix = commonProps.matrix.value();
hr |= element.SetAttributeValue(
SvgStrings::transformAttributeName,
D2D1_MATRIX_3X2_F{matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]});
}
assert(hr == S_OK);
}
} // namespace winrt::RNSVG::implementation
template <typename TProps, typename TUserData>
void RegisterRenderableComponent(
const winrt::hstring &name,
const winrt::Microsoft::ReactNative::IReactPackageBuilderFabric &builder) noexcept {
builder.AddViewComponent(name, [](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept {
builder.SetComponentViewInitializer([](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto userData = winrt::make_self<TUserData>();
view.UserData(*userData);
});
builder.SetCreateProps(
[](winrt::Microsoft::ReactNative::ViewProps props, const winrt::Microsoft::ReactNative::IComponentProps &cloneFrom) noexcept { return winrt::make<TProps>(props, cloneFrom); });
builder.SetUpdatePropsHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::IComponentProps &newProps,
const winrt::Microsoft::ReactNative::IComponentProps &oldProps) noexcept {
auto userData = winrt::get_self<TUserData>(view.UserData());
userData->UpdateProps(view, newProps, oldProps);
});
builder.SetFinalizeUpdateHandler([](const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::ComponentViewUpdateMask mask) noexcept {
auto userData = winrt::get_self<TUserData>(view.UserData());
userData->FinalizeUpates(view, mask);
});
builder.SetMountChildComponentViewHandler(
[](const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::MountChildComponentViewArgs &args) noexcept {
auto userData = winrt::get_self<TUserData>(view.UserData());
return userData->MountChildComponentView(view, args);
});
builder.SetUnmountChildComponentViewHandler(
[](const winrt::Microsoft::ReactNative::ComponentView &view,
const winrt::Microsoft::ReactNative::UnmountChildComponentViewArgs &args) noexcept {
auto userData = winrt::get_self<TUserData>(view.UserData());
return userData->UnmountChildComponentView(view, args);
});
});
}