diff --git a/packages/app/test/pack.test.mts b/packages/app/test/pack.test.mts index ea28a3262..28be8f61a 100644 --- a/packages/app/test/pack.test.mts +++ b/packages/app/test/pack.test.mts @@ -278,6 +278,8 @@ describe("npm pack", () => { "windows/UWP/pch.h", "windows/Win32/AutolinkedNativeModules.g.cpp", "windows/Win32/AutolinkedNativeModules.g.h", + "windows/Win32/DevMenu.cpp", + "windows/Win32/DevMenu.h", "windows/Win32/Images/SplashScreen.scale-100.png", "windows/Win32/Images/SplashScreen.scale-200.png", "windows/Win32/Images/SplashScreen.scale-400.png", diff --git a/packages/app/windows/Win32/DevMenu.cpp b/packages/app/windows/Win32/DevMenu.cpp new file mode 100644 index 000000000..d283e14f9 --- /dev/null +++ b/packages/app/windows/Win32/DevMenu.cpp @@ -0,0 +1,188 @@ +#include "pch.h" + +#include "DevMenu.h" + +#include + +#include "ReactInstance.h" +#include "Session.h" + +using ReactApp::Component; +using ReactApp::DevMenu; +using ReactApp::DevMenuCommand; +using ReactTestApp::JSBundleSource; +using ReactTestApp::ReactInstance; +using ReactTestApp::Session; + +namespace +{ + constexpr wchar_t kLabelLoadFromJSBundle[] = L"Load from &JS bundle"; + constexpr wchar_t kLabelLoadFromDevServer[] = L"Load from &dev server"; + constexpr wchar_t kLabelRememberLastComponent[] = L"&Remember last opened component"; + constexpr wchar_t kLabelReloadJS[] = L"&Reload JavaScript"; + + constexpr wchar_t kLabelEnableDirectDebugger[] = L"Enable &direct debugging"; + constexpr wchar_t kLabelDisableDirectDebugger[] = L"Disable &direct debugging"; + + constexpr wchar_t kLabelEnableBreakOnFirstLine[] = L"Enable &break on first line"; + constexpr wchar_t kLabelDisableBreakOnFirstLine[] = L"Disable &break on first line"; + + constexpr wchar_t kLabelEnableFastRefresh[] = L"Enable &Fast Refresh"; + constexpr wchar_t kLabelDisableFastRefresh[] = L"Disable &Fast Refresh"; + + constexpr wchar_t kLabelToggleInspector[] = L"Toggle &inspector"; + + constexpr UINT ItemID(DevMenuCommand cmd) + { + return static_cast(cmd); + } + + HMENU CreateReactMenu(std::vector const &components) + { + auto hReactMenu = CreatePopupMenu(); + AppendMenuW(hReactMenu, // + MF_STRING, + ItemID(DevMenuCommand::LoadFromBundle), + kLabelLoadFromJSBundle); + AppendMenuW(hReactMenu, + MF_STRING, + ItemID(DevMenuCommand::LoadFromDevServer), + kLabelLoadFromDevServer); + + auto rememberLastComponent = + Session::ShouldRememberLastComponent() ? MF_CHECKED : MF_UNCHECKED; + AppendMenuW(hReactMenu, + MF_DISABLED | MF_STRING | rememberLastComponent, + ItemID(DevMenuCommand::RememberLastComponent), + kLabelRememberLastComponent); + + if (!components.empty()) { + AppendMenuW(hReactMenu, MF_SEPARATOR, 0, nullptr); + constexpr auto offset = ItemID(DevMenuCommand::ComponentsStart); + std::remove_const_t index = 0; + for (auto const &component : components) { + auto const &title = component.displayName.value_or(component.appKey); + // Add keyboard accelerator for the first nine (1-9) components + auto label = index < 8 ? winrt::to_hstring(title) + L"\tCtrl+Shift+" + + std::to_wstring(index + 1) + : winrt::to_hstring(title); + AppendMenuW(hReactMenu, MF_DISABLED | MF_STRING, offset + (++index), label.c_str()); + } + } + + return hReactMenu; + } + + HMENU CreateDebugMenu(ReactInstance const &instance) + { + auto hDebugMenu = CreatePopupMenu(); + AppendMenuW(hDebugMenu, // + MF_STRING, + ItemID(DevMenuCommand::ReloadJS), + kLabelReloadJS); + AppendMenuW(hDebugMenu, + MF_STRING, + ItemID(DevMenuCommand::DirectDebugger), + instance.UseDirectDebugger() ? kLabelDisableDirectDebugger + : kLabelEnableDirectDebugger); + AppendMenuW(hDebugMenu, + MF_STRING, + ItemID(DevMenuCommand::BreakOnFirstLine), + instance.BreakOnFirstLine() ? kLabelDisableBreakOnFirstLine + : kLabelEnableBreakOnFirstLine); + AppendMenuW(hDebugMenu, + MF_STRING, + ItemID(DevMenuCommand::FastRefresh), + instance.UseFastRefresh() ? kLabelDisableFastRefresh : kLabelEnableFastRefresh); + AppendMenuW(hDebugMenu, // + MF_STRING, + ItemID(DevMenuCommand::ToggleInspector), + kLabelToggleInspector); + return hDebugMenu; + } + + HMENU CreateDevMenu(ReactInstance const &instance, std::vector const &components) + { + auto hReactMenu = CreateReactMenu(components); + auto hDebugMenu = CreateDebugMenu(instance); + + auto hMenuBar = CreateMenu(); + AppendMenuW(hMenuBar, MF_POPUP, reinterpret_cast(hReactMenu), L"&React"); + AppendMenuW(hMenuBar, MF_POPUP, reinterpret_cast(hDebugMenu), L"&Debug"); + + return hMenuBar; + } + + void SetMenuItemLabel(HMENU hMenu, DevMenuCommand cmd, LPCWSTR label) + { + MENUITEMINFOW info{.cbSize = sizeof(MENUITEMINFO), + .fMask = MIIM_TYPE, + .dwTypeData = const_cast(label)}; + SetMenuItemInfoW(hMenu, ItemID(cmd), false, &info); + } +} // namespace + +DevMenu::DevMenu(ReactInstance &instance, std::vector const &components) + : instance_(instance), hMenu_(CreateDevMenu(instance, components)) +{ +} + +void DevMenu::OnCommand(DevMenuCommand cmd) const +{ + switch (cmd) { + case DevMenuCommand::LoadFromBundle: { + instance_.LoadJSBundleFrom(JSBundleSource::Embedded); + break; + } + case DevMenuCommand::LoadFromDevServer: { + instance_.LoadJSBundleFrom(JSBundleSource::DevServer); + break; + } + case DevMenuCommand::RememberLastComponent: { + auto rememberLastComponent = !Session::ShouldRememberLastComponent(); + Session::ShouldRememberLastComponent(rememberLastComponent); + CheckMenuItem(hMenu_, ItemID(cmd), rememberLastComponent ? MF_CHECKED : MF_UNCHECKED); + break; + } + case DevMenuCommand::ReloadJS: { + instance_.Reload(); + break; + } + case DevMenuCommand::DirectDebugger: { + auto useDirectDebugger = !instance_.UseDirectDebugger(); + instance_.UseDirectDebugger(useDirectDebugger); + SetMenuItemLabel(hMenu_, + cmd, + useDirectDebugger ? kLabelDisableDirectDebugger + : kLabelEnableDirectDebugger); + break; + } + case DevMenuCommand::BreakOnFirstLine: { + auto breakOnFirstLine = !instance_.BreakOnFirstLine(); + instance_.BreakOnFirstLine(breakOnFirstLine); + SetMenuItemLabel(hMenu_, + cmd, + breakOnFirstLine ? kLabelDisableBreakOnFirstLine + : kLabelEnableBreakOnFirstLine); + break; + } + case DevMenuCommand::FastRefresh: { + auto useFastRefresh = !instance_.UseFastRefresh(); + instance_.UseFastRefresh(useFastRefresh); + SetMenuItemLabel(hMenu_, // + cmd, + useFastRefresh ? kLabelDisableFastRefresh : kLabelEnableFastRefresh); + break; + } + case DevMenuCommand::ToggleInspector: { + instance_.ToggleElementInspector(); + break; + } + default: { + if (cmd > DevMenuCommand::ComponentsStart) { + // TODO + } + break; + } + } +} diff --git a/packages/app/windows/Win32/DevMenu.h b/packages/app/windows/Win32/DevMenu.h new file mode 100644 index 000000000..3b8ae5c69 --- /dev/null +++ b/packages/app/windows/Win32/DevMenu.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include "Manifest.h" + +namespace ReactTestApp +{ + class ReactInstance; +} + +namespace ReactApp +{ + enum class DevMenuCommand { + // Custom functions + LoadFromBundle = 1001, + LoadFromDevServer, + RememberLastComponent, + + // React Native core functions + ReloadJS = 2001, + DirectDebugger, + BreakOnFirstLine, + FastRefresh, + ToggleInspector, + + // User defined components + ComponentsStart = 3000, + }; + + class DevMenu + { + public: + DevMenu(ReactTestApp::ReactInstance &, std::vector const &); + + HMENU Handle() const + { + return hMenu_; + } + + void OnCommand(DevMenuCommand) const; + + // DevMenu is non-copyable + DevMenu(DevMenu const &) = delete; + DevMenu &operator=(DevMenu const &) = delete; + + private: + ReactTestApp::ReactInstance &instance_; + HMENU hMenu_; + }; +} // namespace ReactApp diff --git a/packages/app/windows/Win32/Main.cpp b/packages/app/windows/Win32/Main.cpp index 102d6826c..8e505403a 100644 --- a/packages/app/windows/Win32/Main.cpp +++ b/packages/app/windows/Win32/Main.cpp @@ -2,6 +2,9 @@ #include "Main.h" +#include + +#include "DevMenu.h" #include "JSValueWriterHelper.h" #include "Manifest.g.cpp" #include "ReactInstance.h" @@ -40,12 +43,15 @@ namespace } } // namespace -_Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE /* instance */, - HINSTANCE, - PSTR /* commandLine */, - int /* showCmd */) +LRESULT APIENTRY SubclassProc( + HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData); + +_Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE /* hInstance */, + HINSTANCE /* hPrevInstance */, + PSTR /* lpCmdLine */, + int /* nShowCmd */) { - auto manifest = ::ReactApp::GetManifest(); + auto manifest = ReactApp::GetManifest(); assert(manifest.components.has_value() && (*manifest.components).size() > 0 && "At least one component must be declared"); @@ -91,5 +97,33 @@ _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE /* instance */, window.Title(winrt::to_hstring(manifest.displayName)); window.Resize({600, 800}); +#if _DEBUG + ReactApp::DevMenu devMenu{instance, manifest.components.value_or({})}; + auto hWnd = GetWindowFromWindowId(window.Id()); + SetMenu(hWnd, devMenu.Handle()); + SetWindowSubclass(hWnd, + SubclassProc, + reinterpret_cast(&devMenu), + reinterpret_cast(&devMenu)); +#endif // _DEBUG + app.Start(); + return 0; +} + +LRESULT APIENTRY SubclassProc( + HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_COMMAND: { + auto const &devMenu = *reinterpret_cast(dwRefData); + devMenu.OnCommand(static_cast(LOWORD(wParam))); + return TRUE; + } + case WM_NCDESTROY: { + RemoveWindowSubclass(hWnd, SubclassProc, uIdSubclass); + break; + } + } + return DefSubclassProc(hWnd, uMsg, wParam, lParam); } diff --git a/packages/app/windows/Win32/ReactApp.vcxproj b/packages/app/windows/Win32/ReactApp.vcxproj index c0c267ad5..a9c01e304 100644 --- a/packages/app/windows/Win32/ReactApp.vcxproj +++ b/packages/app/windows/Win32/ReactApp.vcxproj @@ -89,7 +89,7 @@ stdcpp20 - shell32.lib;user32.lib;windowsapp.lib;%(AdditionalDependenices) + comctl32.lib;shell32.lib;user32.lib;windowsapp.lib;%(AdditionalDependenices) Windows true @@ -106,6 +106,7 @@ + @@ -117,6 +118,7 @@ +