Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fabric] Add initial implementation for Switch #11204

Merged
merged 7 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "[Fabric] Add initial Switch implementation",
"packageName": "react-native-windows",
"email": "email not defined",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <Fabric/Composition/ImageComponentView.h>
#include <Fabric/Composition/ParagraphComponentView.h>
#include <Fabric/Composition/ScrollViewComponentView.h>
#include <Fabric/Composition/SwitchComponentView.h>
#include <Fabric/Composition/TextInput/WindowsTextInputComponentView.h>

namespace Microsoft::ReactNative {
Expand All @@ -48,6 +49,8 @@ ComponentViewDescriptor const &ComponentViewRegistry::dequeueComponentViewWithCo
view = std::make_shared<ImageComponentView>(compContext, tag, m_context);
} else if (componentHandle == facebook::react::WindowsTextInputShadowNode::Handle()) {
view = std::make_shared<WindowsTextInputComponentView>(compContext, tag, m_context);
} else if (componentHandle == facebook::react::SwitchShadowNode::Handle()) {
view = std::make_shared<SwitchComponentView>(compContext, tag, m_context);
} else {
view = std::make_shared<CompositionViewComponentView>(compContext, tag);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#pragma once

#include "SwitchComponentView.h"

namespace Microsoft::ReactNative {

SwitchComponentView::SwitchComponentView(
const winrt::Microsoft::ReactNative::Composition::ICompositionContext &compContext,
facebook::react::Tag tag,
winrt::Microsoft::ReactNative::ReactContext const &reactContext)
: Super(compContext, tag), m_context(reactContext) {
m_props = std::make_shared<facebook::react::SwitchProps const>();
}

std::vector<facebook::react::ComponentDescriptorProvider>
SwitchComponentView::supplementalComponentDescriptorProviders() noexcept {
return {};
}

void SwitchComponentView::mountChildComponentView(const IComponentView &childComponentView, uint32_t index) noexcept {
assert(false);
}

void SwitchComponentView::unmountChildComponentView(const IComponentView &childComponentView, uint32_t index) noexcept {
assert(false);
}

void SwitchComponentView::handleCommand(std::string const &commandName, folly::dynamic const &arg) noexcept {
if (commandName == "setValue") {
// TODO - Current implementation always aligns with JS value
// This will be needed when we move to using WinUI controls
} else {
Super::handleCommand(commandName, arg);
}
}

void SwitchComponentView::updateProps(
facebook::react::Props::Shared const &props,
facebook::react::Props::Shared const &oldProps) noexcept {
const auto &oldViewProps = *std::static_pointer_cast<const facebook::react::SwitchProps>(m_props);
const auto &newViewProps = *std::static_pointer_cast<const facebook::react::SwitchProps>(props);

ensureVisual();

if (oldViewProps.backgroundColor != newViewProps.backgroundColor ||
oldViewProps.thumbTintColor != newViewProps.thumbTintColor || oldViewProps.value != newViewProps.value ||
oldViewProps.disabled != newViewProps.disabled) {
m_drawingSurface = nullptr;
}

m_props = std::static_pointer_cast<facebook::react::ViewProps const>(props);
}

void SwitchComponentView::updateState(
facebook::react::State::Shared const &state,
facebook::react::State::Shared const &oldState) noexcept { }

void SwitchComponentView::updateLayoutMetrics(
facebook::react::LayoutMetrics const &layoutMetrics,
facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept {
// Set Position & Size Properties
ensureVisual();

if ((layoutMetrics.displayType != m_layoutMetrics.displayType)) {
m_visual.IsVisible(layoutMetrics.displayType != facebook::react::DisplayType::None);
}

m_layoutMetrics = layoutMetrics;

UpdateCenterPropertySet();
m_visual.Size(
{layoutMetrics.frame.size.width * layoutMetrics.pointScaleFactor,
layoutMetrics.frame.size.height * layoutMetrics.pointScaleFactor});
m_visual.Offset({
layoutMetrics.frame.origin.x * layoutMetrics.pointScaleFactor,
layoutMetrics.frame.origin.y * layoutMetrics.pointScaleFactor,
0.0f,
});
}

void SwitchComponentView::finalizeUpdates(RNComponentViewUpdateMask updateMask) noexcept {
ensureDrawingSurface();
}

void SwitchComponentView::Draw() noexcept {
// Begin our update of the surface pixels. If this is our first update, we are required
// to specify the entire surface, which nullptr is shorthand for (but, as it works out,
// any time we make an update we touch the entire surface, so we always pass nullptr).
winrt::com_ptr<ID2D1DeviceContext> d2dDeviceContext;
POINT offset;

winrt::com_ptr<Composition::ICompositionDrawingSurfaceInterop> drawingSurfaceInterop;
m_drawingSurface.as(drawingSurfaceInterop);

if (CheckForDeviceRemoved(drawingSurfaceInterop->BeginDraw(d2dDeviceContext.put(), &offset))) {
const auto switchProps = std::static_pointer_cast<const facebook::react::SwitchProps>(m_props);

d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::Black, 0.0f));
if (m_props->backgroundColor) {
d2dDeviceContext->Clear(m_props->backgroundColor.AsD2DColor());
}

float offsetX = static_cast<float>(offset.x / m_layoutMetrics.pointScaleFactor);
float offsetY = static_cast<float>(offset.y / m_layoutMetrics.pointScaleFactor);

// https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/CommonStyles/ToggleSwitch_themeresources.xaml
constexpr float thumbMargin = 3.0f;
constexpr float thumbRadius = 7.0f;
constexpr float trackWidth = 40.0f;
constexpr float trackHeight = 20.0f;
constexpr float trackCornerRadius = 10.0f;

auto frame{m_layoutMetrics.frame.size};
float trackMarginX = (frame.width - trackWidth) / 2;
float trackMarginY = (frame.height - trackHeight) / 2;

D2D1_RECT_F trackRect = D2D1::RectF(
offsetX + trackMarginX,
offsetY + trackMarginY,
offsetX + trackMarginX + trackWidth,
offsetY + trackMarginY + trackHeight);

// switchProps->value = false
float thumbX = trackRect.left + thumbMargin + thumbRadius;

if (switchProps->value) {
thumbX = trackRect.right - thumbMargin - thumbRadius;
}

winrt::com_ptr<ID2D1SolidColorBrush> defaultBrush;

D2D1_COLOR_F defaultColor =
switchProps->disabled ? facebook::react::greyColor().AsD2DColor() : facebook::react::blackColor().AsD2DColor();

winrt::check_hresult(d2dDeviceContext->CreateSolidColorBrush(defaultColor, defaultBrush.put()));

winrt::com_ptr<ID2D1SolidColorBrush> thumbBrush;
if (!switchProps->disabled && switchProps->thumbTintColor) {
winrt::check_hresult(
d2dDeviceContext->CreateSolidColorBrush(switchProps->thumbTintColor.AsD2DColor(), thumbBrush.put()));
} else {
thumbBrush = defaultBrush;
}

const auto dpi = m_layoutMetrics.pointScaleFactor * 96.0f;
float oldDpiX, oldDpiY;
d2dDeviceContext->GetDpi(&oldDpiX, &oldDpiY);
d2dDeviceContext->SetDpi(dpi, dpi);

// switch thumb
D2D1_POINT_2F thumbCenter = D2D1 ::Point2F(thumbX, (trackRect.top + trackRect.bottom) / 2);
D2D1_ELLIPSE thumb = D2D1::Ellipse(thumbCenter, thumbRadius, thumbRadius);
d2dDeviceContext->FillEllipse(thumb, thumbBrush.get());
marlenecota marked this conversation as resolved.
Show resolved Hide resolved

// switch track
D2D1_ROUNDED_RECT track = D2D1::RoundedRect(trackRect, trackCornerRadius, trackCornerRadius);
d2dDeviceContext->DrawRoundedRectangle(track, defaultBrush.get());

// Restore old dpi setting
d2dDeviceContext->SetDpi(oldDpiX, oldDpiY);

// Our update is done. EndDraw never indicates rendering device removed, so any
// failure here is unexpected and, therefore, fatal.
winrt::check_hresult(drawingSurfaceInterop->EndDraw());
}
}

void SwitchComponentView::prepareForRecycle() noexcept {}

facebook::react::Props::Shared SwitchComponentView::props() noexcept {
return m_props;
}

void SwitchComponentView::ensureVisual() noexcept {
if (!m_visual) {
m_visual = m_compContext.CreateSpriteVisual();
}
}

void SwitchComponentView::ensureDrawingSurface() noexcept {
if (!m_drawingSurface) {
winrt::Windows::Foundation::Size surfaceSize = {
m_layoutMetrics.frame.size.width * m_layoutMetrics.pointScaleFactor,
m_layoutMetrics.frame.size.height * m_layoutMetrics.pointScaleFactor};
m_drawingSurface = m_compContext.CreateDrawingSurface(
surfaceSize,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied);

Draw();

auto surfaceBrush = m_compContext.CreateSurfaceBrush(m_drawingSurface);

m_visual.Brush(surfaceBrush);
}
}

facebook::react::Tag SwitchComponentView::hitTest(facebook::react::Point pt, facebook::react::Point &localPt)
const noexcept {
facebook::react::Point ptLocal{pt.x - m_layoutMetrics.frame.origin.x, pt.y - m_layoutMetrics.frame.origin.y};

if ((m_props->pointerEvents == facebook::react::PointerEventsMode::Auto ||
m_props->pointerEvents == facebook::react::PointerEventsMode::BoxOnly) &&
ptLocal.x >= 0 && ptLocal.x <= m_layoutMetrics.frame.size.width && ptLocal.y >= 0 &&
ptLocal.y <= m_layoutMetrics.frame.size.height) {
localPt = ptLocal;
return tag();
}

return -1;
}

facebook::react::SharedTouchEventEmitter SwitchComponentView::touchEventEmitter() noexcept {
return m_eventEmitter;
}

winrt::Microsoft::ReactNative::Composition::IVisual SwitchComponentView::Visual() const noexcept {
return m_visual;
}

int64_t SwitchComponentView::SendMessage(uint32_t msg, uint64_t wParam, int64_t lParam) noexcept {
switch (msg) {
case WM_LBUTTONDOWN:
case WM_POINTERDOWN: {
const auto switchProps = std::static_pointer_cast<const facebook::react::SwitchProps>(m_props);

if (!switchProps->disabled && m_eventEmitter) {
auto switchEventEmitter = std::static_pointer_cast<facebook::react::SwitchEventEmitter const>(m_eventEmitter);

facebook::react::SwitchEventEmitter::OnChange args;
args.value = !(switchProps->value);
args.target = tag();

switchEventEmitter->onChange(args);
}
break;
}
}

return 0;
}

} // namespace Microsoft::ReactNative
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#pragma once

#include <Fabric/ComponentView.h>
#include <Microsoft.ReactNative.Cxx/ReactContext.h>

#include "CompositionViewComponentView.h"

#include <react/components/rnwcore/ShadowNodes.h>

namespace Microsoft::ReactNative {

struct SwitchComponentView;

struct SwitchComponentView : CompositionBaseComponentView {
using Super = CompositionBaseComponentView;
SwitchComponentView(
const winrt::Microsoft::ReactNative::Composition::ICompositionContext &compContext,
facebook::react::Tag tag,
winrt::Microsoft::ReactNative::ReactContext const &reactContext);

std::vector<facebook::react::ComponentDescriptorProvider> supplementalComponentDescriptorProviders() noexcept
override;
void mountChildComponentView(const IComponentView &childComponentView, uint32_t index) noexcept override;
void unmountChildComponentView(const IComponentView &childComponentView, uint32_t index) noexcept override;
void handleCommand(std::string const &commandName, folly::dynamic const &arg) noexcept override;
void updateProps(facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept
override;
void updateState(facebook::react::State::Shared const &state, facebook::react::State::Shared const &oldState) noexcept
override;
void updateLayoutMetrics(
facebook::react::LayoutMetrics const &layoutMetrics,
facebook::react::LayoutMetrics const &oldLayoutMetrics) noexcept override;
void finalizeUpdates(RNComponentViewUpdateMask updateMask) noexcept override;
void prepareForRecycle() noexcept override;
facebook::react::Props::Shared props() noexcept override;
facebook::react::SharedTouchEventEmitter touchEventEmitter() noexcept override;

facebook::react::Tag hitTest(facebook::react::Point pt, facebook::react::Point &localPt) const noexcept override;
winrt::Microsoft::ReactNative::Composition::IVisual Visual() const noexcept override;
int64_t SendMessage(uint32_t msg, uint64_t wParam, int64_t lParam) noexcept override;

private:
void ensureVisual() noexcept;
void Draw() noexcept;
void ensureDrawingSurface() noexcept;

facebook::react::Size m_contentSize;
winrt::Microsoft::ReactNative::Composition::SpriteVisual m_visual{nullptr};
winrt::Microsoft::ReactNative::ReactContext m_context;
facebook::react::SharedViewProps m_props;
winrt::Microsoft::ReactNative::Composition::ICompositionDrawingSurface m_drawingSurface;
};

} // namespace Microsoft::ReactNative
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ SharedColor whiteColor() {
return color;
}

SharedColor greyColor() {
static SharedColor color = colorFromComponents(ColorComponents{133, 133, 133, 1});
return color;
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ ColorComponents colorComponentsFromColor(SharedColor const &color);
SharedColor clearColor();
SharedColor blackColor();
SharedColor whiteColor();
SharedColor greyColor();

} // namespace react
} // namespace facebook
Expand Down
1 change: 1 addition & 0 deletions vnext/Shared/Shared.vcxitems
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\CompositionEventHandler.cpp" />
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\CompositionHelpers.cpp" />
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\ImageComponentView.cpp" />
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\SwitchComponentView.cpp" />
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\ParagraphComponentView.cpp" />
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\ScrollViewComponentView.cpp" />
<ClCompile Include="$(ReactNativeWindowsDir)Microsoft.ReactNative\Fabric\Composition\CompositionViewComponentView.cpp" />
Expand Down