diff --git a/change/react-native-windows-a2827b9b-c897-4b58-89e9-6bbb230ed98c.json b/change/react-native-windows-a2827b9b-c897-4b58-89e9-6bbb230ed98c.json new file mode 100644 index 00000000000..21cbcbf2325 --- /dev/null +++ b/change/react-native-windows-a2827b9b-c897-4b58-89e9-6bbb230ed98c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "add window to Modal", + "packageName": "react-native-windows", + "email": "tatianakapos@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp index a2b63a07b61..57f287ede91 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.cpp @@ -1,4 +1,3 @@ - // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -11,14 +10,14 @@ #include "../CompositionDynamicAutomationProvider.h" #include "Unicode.h" -namespace winrt::Microsoft::ReactNative::Composition::implementation { +#include +#include +#include +#include "IReactContext.h" +#include "ReactHost/ReactInstanceWin.h" +#include "ReactNativeHost.h" -winrt::Microsoft::ReactNative::ComponentView WindowsModalHostComponentView::Create( - const winrt::Microsoft::ReactNative::Composition::ICompositionContext &compContext, - facebook::react::Tag tag, - winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept { - return winrt::make(compContext, tag, reactContext); -} +namespace winrt::Microsoft::ReactNative::Composition::implementation { WindowsModalHostComponentView::WindowsModalHostComponentView( const winrt::Microsoft::ReactNative::Composition::ICompositionContext &compContext, facebook::react::Tag tag, @@ -30,20 +29,166 @@ WindowsModalHostComponentView::WindowsModalHostComponentView( ComponentViewFeatures::Default & ~ComponentViewFeatures::Background, false) { m_props = std::make_shared(); + m_context = reactContext; // save context m_visual = compContext.CreateSpriteVisual(); } +winrt::Microsoft::ReactNative::ComponentView WindowsModalHostComponentView::Create( + const winrt::Microsoft::ReactNative::Composition::ICompositionContext &compContext, + facebook::react::Tag tag, + winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept { + return winrt::make(compContext, tag, reactContext); +} + +// constants for creating a new windows (code mostly taken from LogBox) +constexpr PCWSTR c_modalWindowClassName = L"MS_REACTNATIVE_MODAL"; +constexpr auto CompHostProperty = L"CompHost"; +const int MODAL_DEFAULT_WIDTH = 500; +const int MODAL_DEFAULT_HEIGHT = 500; + +// creates a new modal window +void WindowsModalHostComponentView::EnsureModalCreated() { + auto host = + winrt::Microsoft::ReactNative::implementation::ReactNativeHost::GetReactNativeHost(m_context.Properties()); + // return if hwnd already exists + if (!host || m_hwnd) { + return; + } + + RegisterWndClass(); // creates and register a windows class + auto CompositionHwndHost = winrt::Microsoft::ReactNative::CompositionHwndHost(); + winrt::Microsoft::ReactNative::ReactViewOptions viewOptions; + viewOptions.ComponentName(L"Modal"); + CompositionHwndHost.ReactViewHost(winrt::Microsoft::ReactNative::ReactCoreInjection::MakeViewHost(host, viewOptions)); + HINSTANCE hInstance = GetModuleHandle(NULL); + winrt::impl::abi::type *pHost{nullptr}; + winrt::com_ptr<::IUnknown> spunk; + CompositionHwndHost.as(spunk); + + // get the root hwnd + auto roothwnd = reinterpret_cast( + winrt::Microsoft::ReactNative::ReactCoreInjection::GetTopLevelWindowId(m_context.Properties().Handle())); + + m_hwnd = CreateWindow( + c_modalWindowClassName, + L"React-Native Modal", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + MODAL_DEFAULT_WIDTH, + MODAL_DEFAULT_HEIGHT, + roothwnd, // parent + nullptr, + hInstance, + spunk.get()); + + // Check if window creation succeeded + if (!m_hwnd) { + throw std::exception("Failed to create new hwnd for Modal: " + GetLastError()); + } + + spunk.detach(); +} + +void WindowsModalHostComponentView::ShowOnUIThread() { + if (m_hwnd) { + ShowWindow(m_hwnd, SW_NORMAL); + BringWindowToTop(m_hwnd); + SetFocus(m_hwnd); + } +} + +void WindowsModalHostComponentView::HideOnUIThread() noexcept { + if (m_hwnd) { + ::ShowWindow(m_hwnd, SW_HIDE); + } +} + +// Windows Procedure - callback function used for handling all messages (generated by NTUser or manual calls to +// SendMessage) +LRESULT CALLBACK ModalBoxWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) noexcept { + auto data = reinterpret_cast<::IUnknown *>(GetProp( + hwnd, + CompHostProperty)); // gets data handle from the property list of specified window (ie the window we want to make) + winrt::Microsoft::ReactNative::CompositionHwndHost host{nullptr}; + + if (data) { + winrt::check_hresult(data->QueryInterface( + winrt::guid_of(), + winrt::put_abi(host))); // look into the data for a CompositionHwndHost and store it in host + auto result = static_cast(host.TranslateMessage(message, wparam, lparam)); + if (result) { + return result; + } + } + + switch (message) { + case WM_NCCREATE: { // sent before WM_CREATE, lparam should be identical to members of CreateWindowEx + auto createStruct = reinterpret_cast(lparam); // CreateStruct + data = static_cast<::IUnknown *>(createStruct->lpCreateParams); + SetProp(hwnd, CompHostProperty, data); // adds new properties to window + break; + } + case WM_CREATE: { // recieves after window is created but before visible + // host.Initialize((uint64_t)hwnd); cause Modal to throw a not registered error + break; + } + case WM_CLOSE: { + // Just hide the window instead of destroying it + ::ShowWindow(hwnd, SW_HIDE); + return 0; + } + case WM_DESTROY: { // called when we want to destroy the window + data->Release(); + SetProp(hwnd, CompHostProperty, nullptr); + break; + } + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +// Creates and Register a new window class +void WindowsModalHostComponentView::RegisterWndClass() noexcept { + static bool registered = false; + if (registered) { + return; + } + + HINSTANCE hInstance = + GetModuleHandle(NULL); // returns a handle to the file used to create the calling process (.exe file) + + WNDCLASSEX wcex = {}; // contains window class information + wcex.cbSize = sizeof(wcex); // size of windows class (bytes) + wcex.style = CS_HREDRAW | CS_VREDRAW; // class style (redraw window on size adjustment) + wcex.lpfnWndProc = &ModalBoxWndProc; // pointer to windows procedure + wcex.cbClsExtra = DLGWINDOWEXTRA; // extra bytes to allocate + wcex.cbWndExtra = + sizeof(winrt::impl::abi::type *); // extra bytes to allocate + wcex.hInstance = hInstance; + wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); // handle to class cursor + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // background color + wcex.lpszClassName = c_modalWindowClassName; // specify resource name + ATOM classId = RegisterClassEx(&wcex); // register new windows class + WINRT_VERIFY(classId); // 0 = fail + winrt::check_win32(!classId); + + registered = true; +} + void WindowsModalHostComponentView::MountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - assert(false); + // Disabled due to partial Modal implementation. Tracking re-enablement with task list here: + // https://github.com/microsoft/react-native-windows/issues/11157 assert(false); base_type::MountChildComponentView(childComponentView, index); } void WindowsModalHostComponentView::UnmountChildComponentView( const winrt::Microsoft::ReactNative::ComponentView &childComponentView, uint32_t index) noexcept { - assert(false); + // Disabled due to partial Modal implementation.Tracking re-enablement with task list here : https : // + // github.com/microsoft/react-native-windows/issues/11157 assert(false); base_type::UnmountChildComponentView(childComponentView, index); } @@ -56,6 +201,18 @@ void WindowsModalHostComponentView::HandleCommand( void WindowsModalHostComponentView::updateProps( facebook::react::Props::Shared const &props, facebook::react::Props::Shared const &oldProps) noexcept { + const auto &oldModalProps = *std::static_pointer_cast(m_props); + const auto &newModalProps = *std::static_pointer_cast(props); + + // currently Modal only gets Destroyed by closing the window + if (newModalProps.visible) { + EnsureModalCreated(); + ShowOnUIThread(); + } + + // update BaseComponentView props + updateTransformProps(oldModalProps, newModalProps, m_visual); + Super::updateProps(props, oldProps); m_props = std::static_pointer_cast(props); } @@ -66,9 +223,9 @@ void WindowsModalHostComponentView::updateLayoutMetrics( OuterVisual().IsVisible(layoutMetrics.displayType != facebook::react::DisplayType::None); } - // TODO: RedBox placeholder for Modal (taken from unimplementedNativeViewComponent) + // Temparary placeholder for Modal, draws on main hwnd if (m_layoutMetrics.frame.size != layoutMetrics.frame.size || - m_layoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor) { + m_layoutMetrics.pointScaleFactor != layoutMetrics.pointScaleFactor || m_layoutMetrics.frame.size.width == 0) { // Always make visual a min size, so that even if its laid out at zero size, its clear an unimplemented view was // rendered float width = std::max(m_layoutMetrics.frame.size.width, 200.0f); @@ -96,7 +253,7 @@ void WindowsModalHostComponentView::updateLayoutMetrics( { ::Microsoft::ReactNative::Composition::AutoDrawDrawingSurface autoDraw(drawingSurface, &offset); if (auto d2dDeviceContext = autoDraw.GetRenderTarget()) { - d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::Red, 0.3f)); + d2dDeviceContext->Clear(D2D1::ColorF(D2D1::ColorF::Blue, 0.3f)); assert(d2dDeviceContext->GetUnitMode() == D2D1_UNIT_MODE_DIPS); const auto dpi = m_layoutMetrics.pointScaleFactor * 96.0f; float oldDpiX, oldDpiY; @@ -105,6 +262,34 @@ void WindowsModalHostComponentView::updateLayoutMetrics( float offsetX = static_cast(offset.x / m_layoutMetrics.pointScaleFactor); float offsetY = static_cast(offset.y / m_layoutMetrics.pointScaleFactor); + + winrt::com_ptr spTextFormat; + winrt::check_hresult(::Microsoft::ReactNative::DWriteFactory()->CreateTextFormat( + L"Segoe UI", + nullptr, // Font collection (nullptr sets it to use the system font collection). + DWRITE_FONT_WEIGHT_REGULAR, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + 12, + L"", + spTextFormat.put())); + + winrt::com_ptr textBrush; + winrt::check_hresult( + d2dDeviceContext->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), textBrush.put())); + + const D2D1_RECT_F rect = { + static_cast(offset.x), static_cast(offset.y), width + offset.x, height + offset.y}; + + auto label = ::Microsoft::Common::Unicode::Utf8ToUtf16(std::string("This is a Modal")); + d2dDeviceContext->DrawText( + label.c_str(), + static_cast(label.length()), + spTextFormat.get(), + rect, + textBrush.get(), + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL); } } } @@ -126,6 +311,10 @@ winrt::Microsoft::ReactNative::Composition::IVisual WindowsModalHostComponentVie return m_visual; } +winrt::Microsoft::ReactNative::Composition::IVisual WindowsModalHostComponentView::OuterVisual() const noexcept { + return m_visual; +} + facebook::react::Tag WindowsModalHostComponentView::hitTest( facebook::react::Point pt, facebook::react::Point &localPt, diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h index 7b1d93a3751..7917d255e77 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Modal/WindowsModalHostViewComponentView.h @@ -1,4 +1,3 @@ - // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -30,20 +29,21 @@ struct WindowsModalHostComponentView : WindowsModalHostComponentViewT m_props; winrt::Microsoft::ReactNative::Composition::ISpriteVisual m_visual{nullptr}; - facebook::react::SharedViewProps m_props; + HWND m_hwnd{nullptr}; + winrt::Microsoft::ReactNative::ReactContext m_context; }; } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Views/UnimplementedViewManager.cpp b/vnext/Microsoft.ReactNative/Views/UnimplementedViewManager.cpp index a89e29687aa..529b5658dd0 100644 --- a/vnext/Microsoft.ReactNative/Views/UnimplementedViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/UnimplementedViewManager.cpp @@ -59,9 +59,11 @@ const wchar_t *UnimplementedViewManager::GetName() const { void UnimplementedViewManager::GetNativeProps(const winrt::Microsoft::ReactNative::IJSValueWriter &writer) const { Super::GetNativeProps(writer); + // Modal View Props (Todo: fix so these aren't in UnimplementedView) #12753 winrt::Microsoft::ReactNative::WriteProperty(writer, L"animationType", L"string"); winrt::Microsoft::ReactNative::WriteProperty(writer, L"presentationStyle", L"string"); winrt::Microsoft::ReactNative::WriteProperty(writer, L"focusable", L"boolean"); + winrt::Microsoft::ReactNative::WriteProperty(writer, L"visible", L"boolean"); } ShadowNode *UnimplementedViewManager::createShadow() const {