diff --git a/App.xaml b/App.xaml index 15a4efed..2df906df 100644 --- a/App.xaml +++ b/App.xaml @@ -2,8 +2,19 @@ x:Class="moonlight_xbox_dx.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:moonlight_xbox_dx"> + xmlns:local="using:moonlight_xbox_dx" + xmlns:controls="using:Microsoft.UI.Xaml.Controls"> + - + + + + + + + + + + diff --git a/App.xaml.cpp b/App.xaml.cpp index e25e67e8..a7ee16c0 100644 --- a/App.xaml.cpp +++ b/App.xaml.cpp @@ -1,11 +1,15 @@ -// +// // App.xaml.cpp // Implementation of the App class. // #include "pch.h" +#define MLOG_TAG_OVERRIDE "App" #include -#include "MoonlightWelcome.xaml.h" +#include "UI\Pages\MoonlightWelcome.xaml.h" +#include "UI\Pages\MoonlightSettings.xaml.h" +#include "UI\Utilities\XamlHelpers.h" +#include "UI\Utilities\ToastService.h" using namespace moonlight_xbox_dx; @@ -23,6 +27,10 @@ using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Interop; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; +using namespace Windows::UI::Xaml::Media::Animation; + +static Windows::UI::Xaml::Controls::Frame^ s_rootFrame = nullptr; + /// /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). @@ -30,10 +38,36 @@ using namespace Windows::UI::Xaml::Navigation; App::App() { InitializeComponent(); + Utils::InitFileLogging(); + Utils::InstallCrashHandlers(); RequiresPointerMode = Windows::UI::Xaml::ApplicationRequiresPointerMode::WhenRequested; Suspending += ref new SuspendingEventHandler(this, &App::OnSuspending); Resuming += ref new EventHandler(this, &App::OnResuming); displayRequest = ref new Windows::System::Display::DisplayRequest(); + + this->UnhandledException += ref new Windows::UI::Xaml::UnhandledExceptionEventHandler( + [](Platform::Object^, Windows::UI::Xaml::UnhandledExceptionEventArgs^ e) { + try { + if (e != nullptr && e->Message != nullptr) { + std::wstring msg = e->Message->Data(); + MLOGF(Utils::LogLevel::Error, "Unhandled XAML exception: %S", msg.c_str()); + } + } catch (...) {} + try { if (e != nullptr) e->Handled = true; } catch (...) {} + }); + + GlobalMenuItems = ref new Platform::Collections::Vector(); + GlobalMenuItems->Append(ref new moonlight_xbox_dx::MenuItem( + ref new Platform::String(L"App Settings"), + ref new Platform::String(L""), + ref new Windows::Foundation::EventHandler([](Platform::Object^ s, Platform::Object^ e) { + try { + if (s_rootFrame != nullptr) { + s_rootFrame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightSettings::typeid), nullptr, ref new Windows::UI::Xaml::Media::Animation::EntranceNavigationTransitionInfo()); + } + } catch(...) {} + }) + )); } /// @@ -50,34 +84,27 @@ void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEvent // DebugSettings->EnableFrameRateCounter = true; // } // #endif - moonlight_xbox_dx::Utils::Log("Hello from Moonlight!\n"); - auto rootFrame = dynamic_cast(Window::Current->Content); + MLOG(moonlight_xbox_dx::Utils::LogLevel::Info, "Hello from Moonlight!\n"); - // Do not repeat app initialization when the Window already has content, - // just ensure that the window is active - if (rootFrame == nullptr) + if (s_rootFrame == nullptr) { - // Create a Frame to act as the navigation context and associate it with - // a SuspensionManager key - rootFrame = ref new Frame(); - - rootFrame->NavigationFailed += ref new Windows::UI::Xaml::Navigation::NavigationFailedEventHandler(this, &App::OnNavigationFailed); + s_rootFrame = ref new Frame(); + s_rootFrame->NavigationFailed += ref new NavigationFailedEventHandler(this, &App::OnNavigationFailed); - // Place the frame in the current Window - Window::Current->Content = rootFrame; + auto rootGrid = ref new Grid(); + rootGrid->Children->Append(s_rootFrame); + InitializeToastService(rootGrid); + Window::Current->Content = rootGrid; } - if (rootFrame->Content == nullptr) + if (s_rootFrame->Content == nullptr) { - // When the navigation stack isn't restored navigate to the first page, - // configuring the new page by passing required information as a navigation - // parameter - rootFrame->Navigate(TypeName(HostSelectorPage::typeid), e->Arguments); + s_rootFrame->Navigate(TypeName(HostSelectorPage::typeid), e->Arguments, ref new Windows::UI::Xaml::Media::Animation::SuppressNavigationTransitionInfo()); } if (m_menuPage == nullptr) { - m_menuPage = dynamic_cast(rootFrame->Content); + m_menuPage = dynamic_cast(s_rootFrame->Content); } // Ensure the current window is active Window::Current->Activate(); @@ -127,6 +154,9 @@ void App::OnNavigationFailed(Platform::Object ^sender, Windows::UI::Xaml::Naviga dialog->Content = e->Exception.ToString(); dialog->CloseButtonText = L"OK"; dialog->ShowAsync(); - //throw ref new FailureException("Failed to load Page " + e->SourcePageType.Name); } +Windows::UI::Xaml::Controls::Frame^ App::GetRootFrame() +{ + return s_rootFrame; +} diff --git a/App.xaml.h b/App.xaml.h index 0e7486c6..7c3e66b5 100644 --- a/App.xaml.h +++ b/App.xaml.h @@ -1,4 +1,4 @@ -// +// // App.xaml.h // Declaration of the App class. // @@ -6,7 +6,8 @@ #pragma once #include "App.g.h" -#include +#include "UI\Models\MenuItem.h" +#include "UI\Pages\HostSelectorPage.xaml.h" namespace moonlight_xbox_dx { @@ -17,6 +18,10 @@ namespace moonlight_xbox_dx { public: App(); + + static Windows::UI::Xaml::Controls::Frame^ GetRootFrame(); + + property Windows::Foundation::Collections::IObservableVector^ GlobalMenuItems; virtual void OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ e) override; void OnStateLoaded(); private: diff --git a/Assets/LargeTile.scale-100.png b/Assets/LargeTile.scale-100.png index bc8edc72..ba34a92a 100644 Binary files a/Assets/LargeTile.scale-100.png and b/Assets/LargeTile.scale-100.png differ diff --git a/Assets/LargeTile.scale-125.png b/Assets/LargeTile.scale-125.png index fd1fe653..bf408f73 100644 Binary files a/Assets/LargeTile.scale-125.png and b/Assets/LargeTile.scale-125.png differ diff --git a/Assets/LargeTile.scale-150.png b/Assets/LargeTile.scale-150.png index 09b5f9c9..d7e1dfd9 100644 Binary files a/Assets/LargeTile.scale-150.png and b/Assets/LargeTile.scale-150.png differ diff --git a/Assets/LargeTile.scale-200.png b/Assets/LargeTile.scale-200.png index 50080e19..d5afa268 100644 Binary files a/Assets/LargeTile.scale-200.png and b/Assets/LargeTile.scale-200.png differ diff --git a/Assets/LargeTile.scale-400.png b/Assets/LargeTile.scale-400.png index 70fd7c47..66d08f81 100644 Binary files a/Assets/LargeTile.scale-400.png and b/Assets/LargeTile.scale-400.png differ diff --git a/Assets/Play1.png b/Assets/Play1.png new file mode 100644 index 00000000..8ab1a301 Binary files /dev/null and b/Assets/Play1.png differ diff --git a/Assets/SmallTile.scale-100.png b/Assets/SmallTile.scale-100.png index 420a81f5..d407d2e8 100644 Binary files a/Assets/SmallTile.scale-100.png and b/Assets/SmallTile.scale-100.png differ diff --git a/Assets/SmallTile.scale-125.png b/Assets/SmallTile.scale-125.png index c6910731..be5918ae 100644 Binary files a/Assets/SmallTile.scale-125.png and b/Assets/SmallTile.scale-125.png differ diff --git a/Assets/SmallTile.scale-150.png b/Assets/SmallTile.scale-150.png index 7a6715f8..576a34a2 100644 Binary files a/Assets/SmallTile.scale-150.png and b/Assets/SmallTile.scale-150.png differ diff --git a/Assets/SmallTile.scale-200.png b/Assets/SmallTile.scale-200.png index d774338e..a10528d5 100644 Binary files a/Assets/SmallTile.scale-200.png and b/Assets/SmallTile.scale-200.png differ diff --git a/Assets/SmallTile.scale-400.png b/Assets/SmallTile.scale-400.png index f5b4d646..f2296c8f 100644 Binary files a/Assets/SmallTile.scale-400.png and b/Assets/SmallTile.scale-400.png differ diff --git a/Assets/SplashScreen.scale-100.png b/Assets/SplashScreen.scale-100.png index 32486ee1..96e61b7f 100644 Binary files a/Assets/SplashScreen.scale-100.png and b/Assets/SplashScreen.scale-100.png differ diff --git a/Assets/SplashScreen.scale-125.png b/Assets/SplashScreen.scale-125.png index 3ce084db..3ed011e7 100644 Binary files a/Assets/SplashScreen.scale-125.png and b/Assets/SplashScreen.scale-125.png differ diff --git a/Assets/SplashScreen.scale-150.png b/Assets/SplashScreen.scale-150.png index 8715c388..bc0547df 100644 Binary files a/Assets/SplashScreen.scale-150.png and b/Assets/SplashScreen.scale-150.png differ diff --git a/Assets/SplashScreen.scale-200.png b/Assets/SplashScreen.scale-200.png index e6eaf2b8..d3fc9197 100644 Binary files a/Assets/SplashScreen.scale-200.png and b/Assets/SplashScreen.scale-200.png differ diff --git a/Assets/SplashScreen.scale-400.png b/Assets/SplashScreen.scale-400.png index 62fa0763..01786401 100644 Binary files a/Assets/SplashScreen.scale-400.png and b/Assets/SplashScreen.scale-400.png differ diff --git a/Assets/Square150x150Logo.scale-100.png b/Assets/Square150x150Logo.scale-100.png index 7f3daae0..faa8dca4 100644 Binary files a/Assets/Square150x150Logo.scale-100.png and b/Assets/Square150x150Logo.scale-100.png differ diff --git a/Assets/Square150x150Logo.scale-125.png b/Assets/Square150x150Logo.scale-125.png index 80c4772c..cd38ec4e 100644 Binary files a/Assets/Square150x150Logo.scale-125.png and b/Assets/Square150x150Logo.scale-125.png differ diff --git a/Assets/Square150x150Logo.scale-150.png b/Assets/Square150x150Logo.scale-150.png index 5e997c79..f29c87ba 100644 Binary files a/Assets/Square150x150Logo.scale-150.png and b/Assets/Square150x150Logo.scale-150.png differ diff --git a/Assets/Square150x150Logo.scale-200.png b/Assets/Square150x150Logo.scale-200.png index e783f1b9..11dd66df 100644 Binary files a/Assets/Square150x150Logo.scale-200.png and b/Assets/Square150x150Logo.scale-200.png differ diff --git a/Assets/Square150x150Logo.scale-400.png b/Assets/Square150x150Logo.scale-400.png index 3bc8b957..2186ea13 100644 Binary files a/Assets/Square150x150Logo.scale-400.png and b/Assets/Square150x150Logo.scale-400.png differ diff --git a/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png index 2f3379d6..c394f499 100644 Binary files a/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png and b/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png index 0fbedddf..39471031 100644 Binary files a/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png and b/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png index b91aa006..510fcca9 100644 Binary files a/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png and b/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png index 2462dcc0..244b8f9b 100644 Binary files a/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png and b/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png index 427e4b4c..75aa1500 100644 Binary files a/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png and b/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/Assets/Square44x44Logo.altform-unplated_targetsize-16.png index 2f3379d6..c394f499 100644 Binary files a/Assets/Square44x44Logo.altform-unplated_targetsize-16.png and b/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/Assets/Square44x44Logo.altform-unplated_targetsize-256.png index b91aa006..510fcca9 100644 Binary files a/Assets/Square44x44Logo.altform-unplated_targetsize-256.png and b/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/Assets/Square44x44Logo.altform-unplated_targetsize-32.png index 2462dcc0..244b8f9b 100644 Binary files a/Assets/Square44x44Logo.altform-unplated_targetsize-32.png and b/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/Assets/Square44x44Logo.altform-unplated_targetsize-48.png index 427e4b4c..75aa1500 100644 Binary files a/Assets/Square44x44Logo.altform-unplated_targetsize-48.png and b/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Assets/Square44x44Logo.scale-100.png b/Assets/Square44x44Logo.scale-100.png index a81802a3..dea410dd 100644 Binary files a/Assets/Square44x44Logo.scale-100.png and b/Assets/Square44x44Logo.scale-100.png differ diff --git a/Assets/Square44x44Logo.scale-125.png b/Assets/Square44x44Logo.scale-125.png index a9f612f2..7702f1a7 100644 Binary files a/Assets/Square44x44Logo.scale-125.png and b/Assets/Square44x44Logo.scale-125.png differ diff --git a/Assets/Square44x44Logo.scale-150.png b/Assets/Square44x44Logo.scale-150.png index 189c25fb..1f3f64d9 100644 Binary files a/Assets/Square44x44Logo.scale-150.png and b/Assets/Square44x44Logo.scale-150.png differ diff --git a/Assets/Square44x44Logo.scale-200.png b/Assets/Square44x44Logo.scale-200.png index 489ff967..553b3982 100644 Binary files a/Assets/Square44x44Logo.scale-200.png and b/Assets/Square44x44Logo.scale-200.png differ diff --git a/Assets/Square44x44Logo.scale-400.png b/Assets/Square44x44Logo.scale-400.png index 4f4873e5..3508ff57 100644 Binary files a/Assets/Square44x44Logo.scale-400.png and b/Assets/Square44x44Logo.scale-400.png differ diff --git a/Assets/Square44x44Logo.targetsize-16.png b/Assets/Square44x44Logo.targetsize-16.png index b39306e3..e2b73cd8 100644 Binary files a/Assets/Square44x44Logo.targetsize-16.png and b/Assets/Square44x44Logo.targetsize-16.png differ diff --git a/Assets/Square44x44Logo.targetsize-24.png b/Assets/Square44x44Logo.targetsize-24.png index 5809c403..bf165d64 100644 Binary files a/Assets/Square44x44Logo.targetsize-24.png and b/Assets/Square44x44Logo.targetsize-24.png differ diff --git a/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Assets/Square44x44Logo.targetsize-24_altform-unplated.png index b4094c25..39471031 100644 Binary files a/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and b/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Assets/Square44x44Logo.targetsize-256.png b/Assets/Square44x44Logo.targetsize-256.png index 0a394102..7a1940c2 100644 Binary files a/Assets/Square44x44Logo.targetsize-256.png and b/Assets/Square44x44Logo.targetsize-256.png differ diff --git a/Assets/Square44x44Logo.targetsize-32.png b/Assets/Square44x44Logo.targetsize-32.png index c0935b06..411318f4 100644 Binary files a/Assets/Square44x44Logo.targetsize-32.png and b/Assets/Square44x44Logo.targetsize-32.png differ diff --git a/Assets/Square44x44Logo.targetsize-48.png b/Assets/Square44x44Logo.targetsize-48.png index 681953f0..3743a00d 100644 Binary files a/Assets/Square44x44Logo.targetsize-48.png and b/Assets/Square44x44Logo.targetsize-48.png differ diff --git a/Assets/StoreLogo.scale-100.png b/Assets/StoreLogo.scale-100.png index c212fbcf..62dfd12d 100644 Binary files a/Assets/StoreLogo.scale-100.png and b/Assets/StoreLogo.scale-100.png differ diff --git a/Assets/StoreLogo.scale-125.png b/Assets/StoreLogo.scale-125.png index 683f80ee..48fd5a3e 100644 Binary files a/Assets/StoreLogo.scale-125.png and b/Assets/StoreLogo.scale-125.png differ diff --git a/Assets/StoreLogo.scale-150.png b/Assets/StoreLogo.scale-150.png index b7e96207..1c770793 100644 Binary files a/Assets/StoreLogo.scale-150.png and b/Assets/StoreLogo.scale-150.png differ diff --git a/Assets/StoreLogo.scale-200.png b/Assets/StoreLogo.scale-200.png index a9fa64af..5f8bc58b 100644 Binary files a/Assets/StoreLogo.scale-200.png and b/Assets/StoreLogo.scale-200.png differ diff --git a/Assets/StoreLogo.scale-400.png b/Assets/StoreLogo.scale-400.png index eefca9c5..989af75f 100644 Binary files a/Assets/StoreLogo.scale-400.png and b/Assets/StoreLogo.scale-400.png differ diff --git a/Assets/Wide310x150Logo.scale-100.png b/Assets/Wide310x150Logo.scale-100.png index fdd57409..f6def0b8 100644 Binary files a/Assets/Wide310x150Logo.scale-100.png and b/Assets/Wide310x150Logo.scale-100.png differ diff --git a/Assets/Wide310x150Logo.scale-125.png b/Assets/Wide310x150Logo.scale-125.png index 51391d3f..3b49aa05 100644 Binary files a/Assets/Wide310x150Logo.scale-125.png and b/Assets/Wide310x150Logo.scale-125.png differ diff --git a/Assets/Wide310x150Logo.scale-150.png b/Assets/Wide310x150Logo.scale-150.png index 8077cd8d..fc62c214 100644 Binary files a/Assets/Wide310x150Logo.scale-150.png and b/Assets/Wide310x150Logo.scale-150.png differ diff --git a/Assets/Wide310x150Logo.scale-200.png b/Assets/Wide310x150Logo.scale-200.png index 32486ee1..96e61b7f 100644 Binary files a/Assets/Wide310x150Logo.scale-200.png and b/Assets/Wide310x150Logo.scale-200.png differ diff --git a/Assets/Wide310x150Logo.scale-400.png b/Assets/Wide310x150Logo.scale-400.png index e6eaf2b8..d3fc9197 100644 Binary files a/Assets/Wide310x150Logo.scale-400.png and b/Assets/Wide310x150Logo.scale-400.png differ diff --git a/Assets/grid.png b/Assets/grid.png new file mode 100644 index 00000000..02979cc2 Binary files /dev/null and b/Assets/grid.png differ diff --git a/Assets/loading2.gif b/Assets/loading2.gif new file mode 100644 index 00000000..11451479 Binary files /dev/null and b/Assets/loading2.gif differ diff --git a/Assets/logo1.gif b/Assets/logo1.gif new file mode 100644 index 00000000..70afe15e Binary files /dev/null and b/Assets/logo1.gif differ diff --git a/Assets/logo1.png b/Assets/logo1.png new file mode 100644 index 00000000..c68cdff9 Binary files /dev/null and b/Assets/logo1.png differ diff --git a/Assets/logo2.gif b/Assets/logo2.gif new file mode 100644 index 00000000..b155d32f Binary files /dev/null and b/Assets/logo2.gif differ diff --git a/Assets/logo_a.gif b/Assets/logo_a.gif new file mode 100644 index 00000000..f09d96ab Binary files /dev/null and b/Assets/logo_a.gif differ diff --git a/Assets/orbit_load.gif b/Assets/orbit_load.gif new file mode 100644 index 00000000..8af9474d Binary files /dev/null and b/Assets/orbit_load.gif differ diff --git a/Assets/orbit_load2.gif b/Assets/orbit_load2.gif new file mode 100644 index 00000000..ce1f4129 Binary files /dev/null and b/Assets/orbit_load2.gif differ diff --git a/Assets/orbit_load3.gif b/Assets/orbit_load3.gif new file mode 100644 index 00000000..804ed5ed Binary files /dev/null and b/Assets/orbit_load3.gif differ diff --git a/Assets/play.png b/Assets/play.png new file mode 100644 index 00000000..42f348eb Binary files /dev/null and b/Assets/play.png differ diff --git a/Common/DeviceResources.cpp b/Common/DeviceResources.cpp index ffc06598..0b2ff8af 100644 --- a/Common/DeviceResources.cpp +++ b/Common/DeviceResources.cpp @@ -1,11 +1,14 @@ #include "pch.h" #include "DeviceResources.h" +#define MLOG_TAG_OVERRIDE "DeviceResources" +#include "Utils.hpp" #include "DirectXHelper.h" +#include "UI\Utilities\EffectsLibrary.h" #include #include -#include -#include -#include +#include "UI\Pages\StreamPage.xaml.h" +#include "Streaming\FFmpegDecoder.h" +#include "Plot\ImGuiPlots.h" using namespace moonlight_xbox_dx; using namespace D2D1; @@ -220,7 +223,7 @@ void DX::DeviceResources::CreateWindowSizeDependentResources() 0 ); - Utils::Logf("m_swapChain->ResizeBuffers(%d x %d)\n", + MLOGF(Utils::LogLevel::Info, "m_swapChain->ResizeBuffers(%d x %d)\n", lround(m_d3dRenderTargetSize.Width), lround(m_d3dRenderTargetSize.Height)); if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) @@ -286,7 +289,7 @@ void DX::DeviceResources::CreateWindowSizeDependentResources() ) ); - Utils::Logf("CreateSwapChainForComposition(%d x %d)\n", + MLOGF(Utils::LogLevel::Info, "CreateSwapChainForComposition(%d x %d)\n", lround(m_d3dRenderTargetSize.Width), lround(m_d3dRenderTargetSize.Height)); DX::ThrowIfFailed( @@ -374,9 +377,6 @@ void DX::DeviceResources::SetSwapChainPanel(SwapChainPanel^ panel) m_compositionScaleX = panel->CompositionScaleX; m_compositionScaleY = panel->CompositionScaleY; - Utils::Logf("SwapChain logical size: %.0fx%.0f @ composition scale %.1fx%.1f\n", - m_logicalSize.Width, m_logicalSize.Height, m_compositionScaleX, m_compositionScaleY); - CreateWindowSizeDependentResources(); ComPtr panelNative; @@ -387,6 +387,7 @@ void DX::DeviceResources::SetSwapChainPanel(SwapChainPanel^ panel) // Setup Dear ImGui context float dpi = currentDisplayInformation->LogicalDpi / 96.0f; ImGui_Init(panelNative, dpi); + } // This method is called in the event handler for the SizeChanged event. @@ -490,7 +491,7 @@ void DX::DeviceResources::HandleDeviceLost() m_swapChain = nullptr; - Utils::Log("HandleDeviceLost()\n"); + MLOG(Utils::LogLevel::Error, "HandleDeviceLost()\n"); if (m_deviceNotify != nullptr) { @@ -541,7 +542,7 @@ void DX::DeviceResources::Present() } else if (hr == DXGI_ERROR_INVALID_CALL) { // Try to reset - Utils::Logf("Present() failed with DXGI_ERROR_INVALID_CALL\n"); + MLOGF(Utils::LogLevel::Error, "Present() failed with DXGI_ERROR_INVALID_CALL\n"); HandleDeviceLost(); } else { diff --git a/Common/DirectXHelper.h b/Common/DirectXHelper.h index 7a342a41..3c9aec6b 100644 --- a/Common/DirectXHelper.h +++ b/Common/DirectXHelper.h @@ -12,7 +12,7 @@ namespace DX // Set a breakpoint on this line to catch Win32 API errors. char msg[4096]; sprintf(msg, "Got generic error from HRESULT: %x\n", hr); - moonlight_xbox_dx::Utils::Log(msg); + moonlight_xbox_dx::Utils::Log(moonlight_xbox_dx::Utils::LogLevel::Error, msg); throw Platform::Exception::CreateException(hr); } @@ -25,7 +25,7 @@ namespace DX // Set a breakpoint on this line to catch Win32 API errors. char msg[4096]; sprintf(msg, "Got generic error from %s: %x\n", reason, hr); - moonlight_xbox_dx::Utils::Log(msg); + moonlight_xbox_dx::Utils::Log(moonlight_xbox_dx::Utils::LogLevel::Error, msg); throw Platform::Exception::CreateException(hr); } diff --git a/Common/ModalDialog.xaml b/Common/ModalDialog.xaml deleted file mode 100644 index b4493103..00000000 Binary files a/Common/ModalDialog.xaml and /dev/null differ diff --git a/Common/ModalDialog.xaml.cpp b/Common/ModalDialog.xaml.cpp deleted file mode 100644 index 55bd8058..00000000 Binary files a/Common/ModalDialog.xaml.cpp and /dev/null differ diff --git a/Common/ModalDialog.xaml.h b/Common/ModalDialog.xaml.h deleted file mode 100644 index 57ad80f3..00000000 Binary files a/Common/ModalDialog.xaml.h and /dev/null differ diff --git a/Common/TextConsole.cpp b/Common/TextConsole.cpp index 6f842999..24ad0703 100644 --- a/Common/TextConsole.cpp +++ b/Common/TextConsole.cpp @@ -7,6 +7,7 @@ #include "pch.h" #include "TextConsole.h" +#define MLOG_TAG_OVERRIDE "TextConsole" #include "../Utils.hpp" #include "SimpleMath.h" @@ -28,6 +29,7 @@ using namespace moonlight_xbox_dx; TextConsole::TextConsole() noexcept : m_layout{}, m_textColor(1.f, 1.f, 1.f, 1.f), + m_currentWriteColor(1.f, 1.f, 1.f, 1.f), m_debugOutput(false), m_columns(0), m_rows(0) @@ -40,6 +42,7 @@ _Use_decl_annotations_ TextConsole::TextConsole(ID3D11DeviceContext* context, const wchar_t* fontName) noexcept(false) : m_layout{}, m_textColor(1.f, 1.f, 1.f, 1.f), + m_currentWriteColor(1.f, 1.f, 1.f, 1.f), m_debugOutput(false), m_columns(0), m_rows(0), @@ -63,7 +66,7 @@ void TextConsole::Render() const float x = float(m_layout.left); const float y = float(m_layout.top); - const XMVECTOR color = XMLoadFloat4(&m_textColor); + const XMVECTOR defaultColor = XMLoadFloat4(&m_textColor); m_batch->Begin(); @@ -75,6 +78,7 @@ void TextConsole::Render() if (*m_lines[textLine]) { + const XMVECTOR color = m_lineColors ? XMLoadFloat4(&m_lineColors[textLine]) : defaultColor; m_font->DrawString(m_batch.get(), m_lines[textLine], pos, color); } @@ -103,6 +107,23 @@ void TextConsole::Write(const wchar_t* str) { std::lock_guard lock(m_mutex); + m_currentWriteColor = m_textColor; + ProcessString(str); + +#ifndef NDEBUG + if (m_debugOutput) + { + OutputDebugStringW(str); + } +#endif +} + +_Use_decl_annotations_ +void XM_CALLCONV TextConsole::Write(const wchar_t* str, DirectX::FXMVECTOR color) +{ + std::lock_guard lock(m_mutex); + + XMStoreFloat4(&m_currentWriteColor, color); ProcessString(str); #ifndef NDEBUG @@ -119,6 +140,7 @@ void TextConsole::WriteLine(const wchar_t* str) { std::lock_guard lock(m_mutex); + m_currentWriteColor = m_textColor; ProcessString(str); IncrementLine(); @@ -151,6 +173,7 @@ void TextConsole::Format(const wchar_t* strFormat, ...) va_end(argList); + m_currentWriteColor = m_textColor; ProcessString(m_tempBuffer.data()); #ifndef NDEBUG @@ -185,6 +208,12 @@ void TextConsole::SetWindow(const RECT& layout) lines[line] = buffer.get() + (columns + 1) * line; } + auto lineColors = std::make_unique(rows); + for (unsigned int line = 0; line < rows; ++line) + { + lineColors[line] = m_textColor; + } + if (m_lines) { const unsigned int c = std::min(columns, m_columns); @@ -194,14 +223,23 @@ void TextConsole::SetWindow(const RECT& layout) { memcpy(lines[line], m_lines[line], c * sizeof(wchar_t)); } + + if (m_lineColors) + { + for (unsigned int line = 0; line < r; ++line) + { + lineColors[line] = m_lineColors[line]; + } + } } std::swap(columns, m_columns); std::swap(rows, m_rows); std::swap(buffer, m_buffer); std::swap(lines, m_lines); + std::swap(lineColors, m_lineColors); - Utils::Logf("TextConsole initialized at (%d,%d) - (%d,%d) with %u rows, %u columns\n", + MLOGF(Utils::LogLevel::Info, "TextConsole initialized at (%d,%d) - (%d,%d) with %u rows, %u columns\n", layout.left, layout.top, layout.right, layout.bottom, m_rows, m_columns); if ((m_currentColumn >= m_columns) || (m_currentLine >= m_rows)) @@ -283,6 +321,8 @@ void TextConsole::ProcessString(_In_z_ const wchar_t* str) bool increment = false; + if (m_lineColors) m_lineColors[m_currentLine] = m_currentWriteColor; + if (m_currentColumn >= m_columns) { increment = true; diff --git a/Common/TextConsole.h b/Common/TextConsole.h index 83cdc628..78717062 100644 --- a/Common/TextConsole.h +++ b/Common/TextConsole.h @@ -41,6 +41,7 @@ namespace DX void Clear() noexcept; void Write(_In_z_ const wchar_t *str); + void XM_CALLCONV Write(_In_z_ const wchar_t *str, DirectX::FXMVECTOR color); void WriteLine(_In_z_ const wchar_t *str); void Format(_In_z_ _Printf_format_string_ const wchar_t* strFormat, ...); @@ -77,6 +78,8 @@ namespace DX std::unique_ptr m_buffer; std::unique_ptr m_lines; + std::unique_ptr m_lineColors; + DirectX::XMFLOAT4 m_currentWriteColor; std::vector m_tempBuffer; std::unique_ptr m_batch; diff --git a/Converters/BoolToTextConverter.h b/Converters/BoolToTextConverter.h deleted file mode 100644 index 0a72174c..00000000 --- a/Converters/BoolToTextConverter.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include "pch.h" - -namespace moonlight_xbox_dx { - - public ref class BoolToTextConverter sealed : Windows::UI::Xaml::Data::IValueConverter { - public: - property Platform::String^ TrueText; - property Platform::String^ FalseText; - - virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ language); - virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object^ parameter, Platform::String^ language); - }; - -} \ No newline at end of file diff --git a/MoonlightWelcome.xaml b/MoonlightWelcome.xaml deleted file mode 100644 index 3a22b3f3..00000000 --- a/MoonlightWelcome.xaml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - Welcome to Moonlight - Moonlight allows your Xbox to control and stream your Computer! - - - - - Servers - To make Moonlight work, you need a Server, you can use one of the following ones: - - - - - Sunshine - The recommended open source solution. Compatible with Windows/macOS/Linux and with any GPU. - https://github.com/LizardByte/Sunshine - - - - - - - - - - Hints - - - After installing your server, wait or press + for adding your PC manually - - - - During the stream, you can press + to show more options and enable mouse mode - - - - In mouse mode, you can move and scroll using your sticks, Left click Right click Guide/Xbox button - - - - In mouse mode, you can press to show Keyboard. You can also use an USB Keyboard - - - - Is your screen a quarter of its size? Change the global composition scale inside the Global Settings of the App - - - - - - - - Support - Need some Help? - - - - GitHub - Check the Wiki for Support Links, documentation and more! - https://github.com/TheElixZammuto/moonlight-xbox/wiki - - - - - - - - - diff --git a/Pages/AppPage.xaml b/Pages/AppPage.xaml deleted file mode 100644 index a07b0028..00000000 --- a/Pages/AppPage.xaml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Useful Shortcuts: + for toggling Menu and enabling mouse mode. - In Mouse mode: Left click Right click Show Keyboard Guide/Xbox button - In Keyboard mode: Close Left Click Right Click Move Press to Scroll - - - diff --git a/Pages/AppPage.xaml.cpp b/Pages/AppPage.xaml.cpp deleted file mode 100644 index 2558f454..00000000 --- a/Pages/AppPage.xaml.cpp +++ /dev/null @@ -1,382 +0,0 @@ -#include "pch.h" -#include "AppPage.Xaml.h" -#include "Common\ModalDialog.xaml.h" -#include "HostSettingsPage.xaml.h" -#include "HostSelectorPage.xaml.h" -#include "State\MoonlightClient.h" -#include "StreamPage.xaml.h" -#include "Utils.hpp" -#include -#include -#include - -using namespace moonlight_xbox_dx; -using namespace Platform; -using namespace Windows::Foundation; -using namespace Windows::UI::Core; -using namespace Windows::UI::Xaml; -using namespace Windows::UI::Xaml::Controls; -using namespace Windows::UI::Xaml::Input; -using namespace Windows::UI::Xaml::Media; -using namespace Windows::UI::Xaml::Navigation; -using namespace concurrency; -using namespace Windows::UI::Xaml::Hosting; -using namespace Windows::UI::Composition; - -// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 - -static MoonlightApp^ GetAppById(MoonlightHost^ host, int appId) { - if (host == nullptr) { - return nullptr; - } - - for (unsigned int i = 0; i < host->Apps->Size; ++i) { - auto app = host->Apps->GetAt(i); - if (app != nullptr && app->Id == appId) { - return app; - } - } - - return nullptr; -} - -AppPage::AppPage() -{ - InitializeComponent(); - Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseVisible); - - this->Loaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &AppPage::OnLoaded); - this->Unloaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &AppPage::OnUnloaded); -} - -void AppPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { - - MoonlightHost^ mhost = dynamic_cast(e->Parameter); - if (mhost == nullptr) return; - host = mhost; - host->UpdateHostInfo(true); - host->UpdateApps(); - - // Start background polling for app running state and connectivity - continueAppFetch.store(true); - wasConnected.store(host->Connected); - - Platform::WeakReference weakThis(this); - create_task([weakThis]() { - while (true) { - auto that = weakThis.Resolve(); - if (that == nullptr) break; - if (!that->continueAppFetch.load()) break; - try { - if (that->host != nullptr) { - that->host->UpdateAppRunningStates(); - if (that->wasConnected.load() && !that->host->Connected) { - that->wasConnected.store(false); - - // Show the disconnect dialog only if page instance still exists (no visible-page checks) - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - Windows::UI::Core::CoreDispatcherPriority::Normal, - ref new Windows::UI::Core::DispatchedHandler([weakThis]() { - auto inner = weakThis.Resolve(); - if (inner == nullptr) return; - - try { - auto dialog = ref new Windows::UI::Xaml::Controls::ContentDialog(); - dialog->Title = Utils::StringFromStdString("Disconnected"); - dialog->Content = Utils::StringFromStdString("Connection to host was lost."); - dialog->PrimaryButtonText = Utils::StringFromStdString("OK"); - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog)).then([weakThis](Windows::UI::Xaml::Controls::ContentDialogResult result) { - auto that2 = weakThis.Resolve(); - if (that2 == nullptr) return; - that2->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, ref new Windows::UI::Core::DispatchedHandler([that2]() { - try { - that2->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSelectorPage::typeid)); - } catch (const std::exception &e) { - Utils::Logf("[AppPage] Failed to navigate to HostSelectorPage after disconnect. Exception: %s\n", e.what()); - } catch (...) { - Utils::Log("[AppPage] Failed to navigate to HostSelectorPage after disconnect. Unknown Exception.\n"); - } - })); - }); - } catch (const std::exception &e) { - Utils::Logf("[AppPage] Failed to show disconnect dialog. Exception: %s\n", e.what()); - } catch (...) { - Utils::Log("[AppPage] Failed to show disconnect dialog. Unknown Exception.\n"); - } - })); - } - else if (!that->wasConnected.load() && that->host->Connected) { - that->wasConnected.store(true); - } - } - } catch (const std::exception &e) { - Utils::Logf("[AppPage] Failed to poll app and host running state. Exception: %s\n", e.what()); - } catch (...) { - Utils::Log("[AppPage] Failed to poll app and host running state. Unknown Exception.\n"); - } - Sleep(3000); - } - }); - - if (host->AutostartID >= 0 && GetApplicationState()->shouldAutoConnect) { - GetApplicationState()->shouldAutoConnect = false; - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([this]() { - this->Connect(host->AutostartID); - })); - } - GetApplicationState()->shouldAutoConnect = false; -} - -void AppPage::OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { - continueAppFetch.store(false); -} - -void AppPage::AppsGrid_ItemClick(Platform::Object ^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs ^ e) { - MoonlightApp ^ app = (MoonlightApp ^) e->ClickedItem; - this->currentApp = app; - - if (this->host != nullptr) { - for (unsigned int i = 0; i < this->host->Apps->Size; ++i) { - auto candidate = this->host->Apps->GetAt(i); - if (candidate != nullptr && candidate->CurrentlyRunning && candidate->Id != app->Id) { - this->closeAndStartButton_Click(nullptr, nullptr); - return; - } - } - } - - this->Connect(app->Id); -} - -void AppPage::Connect(int appId) { - - continueAppFetch.store(false); - - MoonlightApp^ app = GetAppById(host, appId); - - StreamConfiguration ^ config = ref new StreamConfiguration(); - config->hostname = host->LastHostname; - config->appID = appId; - config->appName = app ? app->Name : "App"; - config->width = host->Resolution->Width; - config->height = host->Resolution->Height; - config->bitrate = host->Bitrate; - config->FPS = host->FPS; - config->audioConfig = host->AudioConfig; - config->videoCodec = host->VideoCodec; - config->playAudioOnPC = host->PlayAudioOnPC; - config->enableHDR = host->EnableHDR; - config->enableSOPS = host->EnableSOPS; - config->framePacing = host->FramePacing; - config->enableStats = host->EnableStats; - config->enableGraphs = host->EnableGraphs; - if (config->enableHDR) { - host->VideoCodec = "HEVC (H.265)"; - } - bool result = this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(StreamPage::typeid), config); - if (!result) { - printf("C"); - } -} - -void AppPage::AppsGrid_RightTapped(Platform::Object ^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs ^ e) { - Utils::Log("AppPage::AppsGrid_RightTapped invoked\n"); - FrameworkElement ^ senderElement = (FrameworkElement ^) e->OriginalSource; - FrameworkElement ^ anchor = senderElement; - - if (senderElement != nullptr && senderElement->GetType()->FullName->Equals(GridViewItem::typeid->FullName)) { - auto gi = (GridViewItem ^) senderElement; - currentApp = (MoonlightApp ^)(gi->Content); - anchor = gi; - } else { - if (senderElement != nullptr) currentApp = (MoonlightApp ^)(senderElement->DataContext); - - if (currentApp == nullptr && this->HostsGrid != nullptr && this->HostsGrid->SelectedIndex >= 0) { - currentApp = (MoonlightApp ^) this->HostsGrid->SelectedItem; - auto container = (GridViewItem ^) this->HostsGrid->ContainerFromIndex(this->HostsGrid->SelectedIndex); - if (container != nullptr) - anchor = container; - else - anchor = this->HostsGrid; - } - } - - bool anyRunning = false; - MoonlightApp^ runningApp = nullptr; - if (this->host != nullptr) { - for (unsigned int i = 0; i < this->host->Apps->Size; ++i) { - auto candidate = this->host->Apps->GetAt(i); - if (candidate != nullptr && candidate->CurrentlyRunning) { - anyRunning = true; - runningApp = candidate; - break; - } - } - } - - this->resumeAppButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - this->closeAppButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - this->closeAndStartButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - - if (!anyRunning) { - this->resumeAppButton->Text = "Open App"; - this->resumeAppButton->Visibility = Windows::UI::Xaml::Visibility::Visible; - } else { - if (currentApp != nullptr && currentApp->CurrentlyRunning) { - this->resumeAppButton->Text = "Resume App"; - this->resumeAppButton->Visibility = Windows::UI::Xaml::Visibility::Visible; - this->closeAppButton->Visibility = Windows::UI::Xaml::Visibility::Visible; - } else { - if (currentApp != nullptr) { - this->closeAndStartButton->Visibility = Windows::UI::Xaml::Visibility::Visible; - } - } - } - - if (anchor != nullptr) { - this->ActionsFlyout->ShowAt(anchor); - } else { - this->ActionsFlyout->ShowAt(this->HostsGrid); - } -} - -void AppPage::resumeAppButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { - this->Connect(this->currentApp->Id); -} - -void AppPage::closeAndStartButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { - if (this->currentApp == nullptr) { - return; - } - - if (sender != nullptr) { - this->ExecuteCloseAndStart(); - return; - } - - auto dialog = ref new Windows::UI::Xaml::Controls::ContentDialog(); - dialog->Title = Utils::StringFromStdString("Confirm"); - dialog->Content = Utils::StringFromStdString("Close currently running app and connect?"); - dialog->PrimaryButtonText = Utils::StringFromStdString("Yes"); - dialog->CloseButtonText = Utils::StringFromStdString("Cancel"); - - Platform::WeakReference weakThis(this); - - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog)).then([weakThis](Windows::UI::Xaml::Controls::ContentDialogResult result) { - try { - if (result != Windows::UI::Xaml::Controls::ContentDialogResult::Primary) { - return; - } - - auto that = weakThis.Resolve(); - if (that == nullptr) return; - that->ExecuteCloseAndStart(); - } catch (const std::exception &e) { - Utils::Logf("closeAndStartButton_Click dialog task exception: %s\n", e.what()); - } catch (...) { - Utils::Log("closeAndStartButton_Click dialog task unknown exception\n"); - } - }); -} - -void AppPage::ExecuteCloseAndStart() { - auto that = this; - auto progressToken = ::moonlight_xbox_dx::ModalDialog::ShowProgressDialogToken(nullptr, Utils::StringFromStdString("Closing app...")); - moonlight_xbox_dx::Utils::Logf("AppPage::ExecuteCloseAndStart: progressToken=%llu\n", (unsigned long long)progressToken); - - concurrency::create_task(concurrency::create_async([that, progressToken]() { - try { - MoonlightClient client; - auto ipAddr = Utils::PlatformStringToStdString(that->host->LastHostname); - int status = client.Connect(ipAddr.c_str()); - if (status == 0) { - client.StopApp(); - Sleep(1000); - } - } catch (...) { - } - - that->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([that, progressToken]() { - try { - if (that->currentApp != nullptr) { - that->Connect(that->currentApp->Id); - } - ::moonlight_xbox_dx::ModalDialog::HideDialogByToken(progressToken); - } catch (const std::exception &e) { - Utils::Logf("ExecuteCloseAndStart UI exception: %s\n", e.what()); - } catch (...) { - Utils::Log("ExecuteCloseAndStart UI unknown exception\n"); - } - })); - })).then([](concurrency::task t) { - try { - t.get(); - } catch (const std::exception &e) { - Utils::Logf("ExecuteCloseAndStart task exception: %s\n", e.what()); - } catch (...) { - Utils::Log("ExecuteCloseAndStart unknown task exception\n"); - } - }); -} - -void AppPage::closeAppButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { - auto that = this; - - auto progressToken = ::moonlight_xbox_dx::ModalDialog::ShowProgressDialogToken(Utils::StringFromStdString("Closing"), Utils::StringFromStdString("Closing app...")); - moonlight_xbox_dx::Utils::Logf("AppPage::closeAppButton_Click: progressToken=%llu\n", (unsigned long long)progressToken); - - concurrency::create_task(concurrency::create_async([that, progressToken]() { - try { - MoonlightClient client; - auto ipAddr = Utils::PlatformStringToStdString(that->host->LastHostname); - int status = client.Connect(ipAddr.c_str()); - if (status == 0) { - client.StopApp(); - Sleep(1000); - } - } catch (...) { - } - - that->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, ref new Windows::UI::Core::DispatchedHandler([that, progressToken]() { - try { - that->host->UpdateHostInfo(true); - that->host->UpdateAppRunningStates(); - } catch (...) { - } - ::moonlight_xbox_dx::ModalDialog::HideDialogByToken(progressToken); - })); - })); -} - -void AppPage::backButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { - this->Frame->GoBack(); -} - -void AppPage::settingsButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { - bool result = this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSettingsPage::typeid), Host); -} - -void AppPage::OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args) -{ - // UWP on Xbox One triggers a back request whenever the B - // button is pressed which can result in the app being - // suspended if unhandled - if (this->Frame->CanGoBack) { - this->Frame->GoBack(); - args->Handled = true; - } - } - -void AppPage::OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { - auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); - m_back_cookie = navigation->BackRequested += ref new EventHandler(this, &AppPage::OnBackRequested); -} - -void AppPage::OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { - auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); - navigation->BackRequested -= m_back_cookie; - // stop background polling loop - continueAppFetch.store(false); -} - diff --git a/Pages/AppPage.xaml.h b/Pages/AppPage.xaml.h deleted file mode 100644 index 9ca8e4ea..00000000 --- a/Pages/AppPage.xaml.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// AppPage.xaml.h -// Declaration of the AppPage class -// - -#pragma once - -#include "Pages\AppPage.g.h" -#include "State\MoonlightApp.h" -#include - -namespace moonlight_xbox_dx -{ - /// - /// An empty page that can be used on its own or navigated to within a Frame. - /// - [Windows::Foundation::Metadata::WebHostHidden] - public ref class AppPage sealed - { - private: - MoonlightHost^ host; - MoonlightApp^ currentApp; - Windows::Foundation::EventRegistrationToken m_back_cookie; - std::atomic continueAppFetch{ false }; - std::atomic wasConnected{ false }; - protected: - virtual void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; - virtual void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; - void Connect(int app); - public: - AppPage(); - property MoonlightHost^ Host { - MoonlightHost^ get() { - return this->host; - } - } - void OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args); - private: - void AppsGrid_ItemClick(Platform::Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e); - void AppsGrid_RightTapped(Platform::Object^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs^ e); - void resumeAppButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void closeAppButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void closeAndStartButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void ExecuteCloseAndStart(); - void backButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void settingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - }; - } diff --git a/Pages/HostSelectorPage.xaml b/Pages/HostSelectorPage.xaml deleted file mode 100644 index c2688e83..00000000 --- a/Pages/HostSelectorPage.xaml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Moonlight - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Pages/HostSelectorPage.xaml.cpp b/Pages/HostSelectorPage.xaml.cpp deleted file mode 100644 index 5a96ec39..00000000 --- a/Pages/HostSelectorPage.xaml.cpp +++ /dev/null @@ -1,486 +0,0 @@ -// -// HostSelectorPage.xaml.cpp -// Implementation of the HostSelectorPage class -// - -#include "pch.h" -#include "HostSelectorPage.xaml.h" -#include "AppPage.xaml.h" -#include -#include "HostSettingsPage.xaml.h" -#include "Utils.hpp" -#include "MoonlightSettings.xaml.h" -#include "State\MDNSHandler.h" -#include "MoonlightWelcome.xaml.h" -#include "Common\ModalDialog.xaml.h" -#include -#include -#include -#include - -using namespace moonlight_xbox_dx; - -using namespace Platform; -using namespace Windows::Foundation; -using namespace Windows::Foundation::Collections; -using namespace Windows::UI::Xaml; -using namespace Windows::UI::Xaml::Controls; -using namespace Windows::UI::Xaml::Controls::Primitives; -using namespace Windows::UI::Xaml::Data; -using namespace Windows::UI::Xaml::Input; -using namespace Windows::UI::Xaml::Media; -using namespace Windows::UI::Xaml::Navigation; -using namespace Windows::UI::ViewManagement::Core; - -HostSelectorPage::HostSelectorPage() -{ - state = GetApplicationState(); - InitializeComponent(); -} - -void HostSelectorPage::NewHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - dialogHostnameTextBox = ref new TextBox(); - dialogHostnameTextBox->AcceptsReturn = false; - dialogHostnameTextBox->KeyDown += ref new Windows::UI::Xaml::Input::KeyEventHandler(this, &HostSelectorPage::OnKeyDown); - ContentDialog^ dialog = ref new ContentDialog(); - dialog->Content = dialogHostnameTextBox; - dialog->Title = L"Add new Host"; - dialog->IsSecondaryButtonEnabled = true; - dialog->PrimaryButtonText = "Ok"; - dialog->SecondaryButtonText = "Cancel"; - concurrency::create_task(dialog->ShowAsync()); - dialog->PrimaryButtonClick += ref new Windows::Foundation::TypedEventHandler(this, &HostSelectorPage::OnNewHostDialogPrimaryClick); -} - -void HostSelectorPage::OnNewHostDialogPrimaryClick(Windows::UI::Xaml::Controls::ContentDialog^ sender, Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs^ args) -{ - sender->IsPrimaryButtonEnabled = false; - Platform::String^ hostname = dialogHostnameTextBox->Text; - auto def = args->GetDeferral(); - Concurrency::create_task([def, hostname, this, args, sender]() { - bool status = state->AddHost(hostname); - if (!status) { - Platform::WeakReference weakThis(this); - concurrency::create_task(Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([sender, weakThis, hostname, def, args]() { - args->Cancel = true; - sender->Content = L"Failed to Connect to " + hostname; - def->Complete(); - }))) .then([](concurrency::task t) { - try { - t.get(); - } - catch (const std::exception &e) { - Utils::Logf("HostSelectorPage NewHost create_task exception: %s", e.what()); - } - catch (...) { - Utils::Log("HostSelectorPage NewHost create_task unknown exception"); - } - }); - return; - } - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([def]() { - def->Complete(); - })); - }); -} - -void HostSelectorPage::GridView_ItemClick(Platform::Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e) -{ - MoonlightHost^ host = (MoonlightHost^)e->ClickedItem; - - if (host->Connected && host->Paired) { - this->Connect(host); - return; - } - - if (host->Connected && !host->Paired) { - this->StartPairing(host); - return; - } - - FrameworkElement^ anchor = nullptr; - if (e && e->OriginalSource) { - anchor = dynamic_cast(e->OriginalSource); - } - - if (!anchor) { - auto gv = dynamic_cast(sender); - if (gv) { - auto container = dynamic_cast(gv->ContainerFromItem(host)); - if (container) anchor = container; - } - } - - this->ShowHostActions(anchor, host); -} - -void HostSelectorPage::ShowHostActions(Windows::UI::Xaml::FrameworkElement^ anchor, MoonlightHost^ host) -{ - currentHost = host; - - if (currentHost == nullptr) { - return; - } - - if (currentHost->Connected || currentHost->WolPolling) { - this->wakeHostButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - this->testConnectionButton->Visibility = Windows::UI::Xaml::Visibility::Visible; - } - else { - this->wakeHostButton->Visibility = Windows::UI::Xaml::Visibility::Visible; - this->testConnectionButton->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - } - - if (anchor) { - this->ActionsFlyout->ShowAt(anchor); - } - else { - this->ActionsFlyout->ShowAt(this->HostsGrid); - } -} - -void HostSelectorPage::StartPairing(MoonlightHost^ host) { - MoonlightClient* client = new MoonlightClient(); - char ipAddressStr[2048]; - wcstombs_s(NULL, ipAddressStr, host->LastHostname->Data(), 2047); - int status = client->Connect(ipAddressStr); - if (status != 0)return; - char* pin = client->GeneratePIN(); - ContentDialog^ dialog = ref new ContentDialog(); - wchar_t msg[4096]; - swprintf(msg, 4096, L"We need to pair the host before continuing. Type %S on your host to continue", pin); - dialog->Content = ref new Platform::String(msg); - dialog->PrimaryButtonText = "Ok"; - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog)); - Concurrency::create_task([dialog, host, client, pin]() { - int a = client->Pair(); - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([a, dialog, host]() - { - if (a == 0) { - ::moonlight_xbox_dx::ModalDialog::HideDialog(dialog); - } - else { - //dialog->Content = Utils::StringFromStdString(std::string(gs_error)); - } - host->UpdateHostInfo(true); - } - )); - }) .then([](concurrency::task t) { - try { - t.get(); - } - catch (const std::exception &e) { - Utils::Logf("HostSelectorPage StartPairing task exception: %s", e.what()); - } - catch (...) { - Utils::Log("HostSelectorPage StartPairing task unknown exception"); - } - }); -} - -void HostSelectorPage::removeHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - State->RemoveHost(currentHost); -} - -void HostSelectorPage::HostsGrid_RightTapped(Platform::Object^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs^ e) -{ - //Thanks to https://stackoverflow.com/questions/62878368/uwp-gridview-listview-get-the-righttapped-item-supporting-both-mouse-and-xbox-co - FrameworkElement^ senderElement = (FrameworkElement^)e->OriginalSource; - - // Determine the host from the tapped element - if (senderElement->GetType()->FullName->Equals(GridViewItem::typeid->FullName)) { - auto gi = (GridViewItem^)senderElement; - currentHost = (MoonlightHost^)(gi->Content); - } - else { - currentHost = (MoonlightHost^)(senderElement->DataContext); - } - - // Delegate to consolidated helper (use senderElement as anchor) - this->ShowHostActions(senderElement, currentHost); -} - -void HostSelectorPage::hostSettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - bool result = this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSettingsPage::typeid), currentHost); -} - -void HostSelectorPage::SettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - bool result = this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightSettings::typeid)); -} - -void HostSelectorPage::OnStateLoaded() { - if (GetApplicationState()->FirstTime) { - this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightWelcome::typeid)); - return; - } - - Concurrency::create_task([this]() { - for (auto a : GetApplicationState()->SavedHosts) { - a->UpdateHostInfo(false); - } - }).then([this]() { - if (GetApplicationState()->autostartInstance.size() > 0) { - auto pii = Utils::StringFromStdString(GetApplicationState()->autostartInstance); - for (unsigned int i = 0; i < GetApplicationState()->SavedHosts->Size; i++) { - auto host = GetApplicationState()->SavedHosts->GetAt(i); - if (host->InstanceId->Equals(pii)) { - auto that = this; - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - Windows::UI::Core::CoreDispatcherPriority::High, - ref new Windows::UI::Core::DispatchedHandler([that, host]() { - that->Connect(host); - }) - ); - break; - } - } - } - }).then([this](concurrency::task t) { - try { - t.get(); - } - catch (const std::exception &e) { - Utils::Logf("HostSelectorPage OnStateLoaded task exception: %s", e.what()); - } - catch (...) { - Utils::Log("HostSelectorPage OnStateLoaded task unknown exception"); - } - }); -} - -void HostSelectorPage::Connect(MoonlightHost^ host) { - if (!host->Connected)return; - if (!host->Paired) { - StartPairing(host); - return; - } - state->shouldAutoConnect = true; - continueFetch.store(false); - bool result = this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(AppPage::typeid), host); -} - -void HostSelectorPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { - Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseVisible); - continueFetch.store(true); - Concurrency::create_task([this] { - init_mdns(); - while (continueFetch.load()) { - query_mdns(); - for (auto a : GetApplicationState()->SavedHosts) { - a->UpdateHostInfo(true); - } - Sleep(5000); - } - }) .then([](concurrency::task t) { - try { - t.get(); - } - catch (const std::exception &e) { - Utils::Logf("HostSelectorPage mdns loop task exception: %s", e.what()); - } - catch (...) { - Utils::Log("HostSelectorPage mdns loop task unknown exception"); - } - }); -} - -void HostSelectorPage::OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e) -{ - if (e->Key == Windows::System::VirtualKey::Enter) { - CoreInputView::GetForCurrentView()->TryHide(); - } -} - -void moonlight_xbox_dx::HostSelectorPage::wakeHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - if (currentHost == nullptr) { - return; - } - - try { - bool success = State->WakeHost(currentHost); - if (!success) { - ContentDialog^ fail = ref new ContentDialog(); - fail->Title = "Wake Host Failed"; - fail->Content = "Failed to send Wake-on-LAN packet.\n\nPlease check if Wake-on-LAN is enabled on the host."; - fail->PrimaryButtonText = "OK"; - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(fail)); - } - - // After sending WoL successfully, aggressively poll this host for a short period - // to detect when it comes online (hosts can take time to boot/respond). - // Poll every 1s for up to 60s, stop early if Connected becomes true. - if (success) { - auto host = currentHost; - host->WolPolling = true; - concurrency::create_task(concurrency::create_async([host]() { - int consecutiveSuccess = 0; - for (int i = 0; i < 60; ++i) { - try { - host->UpdateHostInfo(false); - if (host->Connected) { - consecutiveSuccess++; - if (consecutiveSuccess >= 3) { - host->WolPolling = false; - break; - } - } else { - consecutiveSuccess = 0; - } - } catch (...) { - consecutiveSuccess = 0; - } - Sleep(1000); - } - })).then([host](concurrency::task t) { - try { t.get(); } catch (...) { } - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, ref new Windows::UI::Core::DispatchedHandler([host]() { - host->WolPolling = false; - })); - }); - } - } - catch (std::exception ex) { - ContentDialog^ dialog = ref new ContentDialog(); - dialog->Title = "Wake Host Error"; - dialog->Content = "An error occurred while trying to wake the host:\n\n" + Utils::StringFromChars((char*)ex.what()); - dialog->PrimaryButtonText = "OK"; - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog)); - } -} - -void HostSelectorPage::hostDetailsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { - if (currentHost == nullptr) return; - - auto panel = ref new Windows::UI::Xaml::Controls::StackPanel(); - panel->Spacing = 8; - - auto addLine = [panel](Platform::String^ label, Platform::String^ value) { - auto val = ref new Windows::UI::Xaml::Controls::TextBlock(); - val->Text = label + " " + value; - panel->Children->Append(val); - }; - - addLine(Utils::StringFromStdString("Hostname:"), currentHost->LastHostname == nullptr ? Utils::StringFromStdString("(null)") : currentHost->LastHostname); - addLine(Utils::StringFromStdString("Instance ID:"), currentHost->InstanceId == nullptr ? Utils::StringFromStdString("(null)") : currentHost->InstanceId); - addLine(Utils::StringFromStdString("Computer Name:"), currentHost->ComputerName == nullptr ? Utils::StringFromStdString("(null)") : currentHost->ComputerName); - addLine(Utils::StringFromStdString("Server Address:"), currentHost->ServerAddress == nullptr ? Utils::StringFromStdString("(null)") : currentHost->ServerAddress); - addLine(Utils::StringFromStdString("MAC Address:"), currentHost->MacAddress == nullptr ? Utils::StringFromStdString("(null)") : currentHost->MacAddress); - - auto scroll = ref new Windows::UI::Xaml::Controls::ScrollViewer(); - scroll->Content = panel; - scroll->VerticalScrollBarVisibility = Windows::UI::Xaml::Controls::ScrollBarVisibility::Auto; - scroll->MaxHeight = 400; - - auto dialog = ref new Windows::UI::Xaml::Controls::ContentDialog(); - dialog->Title = Utils::StringFromStdString("Host Details"); - dialog->Content = scroll; - dialog->PrimaryButtonText = Utils::StringFromStdString("OK"); - - try { - dialog->XamlRoot = this->XamlRoot; - } catch(...) {} - - concurrency::create_task(dialog->ShowAsync()); -} - -void HostSelectorPage::testConnectionButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { - if (currentHost == nullptr) return; - - std::string hostname = Utils::PlatformStringToStdString(currentHost->LastHostname); - std::string hostOnly; - int port = 0; - try { - auto split = GetApplicationState()->Split_IP_Address(hostname, ':'); - hostOnly = split.first; - port = split.second; - } - catch (...) { - // If parsing fails for any reason, fall back to original behaviour - auto pos = hostname.find(':'); - hostOnly = (pos == std::string::npos) ? hostname : hostname.substr(0, pos); - port = 0; - } - - concurrency::create_task([hostOnly, port]() { - WSADATA wsaData; - std::string resultMsg = "Unknown"; - if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) { - resultMsg = "WSAStartup failed"; - } else { - struct addrinfo hints; - struct addrinfo *res = nullptr; - ZeroMemory(&hints, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = IPPROTO_TCP; - - // If port is known (non-zero) use it, otherwise default to 47989 - std::string portStorage = (port > 0) ? std::to_string(port) : std::string("47989"); - int gai = getaddrinfo(hostOnly.c_str(), portStorage.c_str(), &hints, &res); - if (gai != 0) { - resultMsg = std::string("DNS lookup failed: ") + std::to_string(gai); - } else { - bool ok = false; - double bestRttMs = -1.0; - for (struct addrinfo *p = res; p != nullptr; p = p->ai_next) { - SOCKET s = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (s == INVALID_SOCKET) continue; - u_long mode = 1; - ioctlsocket(s, FIONBIO, &mode); - - using namespace std::chrono; - auto start = high_resolution_clock::now(); - int rc = connect(s, p->ai_addr, (int)p->ai_addrlen); - if (rc == 0) { - auto end = high_resolution_clock::now(); - double ms = duration_cast(end - start).count() / 1000.0; - if (bestRttMs < 0 || ms < bestRttMs) bestRttMs = ms; - ok = true; - closesocket(s); - break; - } - fd_set writeSet; - FD_ZERO(&writeSet); - FD_SET(s, &writeSet); - timeval tv; tv.tv_sec = 3; tv.tv_usec = 0; - int sel = select(0, NULL, &writeSet, NULL, &tv); - if (sel > 0 && FD_ISSET(s, &writeSet)) { - auto end = high_resolution_clock::now(); - double ms = duration_cast(end - start).count() / 1000.0; - if (bestRttMs < 0 || ms < bestRttMs) bestRttMs = ms; - ok = true; - closesocket(s); - break; - } - closesocket(s); - } - freeaddrinfo(res); - if (ok) { - if (bestRttMs >= 0) { - char buf[64]; - snprintf(buf, sizeof(buf), "Connection OK (RTT: %.1f ms)", bestRttMs); - resultMsg = buf; - } else { - resultMsg = "Connection OK"; - } - } else { - resultMsg = "Connection failed"; - } - } - WSACleanup(); - } - - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([resultMsg, hostOnly]() { - auto dialog = ref new Windows::UI::Xaml::Controls::ContentDialog(); - dialog->Title = Utils::StringFromStdString("Test Connection"); - dialog->Content = Utils::StringFromStdString(hostOnly + ": " + resultMsg); - dialog->PrimaryButtonText = Utils::StringFromStdString("OK"); - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog)); - })); - }); -} diff --git a/Pages/HostSelectorPage.xaml.h b/Pages/HostSelectorPage.xaml.h deleted file mode 100644 index a2008481..00000000 --- a/Pages/HostSelectorPage.xaml.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// HostSelectorPage.xaml.h -// Declaration of the HostSelectorPage class -// - -#pragma once - -#include "Pages\HostSelectorPage.g.h" -#include "State\ApplicationState.h" - -#include - -using namespace Windows::UI::Core; -namespace moonlight_xbox_dx -{ - /// - /// An empty page that can be used on its own or navigated to within a Frame. - /// - [Windows::Foundation::Metadata::WebHostHidden] - public ref class HostSelectorPage sealed - { - public: - HostSelectorPage(); - property ApplicationState^ State { - ApplicationState^ get() { - return this->state; - } - } - void OnStateLoaded(); - void Connect(MoonlightHost^ host); - protected: - virtual void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; - private: - ApplicationState ^state; - void NewHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void OnNewHostDialogPrimaryClick(Windows::UI::Xaml::Controls::ContentDialog^ sender, Windows::UI::Xaml::Controls::ContentDialogButtonClickEventArgs^ args); - Windows::UI::Xaml::Controls::TextBox ^dialogHostnameTextBox; - void GridView_ItemClick(Platform::Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e); - void StartPairing(MoonlightHost^ host); - void removeHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void HostsGrid_RightTapped(Platform::Object^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs^ e); - MoonlightHost^ currentHost; - void hostSettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void hostDetailsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void SettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - std::atomic continueFetch; - void OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e); - void wakeHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void testConnectionButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void ShowHostActions(Windows::UI::Xaml::FrameworkElement^ anchor, MoonlightHost^ host); - }; -} diff --git a/Pages/HostSettingsPage.xaml b/Pages/HostSettingsPage.xaml deleted file mode 100644 index ae0f31fa..00000000 --- a/Pages/HostSettingsPage.xaml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Resolution - - - - - - - - - - - - FPS - - Bitrate (Kbps) - - Audio Configuration - - Play Audio on Host PC - - Autostart - - - Video Codecs - - - Enable HDR: - - - HDR requires Xbox system resolution set to 4K. - - - Optimize host resolution: - - - Frame Pacing: - - - Best for Xbox Series. Lowest latency. Renders frames at the incoming frame rate. - - - Best for Xbox One. Locks rendering frame rate to refresh rate and evenly spaces frames. - - - Show performance stats: - - - Show performance graphs: - - - Graphs are unavailable on Xbox One when system resolution is set to 4K. - - - Other: - - - - - diff --git a/Pages/HostSettingsPage.xaml.cpp b/Pages/HostSettingsPage.xaml.cpp deleted file mode 100644 index 0aeaa137..00000000 --- a/Pages/HostSettingsPage.xaml.cpp +++ /dev/null @@ -1,266 +0,0 @@ -// -// HostSettingsPage.xaml.cpp -// Implementation of the HostSettingsPage class -// - -#include "pch.h" -#include "HostSettingsPage.xaml.h" -#include "MoonlightSettings.xaml.h" -#include "Utils.hpp" -#include -#include // sqrtf, lround -using namespace Windows::UI::Core; - -using namespace moonlight_xbox_dx; - -using namespace Platform; -using namespace Windows::Foundation; -using namespace Windows::Foundation::Collections; -using namespace Windows::Graphics::Display::Core; -using namespace Windows::UI::Xaml; -using namespace Windows::UI::Xaml::Controls; -using namespace Windows::UI::Xaml::Controls::Primitives; -using namespace Windows::UI::Xaml::Data; -using namespace Windows::UI::Xaml::Input; -using namespace Windows::UI::Xaml::Media; -using namespace Windows::UI::Xaml::Navigation; -using namespace Windows::UI::ViewManagement::Core; - -HostSettingsPage::HostSettingsPage() -{ - InitializeComponent(); - Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseVisible); - this->Loaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &HostSettingsPage::OnLoaded); - this->Unloaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &HostSettingsPage::OnUnloaded); -} - - -void HostSettingsPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { - MoonlightHost^ mhost = dynamic_cast(e->Parameter); - if (mhost == nullptr)return; - GAMING_DEVICE_MODEL_INFORMATION info = {}; - GetGamingDeviceModelInformation(&info); - host = mhost; - AvailableResolutions->Append(ref new ScreenResolution(1280, 720)); - AvailableResolutions->Append(ref new ScreenResolution(1920, 1080)); - //No 4K for Old Xbox One - if (!(info.vendorId == GAMING_DEVICE_VENDOR_ID_MICROSOFT && info.deviceId == GAMING_DEVICE_DEVICE_ID_XBOX_ONE)) { - AvailableResolutions->Append(ref new ScreenResolution(2560, 1440)); - AvailableResolutions->Append(ref new ScreenResolution(3840, 2160)); - } - AvailableFPS->Append(30); - AvailableFPS->Append(60); - AvailableFPS->Append(120); - AvailableVideoCodecs->Append("H.264"); - AvailableVideoCodecs->Append("HEVC (H.265)"); - AvailableAudioConfigs->Append("Stereo"); - AvailableAudioConfigs->Append("Surround 5.1"); - AvailableAudioConfigs->Append("Surround 7.1"); - AvailableFramePacing->Append("Immediate"); - AvailableFramePacing->Append("Display-locked"); - CurrentResolutionIndex = 0; - for (int i = 0; i < AvailableResolutions->Size; i++) { - if (host->Resolution->Width == AvailableResolutions->GetAt(i)->Width && - host->Resolution->Height == AvailableResolutions->GetAt(i)->Height - ) { - CurrentResolutionIndex = i; - break; - } - } - CurrentAppIndex = 0; - auto item = ref new ComboBoxItem(); - item->Content = L"No App"; - AutoStartSelector->Items->Append(item); - for (int i = 0; i < Host->Apps->Size; i++) { - auto item = ref new ComboBoxItem(); - item->Content = Host->Apps->GetAt(i)->Name; - AutoStartSelector->Items->Append(item); - if (host->AutostartID == host->Apps->GetAt(i)->Id) { - CurrentAppIndex = i + 1; - } - } - AutoStartSelector->SelectedIndex = CurrentAppIndex; - - // Set frame pacing selection based on saved preference - for (int i = 0; i < AvailableFramePacing->Size; i++) { - if (host->FramePacing == AvailableFramePacing->GetAt(i)) { - FramePacingComboBox->SelectedIndex = i; - break; - } - } - - if (info.vendorId == GAMING_DEVICE_VENDOR_ID_MICROSOFT) { - // Old Xbox One can only use H264, remove from settings everything else - if (info.deviceId == GAMING_DEVICE_DEVICE_ID_XBOX_ONE) { - CodecComboBox->IsEnabled = false; - CodecComboBox->SelectedIndex = 0; - } - - // Disable HDR if console is not set to 4K - auto mode = HdmiDisplayInformation::GetForCurrentView()->GetCurrentDisplayMode(); - auto height = mode->ResolutionHeightInRawPixels; - if (height < 2160) { - EnableHDRCheckbox->IsEnabled = false; - EnableHDRCheckbox->IsChecked = false; - EnableHDRCheckbox->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - HDR4KNote->Visibility = Windows::UI::Xaml::Visibility::Visible; - } else { - EnableHDRCheckbox->IsEnabled = true; - EnableHDRCheckbox->Visibility = Windows::UI::Xaml::Visibility::Visible; - HDR4KNote->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - } - - // Disable graphs at 4K on Xbox One - if (IsXboxOne() && height >= 2160) { - EnableGraphsCheckbox->IsEnabled = false; - EnableGraphsCheckbox->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - XboxOneGraphsNote->Visibility = Windows::UI::Xaml::Visibility::Visible; - } else { - EnableGraphsCheckbox->IsEnabled = true; - EnableGraphsCheckbox->Visibility = Windows::UI::Xaml::Visibility::Visible; - XboxOneGraphsNote->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - } - } -} - -void HostSettingsPage::backButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - GetApplicationState()->UpdateFile(); - this->Frame->GoBack(); -} - -void HostSettingsPage::OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args) -{ - // UWP on Xbox One triggers a back request whenever the B - // button is pressed which can result in the app being - // suspended if unhandled - GetApplicationState()->UpdateFile(); - this->Frame->GoBack(); - args->Handled = true; - -} - -void HostSettingsPage::ResolutionSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) -{ - auto selectedResolution = AvailableResolutions->GetAt(this->ResolutionSelector->SelectedIndex); - - // Default to a new bitrate if a new resolution was chosen - if (selectedResolution->Width != host->Resolution->Width) { - host->Bitrate = getDefaultBitrate(selectedResolution->Width, selectedResolution->Height, host->FPS); - } - - host->Resolution = selectedResolution; -} - -void HostSettingsPage::FPSSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) -{ - if (e->AddedItems->Size == 0) return; - int selectedFPS = (int)e->AddedItems->GetAt(0); - - // Default to a new bitrate if a new FPS was chosen - if (selectedFPS != host->FPS) { - host->Bitrate = getDefaultBitrate(host->Resolution->Width, host->Resolution->Height, selectedFPS); - } - - host->FPS = selectedFPS; -} - -void HostSettingsPage::AutoStartSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) -{ - int index = AutoStartSelector->SelectedIndex - 1; - if (index >= 0 && host->Apps->Size > index) { - host->AutostartID = host->Apps->GetAt(index)->Id; - } - else { - host->AutostartID = -1; - } -} - -void HostSettingsPage::FramePacing_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) -{ - auto selectedFramePacing = AvailableFramePacing->GetAt(this->FramePacingComboBox->SelectedIndex); - - if (selectedFramePacing == "Immediate") { - FramePacingImmediateDesc->Visibility = Windows::UI::Xaml::Visibility::Visible; - FramePacingDisplayLockedDesc->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - } else { - FramePacingImmediateDesc->Visibility = Windows::UI::Xaml::Visibility::Collapsed; - FramePacingDisplayLockedDesc->Visibility = Windows::UI::Xaml::Visibility::Visible; - } - - host->FramePacing = selectedFramePacing; -} - -void HostSettingsPage::GlobalSettingsOption_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightSettings::typeid)); -} - - -void HostSettingsPage::BitrateInput_KeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e) -{ - if (e->Key == Windows::System::VirtualKey::Enter) { - CoreInputView::GetForCurrentView()->TryHide(); - } -} - -void HostSettingsPage::OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); - m_back_cookie = navigation->BackRequested += ref new EventHandler(this, &HostSettingsPage::OnBackRequested); -} - -void HostSettingsPage::OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); - navigation->BackRequested -= m_back_cookie; -} - -int HostSettingsPage::getDefaultBitrate(int width, int height, int fps) -{ - // Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear - // bitrate increase for frame rate once we get to values that high. - float frameRateFactor = (fps <= 60 ? fps : (std::sqrtf(fps / 60.f) * 60.f)) / 30.f; - - // TODO: Collect some empirical data to see if these defaults make sense. - // We're just using the values that the Shield used, as we have for years. - static const struct resTable { - int pixels; - float factor; - } resTable[] { - { 1280 * 720, 5.0f }, - { 1920 * 1080, 10.0f }, - { 2560 * 1440, 20.0f }, - { 3840 * 2160, 40.0f }, - { -1, -1.0f }, - }; - - // Calculate the resolution factor by linear interpolation of the resolution table - float resolutionFactor; - int pixels = width * height; - for (int i = 0;; i++) { - if (pixels == resTable[i].pixels) { - // We can bail immediately for exact matches - resolutionFactor = resTable[i].factor; - break; - } - else if (pixels < resTable[i].pixels) { - if (i == 0) { - // Never go below the lowest resolution entry - resolutionFactor = resTable[i].factor; - } - else { - // Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1) - resolutionFactor = ((float)(pixels - resTable[i-1].pixels) / (resTable[i].pixels - resTable[i-1].pixels)) * (resTable[i].factor - resTable[i-1].factor) + resTable[i-1].factor; - } - break; - } - else if (resTable[i].pixels == -1) { - // Never go above the highest resolution entry - resolutionFactor = resTable[i-1].factor; - break; - } - } - - return std::lround(resolutionFactor * frameRateFactor) * 1000; -} diff --git a/Pages/MoonlightSettings.xaml b/Pages/MoonlightSettings.xaml deleted file mode 100644 index a70203db..00000000 --- a/Pages/MoonlightSettings.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - Looking to change Resolution, FPS or Bitrate? Use the settings button in the app grid to change the settings of your host - - - - - - - - - - - - - - - - - - - - - Autostart Host: - - Screen Margin Height: - - Screen Margin Width: - - Change these parameters until you can see a green border around the screen - Mouse Sensitivity: - - Experimental Keyboard - - Enabled - - Layout - - - - - diff --git a/Pages/MoonlightSettings.xaml.cpp b/Pages/MoonlightSettings.xaml.cpp deleted file mode 100644 index 476786b7..00000000 --- a/Pages/MoonlightSettings.xaml.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// -// MoonlightSettings.xaml.cpp -// Implementazione della classe MoonlightSettings -// - -#include "pch.h" -#include "MoonlightSettings.xaml.h" -#include "MoonlightWelcome.xaml.h" -#include "Utils.hpp" -#include "Keyboard/KeyboardCommon.h" -using namespace Windows::UI::Core; - -using namespace moonlight_xbox_dx; - -using namespace Platform; -using namespace Windows::Foundation; -using namespace Windows::Foundation::Collections; -using namespace Windows::UI::Xaml; -using namespace Windows::UI::Xaml::Controls; -using namespace Windows::UI::Xaml::Controls::Primitives; -using namespace Windows::UI::Xaml::Data; -using namespace Windows::UI::Xaml::Input; -using namespace Windows::UI::Xaml::Media; -using namespace Windows::UI::Xaml::Navigation; - -// Il modello di elemento Pagina vuota è documentato all'indirizzo https://go.microsoft.com/fwlink/?LinkId=234238 - -MoonlightSettings::MoonlightSettings() -{ - InitializeComponent(); - state = GetApplicationState(); - auto item = ref new ComboBoxItem(); - item->Content = "Don't autoconnect"; - item->DataContext = ""; - HostSelector->Items->Append(item); - - Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseCoreWindow); - auto iid = Utils::StringFromStdString(state->autostartInstance); - for (int i = 0; i < state->SavedHosts->Size;i++) { - auto host = state->SavedHosts->GetAt(i); - auto item = ref new ComboBoxItem(); - item->Content = host->LastHostname; - item->DataContext = host->InstanceId; - HostSelector->Items->Append(item); - if (host->InstanceId->Equals(iid)) { - HostSelector->SelectedIndex = i+1; - } - } - int k = 0; - for (auto l : keyboardLayouts) { - auto item = ref new ComboBoxItem(); - auto s = Utils::StringFromStdString(l.first); - item->Content = s; - item->DataContext = s; - KeyboardLayoutSelector->Items->Append(item); - if (state->KeyboardLayout->Equals(s)) { - KeyboardLayoutSelector->SelectedIndex = k; - } - k++; - } - this->Loaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &MoonlightSettings::OnLoaded); - this->Unloaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &MoonlightSettings::OnUnloaded); -} - -void MoonlightSettings::backButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - GetApplicationState()->UpdateFile(); - this->Frame->GoBack(); -} - -void MoonlightSettings::HostSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) -{ - ComboBoxItem^ item = (ComboBoxItem^)this->HostSelector->SelectedItem; - - auto s = Utils::PlatformStringToStdString(item->DataContext->ToString()); - state->autostartInstance = s; - -} - -void MoonlightSettings::OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args) -{ - // UWP on Xbox One triggers a back request whenever the B - // button is pressed which can result in the app being - // suspended if unhandled - GetApplicationState()->UpdateFile(); - this->Frame->GoBack(); - args->Handled = true; - -} - -void MoonlightSettings::WelcomeButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightWelcome::typeid)); -} - -void MoonlightSettings::LayoutSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e) -{ - ComboBoxItem^ item = (ComboBoxItem^)this->KeyboardLayoutSelector->SelectedItem; - - auto s = item->DataContext->ToString(); - state->KeyboardLayout = s; -} - -void MoonlightSettings::OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); - m_back_cookie = navigation->BackRequested += ref new EventHandler(this, &MoonlightSettings::OnBackRequested); -} - - -void MoonlightSettings::OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); - navigation->BackRequested -= m_back_cookie; -} diff --git a/Pages/MoonlightSettings.xaml.h b/Pages/MoonlightSettings.xaml.h deleted file mode 100644 index cec3de75..00000000 --- a/Pages/MoonlightSettings.xaml.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// MoonlightSettings.xaml.h -// Dichiarazione della classe MoonlightSettings -// - -#pragma once - -#include "Pages\MoonlightSettings.g.h" - -namespace moonlight_xbox_dx -{ - /// - /// Pagina vuota che può essere usata autonomamente oppure per l'esplorazione all'interno di un frame. - /// - [Windows::Foundation::Metadata::WebHostHidden] - public ref class MoonlightSettings sealed - { - private: - ApplicationState^ state; - Windows::Foundation::EventRegistrationToken m_back_cookie; - public: - MoonlightSettings(); - property ApplicationState^ State { - ApplicationState^ get() { - return this->state; - } - } - - void OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args); - private: - void backButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void HostSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); - void WelcomeButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void LayoutSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); - void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - }; -} diff --git a/Pages/StreamPage.xaml b/Pages/StreamPage.xaml deleted file mode 100644 index 1d15cde6..00000000 --- a/Pages/StreamPage.xaml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/State/ApplicationState.cpp b/State/ApplicationState.cpp index 611792ca..4fa9a09a 100644 --- a/State/ApplicationState.cpp +++ b/State/ApplicationState.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "ApplicationState.h" +#define MLOG_TAG_OVERRIDE "ApplicationState" #include #include @@ -30,6 +31,11 @@ Concurrency::task moonlight_xbox_dx::ApplicationState::Init() if (stateJson.contains("marginHeight"))this->ScreenMarginHeight = stateJson["marginHeight"]; if (stateJson.contains("mouseSensitivity"))this->MouseSensitivity = stateJson["mouseSensitivity"]; if (stateJson.contains("alternateCombination")) this->AlternateCombination = stateJson["alternateCombination"].get(); + if (stateJson.contains("recentHostnames")) { + for (auto& rh : stateJson["recentHostnames"]) { + recentHostnames.push_back(rh.get()); + } + } for (auto a : stateJson["hosts"]) { MoonlightHost^ h = ref new MoonlightHost(Utils::StringFromStdString(a["hostname"].get())); if (a.contains("instance_id")) h->InstanceId = Utils::StringFromStdString(a["instance_id"].get()); @@ -40,7 +46,6 @@ Concurrency::task moonlight_xbox_dx::ApplicationState::Init() if (a.contains("fps"))h->FPS = a["fps"]; if (a.contains("audioConfig"))h->AudioConfig = Utils::StringFromStdString(a["audioConfig"].get()); if (a.contains("videoCodec"))h->VideoCodec = Utils::StringFromStdString(a["videoCodec"].get()); - if (a.contains("framePacing"))h->FramePacing = Utils::StringFromStdString(a["framePacing"].get()); if (a.contains("autoStartID"))h->AutostartID = a["autoStartID"]; if (a.contains("computername")) h->ComputerName = Utils::StringFromStdString(a["computername"].get()); if (a.contains("playaudioonpc")) h->PlayAudioOnPC = a["playaudioonpc"].get(); @@ -48,19 +53,74 @@ Concurrency::task moonlight_xbox_dx::ApplicationState::Init() if (a.contains("enable_sops")) h->EnableSOPS = a["enable_sops"].get(); if (a.contains("enable_stats")) h->EnableStats = a["enable_stats"].get(); if (a.contains("enable_graphs")) h->EnableGraphs = a["enable_graphs"].get(); + if (a.contains("favoriteAppIds")) { + for (auto& fid : a["favoriteAppIds"]) { + h->favoriteAppIds.push_back(fid.get()); + } + } + if (a.contains("personalization")) { + auto& p = a["personalization"]; + if (p.contains("background")) h->Personalization->Background = Utils::StringFromStdString(p["background"].get()); + if (p.contains("app_view")) h->Personalization->AppView = (AppHostView)p["app_view"].get(); + if (p.contains("use_system_accent")) h->Personalization->UseSystemAccent = p["use_system_accent"].get(); + if (p.contains("accent_r") && p.contains("accent_g") && p.contains("accent_b")) { + Windows::UI::Color c; + c.A = 255; + c.R = (uint8_t)p["accent_r"].get(); + c.G = (uint8_t)p["accent_g"].get(); + c.B = (uint8_t)p["accent_b"].get(); + h->Personalization->AccentColor = c; + } + } if (a.contains("serverAddress")) h->ServerAddress = Utils::StringFromStdString(a["serverAddress"].get()); if (a.contains("macaddress")) h->MacAddress = Utils::StringFromStdString(a["macaddress"].get()); else h->ComputerName = h->LastHostname; this->SavedHosts->Append(h); } + OnPropertyChanged("HostSelectionTitle"); + OnPropertyChanged("HasSavedHosts"); + OnPropertyChanged("HasNoSavedHosts"); } + m_isStateLoaded = true; + OnPropertyChanged("ShowHostList"); + OnPropertyChanged("ShowEmptyState"); }); } +Windows::Foundation::Collections::IVector^ moonlight_xbox_dx::ApplicationState::RecentHostnames::get() +{ + auto result = ref new Platform::Collections::Vector(); + for (auto& s : recentHostnames) { + result->Append(Utils::StringFromStdString(s)); + } + return result; +} + +Windows::Foundation::Collections::IVector^ moonlight_xbox_dx::ApplicationState::RecentHostDisplayNames::get() +{ + auto result = ref new Platform::Collections::Vector(); + for (auto& s : recentHostnames) { + Platform::String^ hostname = Utils::StringFromStdString(s); + Platform::String^ computerName = ""; + for (auto h : SavedHosts) { + if (h->LastHostname == hostname && h->ComputerName != nullptr && h->ComputerName->Length() > 0) { + computerName = h->ComputerName; + break; + } + } + result->Append(computerName); + } + return result; +} + bool moonlight_xbox_dx::ApplicationState::AddHost(Platform::String^ hostname) { MoonlightHost^ host = ref new MoonlightHost(hostname); host->UpdateHostInfo(false); if (!host->Connected)return false; + std::string hostnameStd = Utils::PlatformStringToStdString(hostname); + recentHostnames.erase(std::remove(recentHostnames.begin(), recentHostnames.end(), hostnameStd), recentHostnames.end()); + recentHostnames.insert(recentHostnames.begin(), hostnameStd); + if (recentHostnames.size() > 5) recentHostnames.resize(5); for (auto h : SavedHosts) { if (host->InstanceId == h->InstanceId) { h->LastHostname = host->LastHostname; @@ -70,6 +130,11 @@ bool moonlight_xbox_dx::ApplicationState::AddHost(Platform::String^ hostname) { } Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([this,host]() { SavedHosts->Append(host); + OnPropertyChanged("HostSelectionTitle"); + OnPropertyChanged("HasSavedHosts"); + OnPropertyChanged("HasNoSavedHosts"); + OnPropertyChanged("ShowHostList"); + OnPropertyChanged("ShowEmptyState"); })); UpdateFile(); return true; @@ -90,6 +155,7 @@ Concurrency::task moonlight_xbox_dx::ApplicationState::UpdateFile() stateJson["enableKeyboard"] = that->EnableKeyboard; stateJson["keyboardLayout"] = Utils::PlatformStringToStdString(that->KeyboardLayout); stateJson["alternateCombination"] = that->AlternateCombination; + stateJson["recentHostnames"] = that->recentHostnames; for (auto host : that->SavedHosts) { nlohmann::json hostJson; hostJson["hostname"] = Utils::PlatformStringToStdString(host->LastHostname); @@ -101,13 +167,23 @@ Concurrency::task moonlight_xbox_dx::ApplicationState::UpdateFile() hostJson["fps"] = host->FPS; hostJson["audioConfig"] = Utils::PlatformStringToStdString(host->AudioConfig); hostJson["videoCodec"] = Utils::PlatformStringToStdString(host->VideoCodec); - hostJson["framePacing"] = Utils::PlatformStringToStdString(host->FramePacing); hostJson["autoStartID"] = host->AutostartID; hostJson["playaudioonpc"] = host->PlayAudioOnPC; hostJson["enable_hdr"] = host->EnableHDR; hostJson["enable_sops"] = host->EnableSOPS; hostJson["enable_stats"] = host->EnableStats; hostJson["enable_graphs"] = host->EnableGraphs; + hostJson["favoriteAppIds"] = host->favoriteAppIds; + { + nlohmann::json pJson; + pJson["background"] = Utils::PlatformStringToStdString(host->Personalization->Background); + pJson["app_view"] = (int)host->Personalization->AppView; + pJson["use_system_accent"] = host->Personalization->UseSystemAccent; + pJson["accent_r"] = (int)host->Personalization->AccentColor.R; + pJson["accent_g"] = (int)host->Personalization->AccentColor.G; + pJson["accent_b"] = (int)host->Personalization->AccentColor.B; + hostJson["personalization"] = pJson; + } hostJson["serverAddress"] = Utils::PlatformStringToStdString(host->ServerAddress); std::string macAddr = Utils::PlatformStringToStdString(host->MacAddress); @@ -124,17 +200,53 @@ Concurrency::task moonlight_xbox_dx::ApplicationState::UpdateFile() } void moonlight_xbox_dx::ApplicationState::RemoveHost(MoonlightHost^ host) { - if (host == nullptr)return; + if (host == nullptr) return; unsigned int index; bool found = SavedHosts->IndexOf(host, &index); + if (!found) return; SavedHosts->RemoveAt(index); - if (!host->Connected) { - host->Connect(); - } - if (host->Connected) { - host->Unpair(); - } + OnPropertyChanged("HostSelectionTitle"); + OnPropertyChanged("HasSavedHosts"); + OnPropertyChanged("HasNoSavedHosts"); + OnPropertyChanged("ShowHostList"); + OnPropertyChanged("ShowEmptyState"); UpdateFile(); + Concurrency::create_task([host]() { + + if (host->InstanceId != nullptr && !host->InstanceId->IsEmpty()) { + std::wstring dir = std::wstring( + Windows::Storage::ApplicationData::Current->LocalFolder->Path->Data()) + + L"\\images\\" + host->InstanceId->Data() + L"\\"; + auto deleteAllInDir = [](const std::wstring& d) { + WIN32_FIND_DATA fd; + HANDLE h = FindFirstFile((d + L"*").c_str(), &fd); + if (h != INVALID_HANDLE_VALUE) { + do { + if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + DeleteFile((d + fd.cFileName).c_str()); + } while (FindNextFile(h, &fd)); + FindClose(h); + } + }; + std::wstring blurDir = dir + L"blur\\"; + deleteAllInDir(blurDir); + RemoveDirectory(blurDir.c_str()); + deleteAllInDir(dir); + RemoveDirectory(dir.c_str()); + } + try { + if (!host->Connected) { + host->Connect(); + } + if (host->Connected) { + host->Unpair(); + } + } catch (Platform::Exception^ ex) { + MLOGF(Utils::LogLevel::Warning, "RemoveHost: failed to unpair before removal: %ws\n", ex->Message->Data()); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "RemoveHost: failed to unpair before removal (unknown exception)\n"); + } + }); } void moonlight_xbox_dx::ApplicationState::OnPropertyChanged(Platform::String^ propertyName) @@ -164,20 +276,38 @@ bool moonlight_xbox_dx::ApplicationState::WakeHost(MoonlightHost^ host) 47998, 47999, 48000, 48002, 48010 // Ports opened by GFE }; + std::vector sent; + std::vector failed; + bool result = Send_Payload(descriptor, wolPayload, "255.255.255.255", 0); + (result ? sent : failed).push_back("255.255.255.255:0"); for (int i = 0; i < addressList.size(); i++) { for (int j = 0; j < ports.size(); j++) { bool sendResult = Send_Payload(descriptor, wolPayload, addressList[i], ports[j]); + std::string target = addressList[i] + ":" + std::to_string(ports[j]); if (sendResult) { result = true; + sent.push_back(target); + } + else + { + failed.push_back(target); } } } closesocket(descriptor); + + std::string msg = "Wake-On-Lan packets sent to: " + Utils::Join(sent, ", "); + if (!failed.empty()) { + msg += "; failed to send to: " + Utils::Join(failed, ", "); + } + msg += "\n"; + MLOG(Utils::LogLevel::Info, msg.c_str()); + return result; } @@ -289,15 +419,7 @@ bool moonlight_xbox_dx::ApplicationState::Send_Payload(int descriptor, std::stri bind(descriptor, (struct sockaddr*)&addr, sizeof(addr)); int status = sendto(descriptor, payload.c_str(), (int)payload.length(), 0, (struct sockaddr*)&addr, sizeof(addr)); - if (status == SOCKET_ERROR) { - std::string msg = std::string() + "Error sending Wake-On-Lan packet to " + address.c_str() + ":" + Utils::PlatformStringToStdString(port.ToString()) + "\n"; - Utils::Log(msg.c_str()); - return false; - } - - std::string msg = std::string() + "Wake-On-Lan packet sent to " + address.c_str() + ":" + Utils::PlatformStringToStdString(port.ToString()) + "\n"; - Utils::Log(msg.c_str()); - return true; + return status != SOCKET_ERROR; } std::string moonlight_xbox_dx::ApplicationState::Get_Broadcast_IP(std::string ipAddress) @@ -335,7 +457,7 @@ std::string moonlight_xbox_dx::ApplicationState::Get_Broadcast_IP(std::string ip subnetMask = 4294967040; // 255.255.255.0 } else { - Utils::Log("Could not determine subnet mask from IP address.\n"); + MLOG(Utils::LogLevel::Warning, "Could not determine subnet mask from IP address.\n"); WSACleanup(); return ""; } @@ -380,6 +502,6 @@ std::pair moonlight_xbox_dx::ApplicationState::Split_IP_Addres void moonlight_xbox_dx::ApplicationState::Throw_Error(std::string message) { std::string msg = std::string() + message + "\n"; - Utils::Log(msg.c_str()); + MLOG(Utils::LogLevel::Error, msg.c_str()); throw std::runtime_error(message); } diff --git a/State/ApplicationState.h b/State/ApplicationState.h index 7841411d..5b31bdcc 100644 --- a/State/ApplicationState.h +++ b/State/ApplicationState.h @@ -15,6 +15,8 @@ namespace moonlight_xbox_dx { Platform::String^ keyboardLayout; bool firstTime; bool alternateCombination; + bool m_isStateLoaded = false; + std::vector recentHostnames; internal: Concurrency::task Init(); bool AddHost(Platform::String^ hostname); @@ -34,7 +36,6 @@ namespace moonlight_xbox_dx { std::pair Split_IP_Address(const std::string& address, char deliminator); void Throw_Error(std::string message); - public: //Thanks to https://phsucharee.wordpress.com/2013/06/19/data-binding-and-ccx-inotifypropertychanged/ virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; @@ -50,6 +51,41 @@ namespace moonlight_xbox_dx { return this->hosts; }; } + property Windows::Foundation::Collections::IVector^ RecentHostnames + { + Windows::Foundation::Collections::IVector^ get(); + } + property Windows::Foundation::Collections::IVector^ RecentHostDisplayNames + { + Windows::Foundation::Collections::IVector^ get(); + } + property Platform::String^ HostSelectionTitle + { + Platform::String^ get() + { + return (hosts != nullptr && hosts->Size > 0) ? "Select Host" : "Connect to a host to continue"; + }; + } + property bool HasSavedHosts + { + bool get() { return hosts != nullptr && hosts->Size > 0; } + } + property bool HasNoSavedHosts + { + bool get() { return hosts == nullptr || hosts->Size == 0; } + } + property bool ShowHostList + { + bool get() { return m_isStateLoaded && hosts != nullptr && hosts->Size > 0; } + } + property bool ShowEmptyState + { + bool get() { return m_isStateLoaded && (hosts == nullptr || hosts->Size == 0); } + } + property bool IsStateLoaded + { + bool get() { return m_isStateLoaded; } + } property int ScreenMarginWidth { int get() diff --git a/State/GamepadState.h b/State/GamepadState.h index c1dc21d3..a8ca559e 100644 --- a/State/GamepadState.h +++ b/State/GamepadState.h @@ -8,7 +8,9 @@ enum class ComboState { None, ViewWaiting, MenuWaiting, - ComboActive + MenuLongPressWaiting, + ComboActive, + ComboReleasing }; struct GamepadComboState { @@ -16,12 +18,16 @@ struct GamepadComboState { bool menuPressed = false; int64_t startTime = 0; ComboState comboState = ComboState::None; + bool menuLongPressFired = false; + int64_t menuInjectionStartQpc = 0; + int64_t viewInjectionStartQpc = 0; }; struct ComboResult { Windows::Gaming::Input::GamepadReading currentReading; // untouched reading Windows::Gaming::Input::GamepadReading maskedReading; // reading with pending combo buttons masked out - bool comboTriggered; // True when combo completes + bool comboTriggered; + bool menuLongPressTriggered; }; struct GamepadState { @@ -36,6 +42,7 @@ struct GamepadState { Windows::Gaming::Input::GamepadReading previousReading; bool previousGuideButtonDown; GamepadComboState combo; + Windows::Gaming::Input::GamepadButtons buttonSuppressMask = Windows::Gaming::Input::GamepadButtons::None; short ltX, ltY, rtX, rtY; // after a call to normalizeAxes() these are unsigned char lTrig, rTrig; // populated with values expected by the protocol @@ -73,6 +80,10 @@ struct GamepadState { combo.viewPressed = false; combo.menuPressed = false; combo.startTime = 0; + combo.menuLongPressFired = false; + combo.menuInjectionStartQpc = 0; + combo.viewInjectionStartQpc = 0; + buttonSuppressMask = Windows::Gaming::Input::GamepadButtons::None; } void SetGuideButtonDown(bool isDown) { @@ -83,10 +94,10 @@ struct GamepadState { return isGuideButtonDown.load(); } - ComboResult GetComboResult(int comboTimeoutMs) { + ComboResult GetComboResult(int comboTimeoutMs, int menuLongPressMs = 600) { using namespace Windows::Gaming::Input; - ComboResult result = ComboResult{EmptyReading(), EmptyReading(), false}; + ComboResult result = ComboResult{EmptyReading(), EmptyReading(), false, false}; if (controller == nullptr) { return result; } @@ -115,17 +126,37 @@ struct GamepadState { // Handle simultaneous press in the same poll if (viewCurrentlyPressed && menuCurrentlyPressed && !combo.viewPressed && !combo.menuPressed) { combo.comboState = ComboState::ComboActive; + combo.menuInjectionStartQpc = 0; + combo.viewInjectionStartQpc = 0; result.comboTriggered = true; maskedButtons = clearButtons(buttons, GamepadButtons::View | GamepadButtons::Menu); // Check if either button is newly pressed } else if (viewCurrentlyPressed && !combo.viewPressed) { combo.comboState = ComboState::ViewWaiting; combo.startTime = QpcNow(); + combo.viewInjectionStartQpc = 0; maskedButtons = clearButtons(buttons, GamepadButtons::View); } else if (menuCurrentlyPressed && !combo.menuPressed) { combo.comboState = ComboState::MenuWaiting; combo.startTime = QpcNow(); + combo.menuInjectionStartQpc = 0; maskedButtons = clearButtons(buttons, GamepadButtons::Menu); + } else { + + if (!menuCurrentlyPressed && combo.menuInjectionStartQpc != 0) { + if (QpcToMs(QpcNow() - combo.menuInjectionStartQpc) < 100) { + maskedButtons = setButtons(maskedButtons, GamepadButtons::Menu); + } else { + combo.menuInjectionStartQpc = 0; + } + } + if (!viewCurrentlyPressed && combo.viewInjectionStartQpc != 0) { + if (QpcToMs(QpcNow() - combo.viewInjectionStartQpc) < 100) { + maskedButtons = setButtons(maskedButtons, GamepadButtons::View); + } else { + combo.viewInjectionStartQpc = 0; + } + } } break; @@ -133,14 +164,14 @@ struct GamepadState { // Check timeout if (QpcToMs(QpcNow() - combo.startTime) > comboTimeoutMs) { combo.comboState = ComboState::None; - maskedButtons = setButtons(buttons, GamepadButtons::View); + combo.viewInjectionStartQpc = QpcNow(); break; } // Check if View was released before combo completed if (!viewCurrentlyPressed) { combo.comboState = ComboState::None; - maskedButtons = setButtons(buttons, GamepadButtons::View); + combo.viewInjectionStartQpc = QpcNow(); break; } @@ -150,6 +181,7 @@ struct GamepadState { // Check if Menu is now pressed (combo complete) if (menuCurrentlyPressed && !combo.menuPressed) { combo.comboState = ComboState::ComboActive; + combo.viewInjectionStartQpc = 0; result.comboTriggered = true; maskedButtons = clearButtons(buttons, GamepadButtons::View | GamepadButtons::Menu); } @@ -157,14 +189,15 @@ struct GamepadState { case ComboState::MenuWaiting: if (QpcToMs(QpcNow() - combo.startTime) > comboTimeoutMs) { - combo.comboState = ComboState::None; - maskedButtons = setButtons(buttons, GamepadButtons::Menu); + combo.comboState = ComboState::MenuLongPressWaiting; + combo.menuLongPressFired = false; + maskedButtons = clearButtons(buttons, GamepadButtons::Menu); break; } if (!menuCurrentlyPressed) { combo.comboState = ComboState::None; - maskedButtons = setButtons(buttons, GamepadButtons::Menu); + combo.menuInjectionStartQpc = QpcNow(); break; } @@ -177,13 +210,36 @@ struct GamepadState { } break; + case ComboState::MenuLongPressWaiting: + if (!menuCurrentlyPressed) { + combo.comboState = ComboState::None; + if (!combo.menuLongPressFired) { + combo.menuInjectionStartQpc = QpcNow(); + } + break; + } + + maskedButtons = clearButtons(buttons, GamepadButtons::Menu); + + if (!combo.menuLongPressFired && QpcToMs(QpcNow() - combo.startTime) > menuLongPressMs) { + combo.menuLongPressFired = true; + result.menuLongPressTriggered = true; + } + break; + case ComboState::ComboActive: // Remain in combo state while both are held if (viewCurrentlyPressed && menuCurrentlyPressed) { - // Continue masking both buttons maskedButtons = clearButtons(buttons, GamepadButtons::View | GamepadButtons::Menu); } else { - // One or both released, reset state + combo.comboState = ComboState::ComboReleasing; + maskedButtons = clearButtons(buttons, GamepadButtons::View | GamepadButtons::Menu); + } + break; + + case ComboState::ComboReleasing: + maskedButtons = clearButtons(buttons, GamepadButtons::View | GamepadButtons::Menu); + if (!viewCurrentlyPressed && !menuCurrentlyPressed) { combo.comboState = ComboState::None; } break; @@ -310,6 +366,7 @@ struct GamepadState { char buttons[128]; DumpButtons(reading.Buttons, buttons, sizeof(buttons)); moonlight_xbox_dx::Utils::Logf( + moonlight_xbox_dx::Utils::LogLevel::Verbose, "GamepadState[localId: %d, hostId: %d] buttons: %s %s axes: %d %d, %d %d, triggers: %d %d, combo{ state: %d, viewPressed: %d, menuPressed: %d, startTime: %d }\n", localId, hostId, buttons, diff --git a/State/MDNSHandler.cpp b/State/MDNSHandler.cpp index 6f037aa9..b308dc28 100644 --- a/State/MDNSHandler.cpp +++ b/State/MDNSHandler.cpp @@ -123,6 +123,14 @@ void init_mdns() { } } +void mdns_send_query() { + auto str = "_nvstream._tcp.local"; + for (int i = 0; i < 8; i++) { + if (sockets[i] == 0) break; + mdns_query_send(sockets[i], MDNS_RECORDTYPE_PTR, str, strlen(str), &mdns_buffer, 4096, 0); + } +} + int query_mdns() { for (int i = 0; i < 8; i++) { if (sockets[i] == 0) break; diff --git a/State/MDNSHandler.h b/State/MDNSHandler.h index 4f53eb19..5bcbd95b 100644 --- a/State/MDNSHandler.h +++ b/State/MDNSHandler.h @@ -1,5 +1,5 @@ #pragma once - void init_mdns(); +void mdns_send_query(); int query_mdns(); \ No newline at end of file diff --git a/State/MoonlightApp.cpp b/State/MoonlightApp.cpp index e2b29b46..6e212f01 100644 --- a/State/MoonlightApp.cpp +++ b/State/MoonlightApp.cpp @@ -7,11 +7,19 @@ namespace moonlight_xbox_dx { void MoonlightApp::OnPropertyChanged(Platform::String^ propertyName) { - Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( - Windows::UI::Core::CoreDispatcherPriority::High, - ref new Windows::UI::Core::DispatchedHandler([this, propertyName]() - { - PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName)); - })); + auto dispatcher = Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher; + if (dispatcher->HasThreadAccess) + { + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName)); + } + else + { + dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::High, + ref new Windows::UI::Core::DispatchedHandler([this, propertyName]() + { + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName)); + })); + } } } \ No newline at end of file diff --git a/State/MoonlightApp.h b/State/MoonlightApp.h index 2c977675..e576da86 100644 --- a/State/MoonlightApp.h +++ b/State/MoonlightApp.h @@ -1,4 +1,5 @@ #pragma once +#include namespace moonlight_xbox_dx { [Windows::UI::Xaml::Data::Bindable] @@ -9,6 +10,14 @@ namespace moonlight_xbox_dx { Platform::String^ imagePath = "ms-appx:///Assets/gamepad.svg"; int id; bool currentlyRunning; + bool isGridLayout = false; + bool isSelected = false; + bool isFavorite = false; + Windows::UI::Xaml::Media::Imaging::BitmapImage^ image; + + Windows::UI::Xaml::Media::Imaging::BitmapImage^ blurredImage; + + Windows::UI::Xaml::Media::Imaging::BitmapImage^ glowImage; public: //Thanks to https://phsucharee.wordpress.com/2013/06/19/data-binding-and-ccx-inotifypropertychanged/ virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; @@ -31,6 +40,26 @@ namespace moonlight_xbox_dx { void set(Platform::String^ path) { this->imagePath = path; OnPropertyChanged("ImagePath"); + + if (path != nullptr && this->image == nullptr) { + try { + auto uri = ref new Windows::Foundation::Uri(path); + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::High, + ref new Windows::UI::Core::DispatchedHandler([this, uri]() { + try { + auto bitmap = ref new Windows::UI::Xaml::Media::Imaging::BitmapImage(); + bitmap->UriSource = uri; + bitmap->DecodePixelWidth = 1024; + bitmap->CreateOptions = Windows::UI::Xaml::Media::Imaging::BitmapCreateOptions::IgnoreImageCache; + this->image = bitmap; + OnPropertyChanged("Image"); + } catch(...) {} + }) + ); + } catch (...) { + } + } } } @@ -51,5 +80,66 @@ namespace moonlight_xbox_dx { OnPropertyChanged("CurrentlyRunning"); } } + + property bool IsGridLayout + { + bool get() { return this->isGridLayout; } + void set(bool value) { + if (this->isGridLayout == value) return; + this->isGridLayout = value; + OnPropertyChanged("IsGridLayout"); + } + } + + property bool IsSelected + { + bool get() { return this->isSelected; } + void set(bool value) { + if (this->isSelected == value) return; + this->isSelected = value; + OnPropertyChanged("IsSelected"); + } + } + + property bool IsFavorite + { + bool get() { return this->isFavorite; } + void set(bool value) { + if (this->isFavorite == value) return; + this->isFavorite = value; + OnPropertyChanged("IsFavorite"); + } + } + + property Windows::UI::Xaml::Media::Imaging::BitmapImage^ Image + { + Windows::UI::Xaml::Media::Imaging::BitmapImage^ get() { return this->image; } + void set(Windows::UI::Xaml::Media::Imaging::BitmapImage^ value) { + if (this->image == value) return; + this->image = value; + try { OnPropertyChanged("Image"); } catch(...) {} + } + } + + property Windows::UI::Xaml::Media::Imaging::BitmapImage^ BlurredImage + { + Windows::UI::Xaml::Media::Imaging::BitmapImage^ get() { return this->blurredImage; } + void set(Windows::UI::Xaml::Media::Imaging::BitmapImage^ value) { + if (this->blurredImage == value) return; + this->blurredImage = value; + try { OnPropertyChanged("BlurredImage"); } catch(...) {} + } + } + + property Windows::UI::Xaml::Media::Imaging::BitmapImage^ GlowImage + { + Windows::UI::Xaml::Media::Imaging::BitmapImage^ get() { return this->glowImage; } + void set(Windows::UI::Xaml::Media::Imaging::BitmapImage^ value) { + if (this->glowImage == value) return; + this->glowImage = value; + try { OnPropertyChanged("GlowImage"); } catch(...) {} + } + } + }; } \ No newline at end of file diff --git a/State/MoonlightClient.cpp b/State/MoonlightClient.cpp index 67731e5a..71209d19 100644 --- a/State/MoonlightClient.cpp +++ b/State/MoonlightClient.cpp @@ -1,5 +1,6 @@ #include "MoonlightClient.h" #include "pch.h" +#define MLOG_TAG_OVERRIDE "MoonlightClient" extern "C" { #include @@ -10,7 +11,10 @@ extern "C" { #include #include #include +#include #include +#include +#include #include #include "Streaming\FFMpegDecoder.h" @@ -19,9 +23,7 @@ using namespace Windows::Gaming::Input; using namespace Windows::Graphics::Display; using namespace Windows::Graphics::Display::Core; -std::atomic g_connectionTerminated{false}; - -void log_message(const char* fmt, ...); +void log_message(const char *fmt, ...); void connection_started(); void connection_status_update(int status); void connection_status_completed(int status); @@ -51,7 +53,7 @@ void logDisplayMode(const char *str, HdmiDisplayMode ^ mode) { : mode->PixelEncoding == HdmiDisplayPixelEncoding::Ycc422 ? "Ycc422" : mode->PixelEncoding == HdmiDisplayPixelEncoding::Ycc420 ? "Ycc420" : "Unknown"); - Utils::Log(modeStr); + MLOG(Utils::LogLevel::Debug, modeStr); } // Based on CWIN32Util::ToggleWindowsHDR from xbmc @@ -71,21 +73,21 @@ bool MoonlightClient::SetDisplayHDR(bool enabled, const SS_HDR_METADATA &sunshin // HDR is enabled m_isHDR = true; if (enabled) { - Utils::Log("SetDisplayHDR(true): display is already in HDR mode\n"); + MLOG(Utils::LogLevel::Info, "SetDisplayHDR(true): display is already in HDR mode\n"); resendCurrentMode = true; } } else { // HDR is disabled m_isHDR = false; if (!enabled) { - Utils::Log("SetDisplayHDR(false): display is already in SDR mode\n"); + MLOG(Utils::LogLevel::Info, "SetDisplayHDR(false): display is already in SDR mode\n"); return true; } } // this method is only run on Series S/X, so we can bail out if the system is not set to 4K if (current->ResolutionWidthInRawPixels < 3840) { - Utils::Log("Warning: HDR may be unavailable when Xbox is not set to 4K resolution\n"); + MLOG(Utils::LogLevel::Warning, "Warning: HDR may be unavailable when Xbox is not set to 4K resolution\n"); // return false; } @@ -105,7 +107,7 @@ bool MoonlightClient::SetDisplayHDR(bool enabled, const SS_HDR_METADATA &sunshin hdrMetadata.MaxFrameAverageLightLevel = sunshineHdrMetadata.maxFrameAverageLightLevel; // log all available modes - Utils::Log("Supported display modes:\n"); + MLOG(Utils::LogLevel::Debug, "Supported display modes:\n"); for (auto mode : hdmi->GetSupportedDisplayModes()) { logDisplayMode(" ", mode); } @@ -125,7 +127,7 @@ bool MoonlightClient::SetDisplayHDR(bool enabled, const SS_HDR_METADATA &sunshin // A non-HDR display viewing an HDR stream will error out here with no mode found if (newMode == nullptr) { - Utils::Log("SetDisplayHDR(): HDR is unavailable, no suitable display mode found\n"); + MLOG(Utils::LogLevel::Warning, "SetDisplayHDR(): HDR is unavailable, no suitable display mode found\n"); return false; } @@ -137,7 +139,7 @@ bool MoonlightClient::SetDisplayHDR(bool enabled, const SS_HDR_METADATA &sunshin HdmiDisplayHdrOption hdrOption = HdmiDisplayHdrOption::None; if (enabled) { logDisplayMode("SetDisplayHDR(true): switching to HDR mode", newMode); - Utils::Logf("Sending HDR10 metadata: Min/MaxLuminance %.0f / %u\n", + MLOGF(Utils::LogLevel::Debug, "Sending HDR10 metadata: Min/MaxLuminance %.0f / %u\n", hdrMetadata.MinMasteringLuminance / 10000.0, hdrMetadata.MaxMasteringLuminance); hdrOption = HdmiDisplayHdrOption::Eotf2084; } else { @@ -164,7 +166,7 @@ bool MoonlightClient::SetDisplayHDR(bool enabled, const SS_HDR_METADATA &sunshin return true; } } else { - Utils::Log("SetDisplayHDR(): Error switching display mode.\n"); + MLOG(Utils::LogLevel::Error, "SetDisplayHDR(): Error switching display mode.\n"); } return false; @@ -174,7 +176,8 @@ MoonlightClient *connectedInstance; MoonlightClient::MoonlightClient() : m_isHDR(false), - m_isRGBFull(false) { + m_isRGBFull(false), + m_connectionTerminated(false) { HdmiDisplayInformation ^ hdmi = HdmiDisplayInformation::GetForCurrentView(); if (hdmi) { HdmiDisplayMode ^ current = hdmi->GetCurrentDisplayMode(); @@ -198,14 +201,10 @@ MoonlightClient::~MoonlightClient() { void MoonlightClient::StopApp() { gs_quit_app(&serverData); } -int MoonlightClient::StartStreaming(std::shared_ptr res, StreamConfiguration^ sConfig) { - g_connectionTerminated.store(false, std::memory_order_release); +int MoonlightClient::StartStreaming(std::shared_ptr res, StreamConfiguration ^ sConfig) { - //Thanks to https://stackoverflow.com/questions/11746146/how-to-convert-platformstring-to-char - std::wstring fooW(sConfig->hostname->Begin()); - std::string fooA(fooW.begin(), fooW.end()); - const char *charStr = fooA.c_str(); - this->Connect(charStr); + std::string fooA = Utils::PlatformStringToStdString(sConfig->hostname); + this->Connect(fooA.c_str()); STREAM_CONFIGURATION config; LiInitializeStreamConfiguration(&config); config.width = sConfig->width; @@ -254,7 +253,7 @@ int MoonlightClient::StartStreaming(std::shared_ptr res, St break; } - Utils::Logf("Requesting stream with clientRefreshRateX100=%d for %d FPS on %.2f Hz display\n", + MLOGF(Utils::LogLevel::Debug, "Requesting stream with clientRefreshRateX100=%d for %d FPS on %.2f Hz display\n", config.clientRefreshRateX100, config.fps, rr); } config.colorRange = this->IsRGBFull() ? COLOR_RANGE_FULL : COLOR_RANGE_LIMITED; @@ -280,27 +279,27 @@ int MoonlightClient::StartStreaming(std::shared_ptr res, St config.streamingRemotely = STREAM_CFG_AUTO; char message[2048]; sprintf(message, "Inserted App ID %d\n", sConfig->appID); - Utils::Log(message); + MLOG(Utils::LogLevel::Info, message); auto gamepads = Windows::Gaming::Input::Gamepad::Gamepads; - this->SetGamepadCount(gamepads->Size); // sets activeGamepadMask + + this->SetGamepadCount(std::max((UINT)1, gamepads->Size)); int a = gs_start_app(&serverData, &config, sConfig->appID, sConfig->enableSOPS, sConfig->playAudioOnPC, activeGamepadMask); if (a != 0) { char message[2048]; sprintf(message, "gs_startapp failed with status code %d\n", a); - Utils::Log(message); - + MLOG(Utils::LogLevel::Error, message); if (gs_error) { char errorMessage[2048]; sprintf(errorMessage, "%s\n", gs_error); - - Utils::Log(errorMessage); - this->OnFailed(0, a, errorMessage); + MLOG(Utils::LogLevel::Error, errorMessage); + if (this->OnFailed != nullptr) { + this->OnFailed(0, a, errorMessage); + } } return a; } - - // Sleep(10000); connectedInstance = this; + m_connectionTerminated.store(false); CONNECTION_LISTENER_CALLBACKS callbacks; LiInitializeConnectionCallbacks(&callbacks); callbacks.logMessage = log_message; @@ -314,21 +313,20 @@ int MoonlightClient::StartStreaming(std::shared_ptr res, St callbacks.rumble = connection_rumble; callbacks.rumbleTriggers = connection_trigger_rumble; - FFMpegDecoder::instance().CompleteInitialization(res, &config, sConfig->framePacing == "Immediate"); + const bool framePacingImmediate = (sConfig->framePacing != nullptr && sConfig->framePacing == "Immediate"); + FFMpegDecoder::instance().CompleteInitialization(res, &config, framePacingImmediate); DECODER_RENDERER_CALLBACKS rCallbacks = FFMpegDecoder::getDecoder(); AUDIO_RENDERER_CALLBACKS aCallbacks = AudioPlayer::getDecoder(); - + m_stageFailureReported.store(false); int k = LiStartConnection(&serverData.serverInfo, &config, &callbacks, &rCallbacks, &aCallbacks, NULL, 0, NULL, 0); - sprintf(message, "LiStartConnection %d\n", k); - Utils::Log(message); + MLOG(Utils::LogLevel::Debug, message); - if (k != 0) { + if (k != 0 && !m_stageFailureReported.load() && this->OnFailed != nullptr) { this->OnFailed(0, k, "Connection failed"); } - - return k; + return k; } void MoonlightClient::StopStreaming() { @@ -340,23 +338,13 @@ void log_message(const char *fmt, ...) { va_start(argp, fmt); char message[2048]; vsprintf_s(message, fmt, argp); - - // Append a single '\n' only if the string doesn't already end with one. - size_t len = strlen(message); - if (len == 0 || message[len - 1] != '\n') { - if (len + 1 < sizeof(message)) { - message[len] = '\n'; - message[len + 1] = '\0'; - } - } - - Utils::Log(message); + MLOG(Utils::LogLevel::Debug, message); } void connection_started() { char message[2048]; sprintf(message, "Connection Started\n"); - Utils::Log(message); + MLOG(Utils::LogLevel::Info, message); if (connectedInstance->OnCompleted != nullptr) { connectedInstance->OnCompleted(); } @@ -364,15 +352,14 @@ void connection_started() { void connection_status_update(int status) { char message[4096]; - auto stageName = LiGetFormattedStageName(status); - sprintf(message, "Stage %d: '%s' - Started\n", status, LiGetFormattedStageName(status)); - Utils::Log(message); + sprintf(message, "Stage %d started\n", status); + MLOG(Utils::LogLevel::Debug, message); } void connection_status_completed(int status) { char message[4096]; - sprintf(message, "Stage %d: '%s' - Completed\n", status, LiGetFormattedStageName(status)); - Utils::Log(message); + sprintf(message, "Stage %d completed\n", status); + MLOG(Utils::LogLevel::Debug, message); if (connectedInstance->OnStatusUpdate != nullptr) { connectedInstance->OnStatusUpdate(status); } @@ -387,37 +374,63 @@ void connection_set_hdr(bool enable) { void connection_terminated(int status) { char message[4096]; sprintf(message, "Connection terminated with status %d\n", status); - Utils::Log(message); - - g_connectionTerminated.store(true, std::memory_order_release); + MLOG(Utils::LogLevel::Info, message); + if (connectedInstance != nullptr) { + connectedInstance->SetConnectionTerminated(); + } } void stage_failed(int stage, int err) { char message[4096]; unsigned int portFlags = LiGetPortFlagsFromStage(stage); - // int portResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, portFlags); + int portResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, portFlags); char failingPorts[128]; LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts)); - sprintf(message, "Stage %d: '%s' - Failed with error: %d.\n", stage, LiGetFormattedStageName(stage), err, failingPorts); - Utils::Log(message); + sprintf(message, "%s failed with error %d.\n Check Firewall and Connections to port: %s\n", LiGetStageName(stage), err, failingPorts); + MLOG(Utils::LogLevel::Error, message); + connectedInstance->SetStageFailureReported(); if (connectedInstance->OnFailed != nullptr) { connectedInstance->OnFailed(stage, err, message); } } void connection_rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) { - if (connectedInstance->OnRumble != nullptr) { + if (connectedInstance != nullptr && connectedInstance->OnRumble != nullptr) { connectedInstance->OnRumble(controllerNumber, lowFreqMotor, highFreqMotor); + return; } + if (Windows::Gaming::Input::Gamepad::Gamepads->Size <= controllerNumber) return; + auto gp = Windows::Gaming::Input::Gamepad::Gamepads->GetAt(controllerNumber); + float normalizedHigh = highFreqMotor / (float)(256 * 256); + float normalizedLow = lowFreqMotor / (float)(256 * 256); + Windows::Gaming::Input::GamepadVibration v = gp->Vibration; + + v.LeftMotor = normalizedLow; + v.RightMotor = normalizedHigh; + gp->Vibration = v; } void connection_trigger_rumble(unsigned short controllerNumber, unsigned short leftTriggerMotor, unsigned short rightTriggerMotor) { - if (connectedInstance->OnTriggerRumble != nullptr) { + if (connectedInstance != nullptr && connectedInstance->OnTriggerRumble != nullptr) { connectedInstance->OnTriggerRumble(controllerNumber, leftTriggerMotor, rightTriggerMotor); + return; } + if (Windows::Gaming::Input::Gamepad::Gamepads->Size <= controllerNumber) return; + auto gp = Windows::Gaming::Input::Gamepad::Gamepads->GetAt(controllerNumber); + float normalizedLeft = leftTriggerMotor / (float)(256 * 256); + float normalizedRight = rightTriggerMotor / (float)(256 * 256); + Windows::Gaming::Input::GamepadVibration v = gp->Vibration; + v.LeftTrigger = normalizedLeft; + v.RightTrigger = normalizedRight; + gp->Vibration = v; } int MoonlightClient::Connect(const char *hostname) { + std::lock_guard lock(m_connectMutex); + if (this->hostname != NULL) { + free(this->hostname); + this->hostname = NULL; + } this->hostname = (char *)malloc(2048 * sizeof(char)); strcpy_s(this->hostname, 2048, hostname); if (strchr(this->hostname, ':') != 0) { @@ -432,19 +445,10 @@ int MoonlightClient::Connect(const char *hostname) { char folder[2048]; wcstombs_s(NULL, folder, folderString->Data(), 2047); - int status = 0; - status = gs_init(&serverData, this->hostname, port, folder, 3, true); + int status = gs_init(&serverData, this->hostname, port, folder, 3, true); return status; } -bool MoonlightClient::IsConnectionTerminated() { - return g_connectionTerminated.load(std::memory_order_acquire); -} - -void MoonlightClient::SetConnectionTerminated() { - g_connectionTerminated.store(true, std::memory_order_release); -} - bool MoonlightClient::IsHDR() { return m_isHDR; } @@ -453,6 +457,18 @@ bool MoonlightClient::IsRGBFull() { return m_isRGBFull; } +bool MoonlightClient::IsConnectionTerminated() { + return m_connectionTerminated.load(); +} + +void MoonlightClient::SetConnectionTerminated() { + m_connectionTerminated.store(true); +} + +void MoonlightClient::SetStageFailureReported() { + m_stageFailureReported.store(true); +} + bool MoonlightClient::IsPaired() { return serverData.paired; } @@ -468,7 +484,6 @@ int MoonlightClient::Pair() { if (serverData.paired) return -7; int status; if ((status = gs_pair(&serverData, &connectionPin[0])) != 0) { - // TODO: Handle gs WRONG STATE gs_unpair(&serverData); return status; } @@ -504,15 +519,23 @@ std::vector MoonlightClient::GetApplications(bool fetchAssets) { a->Name = s.Name; values.push_back(a); } - Platform::String ^ folderString = Windows::Storage::ApplicationData::Current->LocalFolder->Path; - folderString = folderString->Concat(folderString, "\\images\\"); + Platform::String ^ baseImages = Windows::Storage::ApplicationData::Current->LocalFolder->Path; + baseImages = Platform::String::Concat(baseImages, L"\\images\\"); + Platform::String ^ hostId = (serverData.uniqueId != nullptr) + ? Utils::StringFromChars(serverData.uniqueId) + : ref new Platform::String(L"unknown"); + Platform::String ^ folderString = Platform::String::Concat(baseImages, Platform::String::Concat(hostId, L"\\")); char folder[2048]; wcstombs_s(NULL, folder, folderString->Data(), 2047); + CreateDirectory(baseImages->Data(), NULL); CreateDirectory(folderString->Data(), NULL); if (fetchAssets) { Concurrency::create_task([folder, folderString, values, this]() { + std::unordered_set currentIds; + for (auto a : values) currentIds.insert(a->Id); + for (auto a : values) { - auto imgPath = folderString->Concat(folderString, a->Id + ".png"); + auto imgPath = Platform::String::Concat(folderString, a->Id + ".png"); // https://stackoverflow.com/a/6218957 DWORD dwAttrib = GetFileAttributes(imgPath->Data()); if (dwAttrib == INVALID_FILE_ATTRIBUTES) { @@ -520,12 +543,80 @@ std::vector MoonlightClient::GetApplications(bool fetchAssets) { } a->ImagePath = imgPath; } + + std::wstring searchPath(folderString->Data()); + searchPath += L"*.png"; + WIN32_FIND_DATAW findData; + HANDLE hFind = FindFirstFileW(searchPath.c_str(), &findData); + if (hFind != INVALID_HANDLE_VALUE) { + do { + std::wstring fname(findData.cFileName); + if (fname.size() > 4 && fname.substr(fname.size() - 4) == L".png") { + std::wstring numStr = fname.substr(0, fname.size() - 4); + wchar_t* end = nullptr; + long id = wcstol(numStr.c_str(), &end, 10); + if (end != numStr.c_str() && *end == L'\0' && currentIds.find((int)id) == currentIds.end()) { + std::wstring base(folderString->Data()); + DeleteFileW((base + fname).c_str()); + DeleteFileW((base + L"blur\\" + numStr + L"_bg.png").c_str()); + DeleteFileW((base + L"blur\\" + numStr + L"_glow.png").c_str()); + } + } + } while (FindNextFileW(hFind, &findData)); + FindClose(hFind); + } }); } return values; } +static bool hasGamepadReadingChanged(GamepadReading a, GamepadReading b) { + if (a.Buttons != b.Buttons) { + return true; + } + + short altX = (short)(a.LeftThumbstickX * 32767); + short altY = (short)(a.LeftThumbstickY * 32767); + short artX = (short)(a.RightThumbstickX * 32767); + short artY = (short)(a.RightThumbstickY * 32767); + short bltX = (short)(b.LeftThumbstickX * 32767); + short bltY = (short)(b.LeftThumbstickY * 32767); + short brtX = (short)(b.RightThumbstickX * 32767); + short brtY = (short)(b.RightThumbstickY * 32767); + if (altX != bltX || altY != bltY || artX != brtX || artY != brtY) { + return true; + } + + unsigned char alTrig = (unsigned char)(round(a.LeftTrigger * 255.0f)); + unsigned char arTrig = (unsigned char)(round(a.RightTrigger * 255.0f)); + unsigned char blTrig = (unsigned char)(round(b.LeftTrigger * 255.0f)); + unsigned char brTrig = (unsigned char)(round(b.RightTrigger * 255.0f)); + if (alTrig != blTrig || arTrig != brTrig) { + return true; + } + + return false; +} + +void MoonlightClient::SendGamepadReading(short controllerNumber, GamepadReading reading) { + int buttonFlags = 0; + GamepadButtons buttons[] = {GamepadButtons::A, GamepadButtons::B, GamepadButtons::X, GamepadButtons::Y, GamepadButtons::DPadLeft, GamepadButtons::DPadRight, GamepadButtons::DPadUp, GamepadButtons::DPadDown, GamepadButtons::LeftShoulder, GamepadButtons::RightShoulder, GamepadButtons::Menu, GamepadButtons::View, GamepadButtons::LeftThumbstick, GamepadButtons::RightThumbstick}; + int LiButtonFlags[] = {A_FLAG, B_FLAG, X_FLAG, Y_FLAG, LEFT_FLAG, RIGHT_FLAG, UP_FLAG, DOWN_FLAG, LB_FLAG, RB_FLAG, PLAY_FLAG, BACK_FLAG, LS_CLK_FLAG, RS_CLK_FLAG}; + for (int i = 0; i < 14; i++) { + if ((reading.Buttons & buttons[i]) == buttons[i]) { + buttonFlags |= LiButtonFlags[i]; + } + } + unsigned char leftTrigger = (unsigned char)(round(reading.LeftTrigger * 255.0f)); + unsigned char rightTrigger = (unsigned char)(round(reading.RightTrigger * 255.0f)); + + if (hasGamepadReadingChanged(reading, m_lastGamepadReading[controllerNumber])) { + LiSendMultiControllerEvent(controllerNumber, activeGamepadMask, buttonFlags, leftTrigger, rightTrigger, (short)(reading.LeftThumbstickX * 32767), (short)(reading.LeftThumbstickY * 32767), (short)(reading.RightThumbstickX * 32767), (short)(reading.RightThumbstickY * 32767)); + m_lastGamepadReading[controllerNumber] = reading; + } +} + void MoonlightClient::SendGuide(int controllerNumber, bool s) { LiSendMultiControllerEvent(controllerNumber, activeGamepadMask, s ? SPECIAL_FLAG : 0, 0, 0, 0, 0, 0, 0); } diff --git a/State/MoonlightClient.h b/State/MoonlightClient.h index a15836f4..1926cee2 100644 --- a/State/MoonlightClient.h +++ b/State/MoonlightClient.h @@ -1,68 +1,74 @@ #pragma once #include "pch.h" -#include #include "../Common/DeviceResources.h" +#include #include "State\MoonlightApp.h" +#include extern "C" { -#include -#include -#include + #include + #include + #include } -typedef void (*MoonlightErrorCallback)(const char *msg); +typedef void(*MoonlightErrorCallback)(const char *msg); namespace moonlight_xbox_dx { -class MoonlightClient { - public: - MoonlightClient(); - ~MoonlightClient(); - bool SetDisplayHDR(bool enabled, const SS_HDR_METADATA &sunshineHdrMetadata); - int StartStreaming(std::shared_ptr res, StreamConfiguration ^ config); - int Connect(const char *hostname); - bool IsConnectionTerminated(); - void SetConnectionTerminated(); - bool IsHDR(); - bool IsPaired(); - bool IsRGBFull(); - int Pair(); - char *GeneratePIN(); - std::vector GetApplications(bool fetchAssets = true); - void SendMousePosition(float x, float y); - void SendMousePressed(int button); - void SendMouseReleased(int button); - void SendScroll(float value); - void SendScrollH(float value); - void SetSoftwareEncoder(bool value); - void SetGamepadCount(short count); - int GetRunningAppID(); - void StopStreaming(); - void StopApp(); - void Unpair(); - void KeyDown(unsigned short v, char modifiers); - void KeyUp(unsigned short v, char modifiers); - void SendGuide(int controllerNumber, bool v); - Platform::String ^ GetInstanceID(); - Platform::String ^ GetComputerName(); - Platform::String ^ GetServerAddress(); - Platform::String ^ GetServerMacAddress(); - std::function OnStatusUpdate; - std::function OnCompleted; - std::function SetHDR; - std::function OnFailed; - std::function OnRumble; - std::function OnTriggerRumble; - - private: - SERVER_DATA serverData; - char *connectionPin = NULL; - char *hostname = NULL; - int port = 0; - bool useSoftwareEncoder = false; - bool m_isHDR; - bool m_isRGBFull; - uint16_t activeGamepadMask = 0; - Windows::Gaming::Input::GamepadReading m_lastGamepadReading[16]; -}; -} // namespace moonlight_xbox_dx + class MoonlightClient + { + public: + MoonlightClient(); + ~MoonlightClient(); + bool SetDisplayHDR(bool enabled, const SS_HDR_METADATA& sunshineHdrMetadata); + int StartStreaming(std::shared_ptr res, StreamConfiguration ^config); + int Connect(const char* hostname); + bool IsHDR(); + bool IsPaired(); + bool IsRGBFull(); + bool IsConnectionTerminated(); + void SetConnectionTerminated(); + void SetStageFailureReported(); + int Pair(); + char *GeneratePIN(); + std::vector GetApplications(bool fetchAssets = true); + void SendGamepadReading(short controllerNumber, Windows::Gaming::Input::GamepadReading reading); + void SendMousePosition(float x, float y); + void SendMousePressed(int button); + void SendMouseReleased(int button); + void SendScroll(float value); + void SendScrollH(float value); + void SetSoftwareEncoder(bool value); + void SetGamepadCount(short count); + int GetRunningAppID(); + void StopStreaming(); + void StopApp(); + void Unpair(); + void KeyDown(unsigned short v,char modifiers); + void KeyUp(unsigned short v, char modifiers); + void SendGuide(int controllerNumber, bool v); + Platform::String^ GetInstanceID(); + Platform::String^ GetComputerName(); + Platform::String^ GetServerAddress(); + Platform::String^ GetServerMacAddress(); + std::function OnStatusUpdate; + std::function OnCompleted; + std::function SetHDR; + std::function OnFailed; + std::function OnRumble; + std::function OnTriggerRumble; + private: + SERVER_DATA serverData; + char* connectionPin = NULL; + char* hostname = NULL; + int port = 0; + std::mutex m_connectMutex; + bool useSoftwareEncoder = false; + int activeGamepadMask = 0; + bool m_isHDR; + bool m_isRGBFull; + std::atomic m_connectionTerminated{ false }; + std::atomic m_stageFailureReported{ false }; + Windows::Gaming::Input::GamepadReading m_lastGamepadReading[16]; + }; +} diff --git a/State/MoonlightHost.cpp b/State/MoonlightHost.cpp index ad6bf2e6..3b1fcde3 100644 --- a/State/MoonlightHost.cpp +++ b/State/MoonlightHost.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include "MoonlightHost.h" #include "State\MoonlightClient.h" +#include namespace moonlight_xbox_dx { MoonlightHost::MoonlightHost(Platform::String ^host) { @@ -48,11 +49,35 @@ namespace moonlight_xbox_dx { Apps->Clear(); for (auto a : apps) { if (a->Id == CurrentlyRunningAppId) a->CurrentlyRunning = true; + a->IsFavorite = IsAppFavorite(a->Id); Apps->Append(a); } })); } + bool MoonlightHost::IsAppFavorite(int appId) { + return std::find(favoriteAppIds.begin(), favoriteAppIds.end(), appId) != favoriteAppIds.end(); + } + + void MoonlightHost::ToggleFavorite(int appId) { + auto it = std::find(favoriteAppIds.begin(), favoriteAppIds.end(), appId); + bool nowFavorite; + if (it != favoriteAppIds.end()) { + favoriteAppIds.erase(it); + nowFavorite = false; + } else { + favoriteAppIds.push_back(appId); + nowFavorite = true; + } + for (unsigned int i = 0; i < Apps->Size; ++i) { + auto a = Apps->GetAt(i); + if (a != nullptr && a->Id == appId) { + a->IsFavorite = nowFavorite; + break; + } + } + } + void MoonlightHost::UpdateAppRunningStates() { try { UpdateHostInfo(false); @@ -83,4 +108,31 @@ namespace moonlight_xbox_dx { PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName)); })); } + + void MoonlightHost::ScheduleStatusDebounce() + { + int version = ++m_debounceVersion; + auto delay = Windows::Foundation::TimeSpan{ 10000000LL }; + Windows::System::Threading::ThreadPoolTimer::CreateTimer( + ref new Windows::System::Threading::TimerElapsedHandler([this, version](Windows::System::Threading::ThreadPoolTimer^) { + if (m_debounceVersion == version) { + CommitDisplayedStatus(); + } + }), + delay + ); + } + + void MoonlightHost::CommitDisplayedStatus() + { + this->displayedConnected = this->connected; + this->displayedPaired = this->paired; + this->displayedLoading = this->loading; + this->displayedWolPolling = this->wolPolling; + OnPropertyChanged("StatusIsPolling"); + OnPropertyChanged("StatusIsConnectedAndPaired"); + OnPropertyChanged("StatusIsNotPaired"); + OnPropertyChanged("StatusIsDisconnected"); + OnPropertyChanged("StatusIsUnavailable"); + } } diff --git a/State/MoonlightHost.h b/State/MoonlightHost.h index 52c2e126..753e4e12 100644 --- a/State/MoonlightHost.h +++ b/State/MoonlightHost.h @@ -2,6 +2,7 @@ #include "pch.h" #include "State\MoonlightClient.h" #include "State\ScreenResolution.h" +#include "UI\Models\UIPersonalization.h" namespace moonlight_xbox_dx { [Windows::UI::Xaml::Data::Bindable] @@ -17,6 +18,13 @@ namespace moonlight_xbox_dx { bool connected; bool loading = true; bool wolPolling = false; + bool displayedConnected = false; + bool displayedPaired = false; + bool displayedLoading = true; + bool displayedWolPolling = false; + std::atomic m_debounceVersion{ 0 }; + void ScheduleStatusDebounce(); + void CommitDisplayedStatus(); bool playAudioOnPC = false; MoonlightClient* client; int currentlyRunningAppId; @@ -31,7 +39,10 @@ namespace moonlight_xbox_dx { bool enableSOPS = false; bool enableStats = false; bool enableGraphs = true; + UIPersonalization^ personalization; Windows::Foundation::Collections::IVector^ apps; + internal: + std::vector favoriteAppIds; public: //Thanks to https://phsucharee.wordpress.com/2013/06/19/data-binding-and-ccx-inotifypropertychanged/ virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; @@ -43,6 +54,8 @@ namespace moonlight_xbox_dx { void Unpair(); void UpdateApps(); void UpdateAppRunningStates(); + bool IsAppFavorite(int appId); + void ToggleFavorite(int appId); property Platform::String^ InstanceId { Platform::String^ get() { return this->instanceId; } @@ -96,6 +109,7 @@ namespace moonlight_xbox_dx { OnPropertyChanged("NotPaired"); OnPropertyChanged("Connected"); OnPropertyChanged("NotConnected"); + ScheduleStatusDebounce(); } } @@ -107,6 +121,7 @@ namespace moonlight_xbox_dx { OnPropertyChanged("Connected"); OnPropertyChanged("NotConnected"); OnPropertyChanged("NotPaired"); + ScheduleStatusDebounce(); } } @@ -120,6 +135,31 @@ namespace moonlight_xbox_dx { bool get() { return this->connected && !this->paired; } } + property bool StatusIsPolling + { + bool get() { return this->displayedWolPolling || this->displayedLoading; } + } + + property bool StatusIsConnectedAndPaired + { + bool get() { return this->displayedConnected && this->displayedPaired && !StatusIsPolling; } + } + + property bool StatusIsNotPaired + { + bool get() { return this->displayedConnected && !this->displayedPaired && !StatusIsPolling; } + } + + property bool StatusIsDisconnected + { + bool get() { return !this->displayedConnected && !StatusIsPolling; } + } + + property bool StatusIsUnavailable + { + bool get() { return !(this->displayedConnected && this->displayedPaired); } + } + property bool Loading { bool get() { return this->loading; } @@ -131,6 +171,7 @@ namespace moonlight_xbox_dx { OnPropertyChanged("NotConnected"); OnPropertyChanged("NotPaired"); OnPropertyChanged("Paired"); + ScheduleStatusDebounce(); } } @@ -146,6 +187,7 @@ namespace moonlight_xbox_dx { this->wolPolling = value; OnPropertyChanged("WolPolling"); OnPropertyChanged("WolPollingVisibility"); + ScheduleStatusDebounce(); } } @@ -284,5 +326,14 @@ namespace moonlight_xbox_dx { OnPropertyChanged("EnableGraphs"); } } + + property UIPersonalization^ Personalization + { + UIPersonalization^ get() { + if (this->personalization == nullptr) + this->personalization = ref new UIPersonalization(); + return this->personalization; + } + } }; } diff --git a/State/Stats.cpp b/State/Stats.cpp index c41027f4..e7fc00d7 100644 --- a/State/Stats.cpp +++ b/State/Stats.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "Stats.h" +#define MLOG_TAG_OVERRIDE "Stats" #include "Utils.hpp" #include "../Plot/ImGuiPlots.h" #include "../Streaming/FFMpegDecoder.h" @@ -290,7 +291,7 @@ void Stats::formatVideoStats(DX::StepTimer const& timer, VIDEO_STATS& stats, cha stats.totalFps, codecString); if (ret < 0 || (size_t)ret >= (length - offset)) { - Utils::Log("Error: stringifyVideoStats length overflow\n"); + MLOG(Utils::LogLevel::Error, "Error: stringifyVideoStats length overflow\n"); return; } @@ -313,7 +314,7 @@ void Stats::formatVideoStats(DX::StepTimer const& timer, VIDEO_STATS& stats, cha stats.renderedFps, Pacer::instance().getPacingImmediate() ? "immediate" : "display-locked"); if (ret < 0 || (size_t)ret >= (length - offset)) { - Utils::Log("Error: stringifyVideoStats length overflow\n"); + MLOG(Utils::LogLevel::Error, "Error: stringifyVideoStats length overflow\n"); return; } @@ -328,7 +329,7 @@ void Stats::formatVideoStats(DX::StepTimer const& timer, VIDEO_STATS& stats, cha (double)stats.maxHostProcessingLatency / 10, (double)stats.totalHostProcessingLatency / 10 / stats.framesWithHostProcessingLatency); if (ret < 0 || (size_t)ret >= (length - offset)) { - Utils::Log("Error: stringifyVideoStats length overflow\n"); + MLOG(Utils::LogLevel::Error, "Error: stringifyVideoStats length overflow\n"); return; } @@ -340,7 +341,7 @@ void Stats::formatVideoStats(DX::StepTimer const& timer, VIDEO_STATS& stats, cha length - offset, "Host processing latency min/max/avg: -/-/- ms\n"); if (ret < 0 || (size_t)ret >= (length - offset)) { - Utils::Log("Error: stringifyVideoStats length overflow\n"); + MLOG(Utils::LogLevel::Error, "Error: stringifyVideoStats length overflow\n"); return; } @@ -375,7 +376,7 @@ void Stats::formatVideoStats(DX::StepTimer const& timer, VIDEO_STATS& stats, cha stats.renderedFrames ? (double)stats.totalRenderTimeUs / 1000.0 / stats.renderedFrames : 0.0f, stats.renderedFrames ? (double)stats.totalPresentTimeUs / 1000.0 / stats.renderedFrames : 0.0f); if (ret < 0 || (size_t)ret >= (length - offset)) { - Utils::Log("Error: stringifyVideoStats length overflow\n"); + MLOG(Utils::LogLevel::Error, "Error: stringifyVideoStats length overflow\n"); return; } @@ -395,7 +396,7 @@ void Stats::formatVideoStats(DX::StepTimer const& timer, VIDEO_STATS& stats, cha (double)stats.totalPreWaitTimeUs / 1000.0 / stats.renderedFrames, (double)stats.totalRenderTimeUs / 1000.0 / stats.renderedFrames); if (ret < 0 || (size_t)ret >= (length - offset)) { - Utils::Log("Error: stringifyVideoStats length overflow\n"); + MLOG(Utils::LogLevel::Error, "Error: stringifyVideoStats length overflow\n"); return; } diff --git a/State/StreamConfiguration.h b/State/StreamConfiguration.h index f0baa3c6..84b4f0c7 100644 --- a/State/StreamConfiguration.h +++ b/State/StreamConfiguration.h @@ -21,6 +21,7 @@ namespace moonlight_xbox_dx property bool enableSOPS; property bool enableStats; property bool enableGraphs; + property Windows::UI::Xaml::Media::Imaging::BitmapImage^ backgroundImage; }; moonlight_xbox_dx::StreamConfiguration^ GetStreamConfig(); diff --git a/Streaming/AudioPlayer.cpp b/Streaming/AudioPlayer.cpp index a759591f..57905209 100644 --- a/Streaming/AudioPlayer.cpp +++ b/Streaming/AudioPlayer.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#define MLOG_TAG_OVERRIDE "AudioPlayer" #include #include #include @@ -12,7 +13,7 @@ static void AudioPlayer_LogCallback(void* pUserData, ma_uint32 level, const char* pMessage) { if (level <= MA_LOG_LEVEL_INFO) { - moonlight_xbox_dx::Utils::Logf("[miniaudio] %s", pMessage); + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Info, "[miniaudio] %s", pMessage); } } @@ -65,14 +66,14 @@ namespace moonlight_xbox_dx { ma_uint32 len = frameCount; ma_result res = ma_pcm_rb_acquire_read(&rb, &len, &buffer); if (res != MA_SUCCESS) { - Utils::Log("Failed to read audio data\n"); + MLOG(Utils::LogLevel::Error, "Failed to read audio data\n"); return; } if (len > 0) { memcpy(pOutput, buffer, len * ma_pcm_rb_get_bpf(&rb)); res = ma_pcm_rb_commit_read(&rb, len); if (res != MA_SUCCESS && res != MA_AT_END) { - Utils::Log("Failed to read audio data to shared buffer\n"); + MLOG(Utils::LogLevel::Error, "Failed to read audio data to shared buffer\n"); return; } } @@ -100,24 +101,24 @@ namespace moonlight_xbox_dx { config.pLog = &log; if (ma_context_init(NULL, 1, &config, &context) != MA_SUCCESS) { - Utils::Log("Failed to create miniaudio context.\n"); + MLOG(Utils::LogLevel::Error, "Failed to create miniaudio context.\n"); return -3; } if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { - Utils::Log("Failed to open playback device.\n"); + MLOG(Utils::LogLevel::Error, "Failed to open playback device.\n"); return -3; } ma_result r = ma_pcm_rb_init(ma_format_f32, opusConfig->channelCount, opusConfig->samplesPerFrame * 10, NULL, NULL, &rb); if (r != MA_SUCCESS) { - Utils::Log("Failed to create shared buffer\n"); + MLOG(Utils::LogLevel::Error, "Failed to create shared buffer\n"); } return r; } void AudioPlayer::Cleanup() { - Utils::Log("Audio Cleanup\n"); + MLOG(Utils::LogLevel::Info, "Audio Cleanup\n"); if (decoder != NULL) opus_multistream_decoder_destroy(decoder); ma_pcm_rb_uninit(&rb); ma_device_uninit(&device); @@ -130,24 +131,24 @@ namespace moonlight_xbox_dx { ma_uint32 bufferLen = (ma_uint32)this->samplePerFrame; ma_result r = ma_pcm_rb_acquire_write(&rb, &bufferLen, &buffer); if (r != MA_SUCCESS) { - Utils::Log("Failed to acquire shared buffer\n"); + MLOG(Utils::LogLevel::Error, "Failed to acquire shared buffer\n"); return -1; } if (bufferLen < (ma_uint32)this->samplePerFrame || buffer == nullptr) { - Utils::Logf("Audio buffer overflow (%d > %d)\n", bufferLen, this->samplePerFrame); + MLOGF(Utils::LogLevel::Warning, "Audio buffer overflow (%d > %d)\n", bufferLen, this->samplePerFrame); return -1; } int decodeLen = opus_multistream_decode_float(decoder, (unsigned char*)sampleData, sampleLength, (float *)buffer, this->samplePerFrame, 0); if (decodeLen < 0) { - Utils::Logf("opus_multistream_decode_float failed: %d\n", decodeLen); + MLOGF(Utils::LogLevel::Error, "opus_multistream_decode_float failed: %d\n", decodeLen); return -1; } if (decodeLen > 0) { r = ma_pcm_rb_commit_write(&rb, (ma_uint32)decodeLen); if (r != MA_SUCCESS && r != MA_AT_END) { - Utils::Log("Failed to write to shared buffer\n"); + MLOG(Utils::LogLevel::Error, "Failed to write to shared buffer\n"); return -1; } } @@ -156,7 +157,7 @@ namespace moonlight_xbox_dx { void AudioPlayer::Start() { if (ma_device_start(&device) != MA_SUCCESS) { - Utils::Log("Failed to start playback device.\n"); + MLOG(Utils::LogLevel::Error, "Failed to start playback device.\n"); ma_device_uninit(&device); } @@ -164,7 +165,7 @@ namespace moonlight_xbox_dx { void AudioPlayer::Stop() { if (ma_device_stop(&device) != MA_SUCCESS) { - Utils::Log("Failed to start playback device.\n"); + MLOG(Utils::LogLevel::Error, "Failed to start playback device.\n"); ma_device_uninit(&device); } } diff --git a/Streaming/FFmpegDecoder.cpp b/Streaming/FFmpegDecoder.cpp index 1a483fb8..823f54c2 100644 --- a/Streaming/FFmpegDecoder.cpp +++ b/Streaming/FFmpegDecoder.cpp @@ -1,4 +1,5 @@ #include "pch.h" +#define MLOG_TAG_OVERRIDE "FFmpegDecoder" #include "FFMpegDecoder.h" #include "../Plot/ImGuiPlots.h" #include "StatsRenderer.h" @@ -82,7 +83,7 @@ namespace moonlight_xbox_dx { bool shouldPrefixThisMessage = printPrefix != 0; av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix); - Utils::Logf(shouldPrefixThisMessage ? "[ffmpeg] %s" : "%s", lineBuffer); + MLOGF(Utils::LogLevel::Error, shouldPrefixThisMessage ? "[ffmpeg] %s" : "%s", lineBuffer); } void FFMpegDecoder::CompleteInitialization(const std::shared_ptr& res, STREAM_CONFIGURATION *config, bool framePacingImmediate) { @@ -113,21 +114,21 @@ namespace moonlight_xbox_dx { if (videoFormat & VIDEO_FORMAT_MASK_H264) { decoder = avcodec_find_decoder(AV_CODEC_ID_H264); - Utils::Log("Using H264\n"); + MLOG(Utils::LogLevel::Info, "Using H264\n"); } else if (videoFormat & VIDEO_FORMAT_MASK_H265) { decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC); - Utils::Log("Using HEVC\n"); + MLOG(Utils::LogLevel::Info, "Using HEVC\n"); } if (decoder == NULL) { - Utils::Log("Couldn't find decoder\n"); + MLOG(Utils::LogLevel::Error, "Couldn't find decoder\n"); return -1; } decoder_ctx = avcodec_alloc_context3(decoder); if (decoder_ctx == NULL) { - Utils::Log("Couldn't allocate context\n"); + MLOG(Utils::LogLevel::Error, "Couldn't allocate context\n"); return -1; } decoder_ctx->opaque = this; @@ -142,7 +143,7 @@ namespace moonlight_xbox_dx { d3d11va_device_ctx->lock_ctx = this; int err2; if ((err2 = av_hwdevice_ctx_init(hw_device_ctx)) < 0) { - Utils::Logf("Failed to create specified DirectX Video device: %d\n", err2); + MLOGF(Utils::LogLevel::Error, "Failed to create specified DirectX Video device: %d\n", err2); Cleanup(); return err2; } @@ -160,16 +161,16 @@ namespace moonlight_xbox_dx { if (err < 0) { char msg[2048]; sprintf(msg, "Failed to create FFMpeg Codec: %d\n", err); - Utils::Log(msg); + MLOG(Utils::LogLevel::Error, msg); return err; } if (decoder_ctx->pix_fmt != AV_PIX_FMT_D3D11) { - Utils::Log("Warning: decoder did not select AV_PIX_FMT_D3D11\n"); + MLOG(Utils::LogLevel::Warning, "Warning: decoder did not select AV_PIX_FMT_D3D11\n"); } if (!ensure_buf_size(&ffmpeg_buffer, &ffmpeg_buffer_size, INITIAL_DECODER_BUFFER_SIZE + AV_INPUT_BUFFER_PADDING_SIZE)) { - Utils::Log("Couldn't allocate initial ffmpeg_buffer\n"); + MLOG(Utils::LogLevel::Error, "Couldn't allocate initial ffmpeg_buffer\n"); Cleanup(); return -1; } @@ -188,7 +189,12 @@ namespace moonlight_xbox_dx { Pacer::instance().deinit(); - Utils::Log("FFMpegDecoder::Cleanup\n"); + + if (m_deviceResources) { + m_deviceResources->GetD3DDeviceContext()->AddRef(); + } + + MLOG(Utils::LogLevel::Info, "FFMpegDecoder::Cleanup\n"); } static inline int frame_attach_userdata(AVFrame *frame, int64_t decodeEndQpc) { @@ -218,7 +224,7 @@ namespace moonlight_xbox_dx { if (m_StreamEpochQpc == 0) m_StreamEpochQpc = decodeStart.QuadPart; if (!ensure_buf_size(&ffmpeg_buffer, &ffmpeg_buffer_size, decodeUnit->fullLength + AV_INPUT_BUFFER_PADDING_SIZE)) { - Utils::Logf("Couldn't realloc ffmpeg_buffer\n"); + MLOGF(Utils::LogLevel::Error, "Couldn't realloc ffmpeg_buffer\n"); return DR_NEED_IDR; } @@ -261,7 +267,7 @@ namespace moonlight_xbox_dx { if (err < 0) { char ffmpegError[1024]; av_strerror(err, ffmpegError, 1024); - Utils::Logf("avcodec_send_packet failed: %s\n", ffmpegError); + MLOGF(Utils::LogLevel::Error, "avcodec_send_packet failed: %s\n", ffmpegError); return DR_NEED_IDR; } @@ -275,7 +281,7 @@ namespace moonlight_xbox_dx { else if (err < 0) { char ffmpegError[1024]; av_strerror(err, ffmpegError, sizeof(ffmpegError)); - Utils::Logf("avcodec_receive_frame failed: %s\n", ffmpegError); + MLOGF(Utils::LogLevel::Error, "avcodec_receive_frame failed: %s\n", ffmpegError); av_frame_free(&frame); return DR_NEED_IDR; } diff --git a/Streaming/FrameQueue.cpp b/Streaming/FrameQueue.cpp index 75c417b1..e7fdeb2b 100644 --- a/Streaming/FrameQueue.cpp +++ b/Streaming/FrameQueue.cpp @@ -1,6 +1,7 @@ // clang-format off #include "pch.h" // clang-format on +#define MLOG_TAG_OVERRIDE "FrameQueue" #include "FrameQueue.h" #include "Utils.hpp" #include @@ -44,13 +45,13 @@ void FrameQueue::setPaused(bool p) { void FrameQueue::start() { setPaused(false); - Utils::Logf("FrameQueue started\n"); + MLOGF(Utils::LogLevel::Info, "FrameQueue started\n"); } void FrameQueue::stop() { setPaused(true); clear(); - Utils::Logf("FrameQueue stopped\n"); + MLOGF(Utils::LogLevel::Info, "FrameQueue stopped\n"); } std::size_t FrameQueue::count() const { diff --git a/Streaming/LogRenderer.cpp b/Streaming/LogRenderer.cpp index 578d0826..4e97ad5c 100644 --- a/Streaming/LogRenderer.cpp +++ b/Streaming/LogRenderer.cpp @@ -36,7 +36,17 @@ void LogRenderer::Update(DX::StepTimer const& timer) Utils::logMutex.lock(); std::vector lines = Utils::GetLogLines(); for (std::wstring line : lines) { - m_console->Write(line.c_str()); + switch (Utils::ParseLogLevelFromLine(line)) { + case Utils::LogLevel::Error: + m_console->Write(line.c_str(), Colors::Red); + break; + case Utils::LogLevel::Warning: + m_console->Write(line.c_str(), Colors::Orange); + break; + default: + m_console->Write(line.c_str()); + break; + } } Utils::logMutex.unlock(); diff --git a/Streaming/Pacer.cpp b/Streaming/Pacer.cpp index cc3382ed..20fa848a 100644 --- a/Streaming/Pacer.cpp +++ b/Streaming/Pacer.cpp @@ -1,6 +1,7 @@ // clang-format off #include "pch.h" // clang-format on +#define MLOG_TAG_OVERRIDE "Pacer" #include "Pacer.h" #include #include @@ -75,7 +76,7 @@ void Pacer::deinit() { m_CurrentFrame = nullptr; } - Utils::Logf("Pacer: deinit\n"); + MLOGF(Utils::LogLevel::Info, "Pacer: deinit\n"); } void Pacer::init(const std::shared_ptr &res, int streamFps, double refreshRate, bool framePacingImmediate) { @@ -87,7 +88,7 @@ void Pacer::init(const std::shared_ptr &res, int streamFps, m_FrameCadence.init(m_RefreshRate > 0.0 ? m_RefreshRate : 60.0, static_cast(streamFps)); - Utils::Logf("Frame Pacer init: mode %s, streamFps %d, refreshRate %.2f\n", + MLOGF(Utils::LogLevel::Info, "Frame Pacer init: mode %s, streamFps %d, refreshRate %.2f\n", framePacingImmediate ? "immediate" : "display-locked", m_StreamFps, m_RefreshRate); m_vhsum = 0; @@ -119,10 +120,10 @@ void Pacer::setPacingImmediate(bool framePacingImmediate) { void Pacer::vsyncHardware() { if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL)) { - Utils::Logf("Failed to set vsyncHardware priority: %d\n", GetLastError()); + MLOGF(Utils::LogLevel::Warning, "Failed to set vsyncHardware priority: %d\n", GetLastError()); } - Utils::Logf("vsyncHardware stats thread started, qpcFreq=%lld ticksPerMs=%lld\n", + MLOGF(Utils::LogLevel::Info, "vsyncHardware stats thread started, qpcFreq=%lld ticksPerMs=%lld\n", QpcFreq(), MsToQpc(1.0)); while (!stopping()) { @@ -133,7 +134,7 @@ void Pacer::vsyncHardware() { updateFrameStats(); } - Utils::Logf("vsyncHardware stats thread stopped\n"); + MLOGF(Utils::LogLevel::Info, "vsyncHardware stats thread stopped\n"); } // based on mpv's d3d11_get_vsync() diff --git a/Streaming/VideoRenderer.cpp b/Streaming/VideoRenderer.cpp index 1f6c1e8d..9bf61d75 100644 --- a/Streaming/VideoRenderer.cpp +++ b/Streaming/VideoRenderer.cpp @@ -1,11 +1,10 @@ #include "pch.h" +#define MLOG_TAG_OVERRIDE "VideoRenderer" #include "VideoRenderer.h" #include #include "..\Common\DirectXHelper.h" #include #include -#include "..\Common\ModalDialog.xaml.h" - #include #include @@ -173,7 +172,7 @@ bool VideoRenderer::Render(AVFrame *frame) { UINT colorSpaceSupport = 0; if (colorspace && SUCCEEDED(m_deviceResources->GetSwapChain()->CheckColorSpaceSupport(colorspace, &colorSpaceSupport)) && (colorSpaceSupport & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT)) { DX::ThrowIfFailed(m_deviceResources->GetSwapChain()->SetColorSpace1(colorspace)); - Utils::Logf("Colorspace changed to %s\n", + MLOGF(Utils::LogLevel::Info, "Colorspace changed to %s\n", colorspace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 ? "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020" : "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709"); @@ -187,7 +186,7 @@ bool VideoRenderer::Render(AVFrame *frame) { void VideoRenderer::CreateDeviceDependentResources() { - Utils::Log("Started with creation of DXView\n"); + MLOG(Utils::LogLevel::Info, "Started with creation of DXView\n"); // Vertex shader { @@ -282,7 +281,7 @@ void VideoRenderer::CreateDeviceDependentResources() int status = this->client->StartStreaming(devRes, cfg); if (status != 0) { - Utils::Logf("StartStreaming failed with status %d\n", status); + MLOGF(Utils::LogLevel::Error, "StartStreaming failed with status %d\n", status); m_loadingSuccessful.store(false, std::memory_order_release); m_loadingComplete.store(true, std::memory_order_release); return; @@ -290,7 +289,7 @@ void VideoRenderer::CreateDeviceDependentResources() m_loadingSuccessful.store(true, std::memory_order_release); m_loadingComplete.store(true, std::memory_order_release); - Utils::Log("Loading Complete!\n"); + MLOG(Utils::LogLevel::Info, "Loading Complete!\n"); })); } @@ -400,7 +399,7 @@ void VideoRenderer::setupVertexBuffer(D3D11_TEXTURE2D_DESC frameDesc) float uMax = m_TextureWidth > 0 ? (float)m_DecoderParams.width / m_TextureWidth : 1.0f; float vMax = m_TextureHeight > 0 ? (float)m_DecoderParams.height / m_TextureHeight : 1.0f; - Utils::Logf("Setup vertex shader params: uMax %f, vMax %f\n", uMax, vMax); + MLOGF(Utils::LogLevel::Debug, "Setup vertex shader params: uMax %f, vMax %f\n", uMax, vMax); VERTEX verts[] = { @@ -532,7 +531,7 @@ void VideoRenderer::getFramePremultipliedCscConstants(const AVFrame* frame, std: cscMatrix[i] *= uvScale; } - Utils::Logf("Shader config: %s %d-bit %s, (AVColorSpace %d, AVChromaLocation %d)\n", + MLOGF(Utils::LogLevel::Debug, "Shader config: %s %d-bit %s, (AVColorSpace %d, AVChromaLocation %d)\n", colorspace == COLORSPACE_REC_601 ? "Rec. 601" : colorspace == COLORSPACE_REC_709 ? "Rec. 709" : "Rec. 2020", @@ -657,7 +656,7 @@ void VideoRenderer::bindColorConversion(AVFrame* frame, D3D11_TEXTURE2D_DESC fra D3D11_SUBRESOURCE_DATA constData = {}; constData.pSysMem = &constBuf; - Utils::Logf("Setup pixel shader params: chromaOffset[0] %f, chromaOffset[1] %f, chromaUVMax[0] %f, chromaUVMax[1] %f\n", + MLOGF(Utils::LogLevel::Debug, "Setup pixel shader params: chromaOffset[0] %f, chromaOffset[1] %f, chromaUVMax[0] %f, chromaUVMax[1] %f\n", constBuf.chromaOffset[0], constBuf.chromaOffset[1], constBuf.chromaUVMax[0], constBuf.chromaUVMax[1]); diff --git a/Streaming/moonlight_xbox_dxMain.cpp b/Streaming/moonlight_xbox_dxMain.cpp index 5d366b8b..d960c9bc 100644 --- a/Streaming/moonlight_xbox_dxMain.cpp +++ b/Streaming/moonlight_xbox_dxMain.cpp @@ -1,13 +1,15 @@ -#include "moonlight_xbox_dxMain.h" +#define MLOG_TAG_OVERRIDE "Main" +#include "moonlight_xbox_dxMain.h" #include "pch.h" -#include -#include -#include +#include +#include +#include #include #include "../Plot/ImGuiPlots.h" #include "Common\DirectXHelper.h" #include "State\GamepadState.h" #include "Utils.hpp" +#include #include @@ -20,8 +22,9 @@ using namespace Windows::Gaming::Input; using namespace Windows::System::Threading; using namespace Windows::UI::ViewManagement::Core; +#include + extern "C" { -#include #include } @@ -46,57 +49,52 @@ moonlight_xbox_dxMain::moonlight_xbox_dxMain(const std::shared_ptr>(); *showErrorDialog = [self, msgCopy, showLogsDialog, appName]() { - auto dialog1 = ref new Windows::UI::Xaml::Controls::ContentDialog(); - dialog1->Title = L"Failed to start " + appName; - dialog1->Content = Utils::StringFromStdString(msgCopy); - dialog1->PrimaryButtonText = L"OK"; - dialog1->SecondaryButtonText = L"Show Logs"; - - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog1)).then([self, showLogsDialog](concurrency::task t) { - auto result = t.get(); - if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) { + auto dialog = ref new ::moonlight_xbox_dx::StreamErrorDialog(); + dialog->Configure( + L"Failed to start " + appName, + Utils::StringFromStdString(msgCopy), + L"\xE783", + L"OK", L"\xE8FB", + L"Show Logs", L"\xF0B5", + ref new Windows::UI::Xaml::RoutedEventHandler([self](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { self->StopRenderLoop(); self->ExitStreamPage(); - } else if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Secondary) { + }), + ref new Windows::UI::Xaml::RoutedEventHandler([showLogsDialog](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { (*showLogsDialog)(); - } - }); + }) + ); + concurrency::create_task(dialog->ShowAsync()); }; *showLogsDialog = [self, showErrorDialog]() { - auto dialog2 = ref new Windows::UI::Xaml::Controls::ContentDialog(); - - std::wstring m_text = L""; + std::wstring logText = L""; std::vector lines = Utils::GetLogLines(); - for (int i = 0; i < (int)lines.size(); i++) { - // Get only the last 8 lines - // More than that cannot be fully viewed on the screen at the current scaling if ((int)lines.size() - i <= 8) { - m_text += lines[i]; + logText += lines[i]; } } - Utils::showLogs = true; - dialog2->MaxWidth = 600; - dialog2->Title = "Logs"; - dialog2->Content = ref new Platform::String(m_text.c_str()); - dialog2->PrimaryButtonText = L"OK"; - dialog2->SecondaryButtonText = L"Show Error"; - - concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog2)).then([self, showErrorDialog](concurrency::task t) { - auto result = t.get(); - if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) { + auto dialog = ref new ::moonlight_xbox_dx::StreamErrorDialog(); + dialog->Configure( + L"Logs", + ref new Platform::String(logText.c_str()), + L"\xF0B5", + L"OK", L"\xE8FB", + L"Show Error", L"\xE783", + ref new Windows::UI::Xaml::RoutedEventHandler([self](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { self->StopRenderLoop(); self->ExitStreamPage(); - } else if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Secondary) { + }), + ref new Windows::UI::Xaml::RoutedEventHandler([showErrorDialog](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { (*showErrorDialog)(); - } - }); + }) + ); + concurrency::create_task(dialog->ShowAsync()); }; - // Start by showing the error dialog (*showErrorDialog)(); })); }); @@ -126,8 +124,24 @@ moonlight_xbox_dxMain::moonlight_xbox_dxMain(const std::shared_ptrm_sceneRenderer && this->m_sceneRenderer->IsLoadingSuccessful()) { DISPATCH_UI(([streamPage]() { Sleep(500); - streamPage->m_progressRing->IsActive = false; - streamPage->m_progressView->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + using namespace Windows::UI::Xaml::Media::Animation; + auto anim = ref new DoubleAnimation(); + anim->From = ref new Platform::Box(1.0); + anim->To = ref new Platform::Box(0.0); + anim->Duration = Windows::UI::Xaml::Duration(Windows::Foundation::TimeSpan{ 10000000LL }); + auto sb = ref new Storyboard(); + sb->Children->Append(anim); + Storyboard::SetTarget(anim, streamPage->m_progressView); + Storyboard::SetTargetProperty(anim, "(UIElement.Opacity)"); + Platform::WeakReference weakPage(streamPage); + sb->Completed += ref new Windows::Foundation::EventHandler( + [weakPage](Platform::Object^, Platform::Object^) { + auto page = weakPage.Resolve(); + if (page == nullptr) return; + page->m_progressView->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + page->m_progressView->Opacity = 1.0; + }); + sb->Begin(); })); } }); @@ -202,8 +216,10 @@ moonlight_xbox_dxMain::moonlight_xbox_dxMain(const std::shared_ptrRegisterDeviceNotify(nullptr); + + m_deviceResources->GetD3DDeviceContext()->ClearState(); + m_deviceResources->GetD3DDeviceContext()->Flush(); } void moonlight_xbox_dxMain::CreateDeviceDependentResources() { @@ -218,14 +234,14 @@ void moonlight_xbox_dxMain::CreateWindowSizeDependentResources() { void moonlight_xbox_dxMain::StartRenderLoop() { // If the animation render loop is already running then do not start another thread. - if (m_renderLoopWorker != nullptr && m_renderLoopWorker->Status == AsyncStatus::Started) { + if (m_renderLoopWorker != nullptr && m_renderLoopWorker->Status == Windows::Foundation::AsyncStatus::Started) { return; } // Create a task that will be run on a background thread. auto workItemHandler = ref new WorkItemHandler([this](IAsyncAction ^ action) { if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL)) { - Utils::Logf("Failed to set render thread priority: %d\n", GetLastError()); + MLOGF(Utils::LogLevel::Warning, "Failed to set render thread priority: %d\n", GetLastError()); } int64_t t0 = 0, t1 = 0, t2 = 0, t3 = 0; @@ -237,7 +253,7 @@ void moonlight_xbox_dxMain::StartRenderLoop() { double ewmaRenderMs = 3.0; // Initial guess for render cost // Calculate the updated frame and render once per vertical blanking interval. - while (action->Status == AsyncStatus::Started && !moonlightClient->IsConnectionTerminated()) { + while (action->Status == Windows::Foundation::AsyncStatus::Started && !moonlightClient->IsConnectionTerminated()) { // Get overall deadline we must hit by the Present for this frame int64_t deadline = Pacer::instance().getNextVBlankQpc(&t0); @@ -326,12 +342,12 @@ void moonlight_xbox_dxMain::StartRenderLoop() { StopRenderLoop(); // also stops input Disconnect(); - DISPATCH_UI([this]() { - ExitStreamPage(); - }); + DISPATCH_UI([this]() { + ExitStreamPage(); + }); }); m_renderLoopWorker = ThreadPool::RunAsync(workItemHandler, WorkItemPriority::High, WorkItemOptions::TimeSliced); - if (m_inputLoopWorker != nullptr && m_inputLoopWorker->Status == AsyncStatus::Started) { + if (m_inputLoopWorker != nullptr && m_inputLoopWorker->Status == Windows::Foundation::AsyncStatus::Started) { return; } auto inputItemHandler = ref new WorkItemHandler([this](IAsyncAction ^ action) { @@ -339,7 +355,7 @@ void moonlight_xbox_dxMain::StartRenderLoop() { const int64_t pollIntervalQpc = MsToQpc(1000.0 / pollingHz); int64_t lastProcessInput = 0; - while (action->Status == AsyncStatus::Started) { + while (action->Status == Windows::Foundation::AsyncStatus::Started) { int64_t now = QpcNow(); if (now - lastProcessInput >= pollIntervalQpc) { lastProcessInput = now; @@ -408,27 +424,51 @@ void moonlight_xbox_dxMain::ProcessInput() { auto result = state.GetComboResult(50); // hold buttons for a short time for View + Menu combo if (result.comboTriggered) { - DISPATCH_UI(([this]() { - Windows::UI::Xaml::Controls::Flyout::ShowAttachedFlyout(m_streamPage->m_flyoutButton); + bool newVisible = !insideMenu; + DISPATCH_UI(([this, newVisible]() { + m_streamPage->SetStreamMenuVisible(newVisible); })); // send an empty controller packet, otherwise Sunshine may see View being kept held down, // triggering the "Home/Guide Button Emulation Timeout" to send a Guide button press after a few seconds. SendGamepadReadingForState(state, EmptyReading()); - // disable future input until the flyout is closed - insideFlyout = true; + insideMenu = newVisible; continue; } - if (insideFlyout) { + if (insideMenu) { + if (PressedEdge(result.maskedReading, state.previousReading, GamepadButtons::B)) { + DISPATCH_UI(([this]() { + m_streamPage->SetStreamMenuVisible(false); + })); + insideMenu = false; + state.buttonSuppressMask = static_cast( + static_cast(state.buttonSuppressMask) | static_cast(GamepadButtons::B)); + SendGamepadReadingForState(state, EmptyReading()); + } state.reading = EmptyReading(); state.previousReading = EmptyReading(); continue; } + if (result.menuLongPressTriggered) { + DISPATCH_UI(([this]() { + bool newMode = !m_streamPage->MouseMode; + m_streamPage->SetMouseMode(newMode); + ShowToast(newMode ? L"Mouse Mode: On" : L"Mouse Mode: Off"); + })); + SendGamepadReadingForState(state, EmptyReading()); + continue; + } + // GetComboResult() will have masked off our combo buttons if they are pending auto reading = result.maskedReading; + + state.buttonSuppressMask = static_cast( + static_cast(state.buttonSuppressMask) & static_cast(reading.Buttons)); + reading.Buttons = GamepadState::clearButtons(reading.Buttons, state.buttonSuppressMask); + auto prevReading = state.previousReading; // If mouse mode is enabled the gamepad acts as a mouse, instead we pass the raw events to the host @@ -541,7 +581,9 @@ void moonlight_xbox_dxMain::ProcessInput() { })); keyboardMode = true; } else { - CoreInputView::GetForCurrentView()->TryShow(CoreInputViewKind::Keyboard); + m_streamPage->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, ref new Windows::UI::Core::DispatchedHandler([this]() { + CoreInputView::GetForCurrentView()->TryShow(CoreInputViewKind::Keyboard); + })); } } // Scroll @@ -582,7 +624,7 @@ void moonlight_xbox_dxMain::DumpGamepads() { // list all controllers with their connected status for (int i = 0; i < MAX_GAMEPADS; ++i) { if (m_GamepadState[i].controller != nullptr) { - Utils::Logf(" Gamepad #%d: hostId %d\n", m_GamepadState[i].localId, m_GamepadState[i].hostId); + MLOGF(Utils::LogLevel::Debug, " Gamepad #%d: hostId %d\n", m_GamepadState[i].localId, m_GamepadState[i].hostId); } } } @@ -607,7 +649,7 @@ void moonlight_xbox_dxMain::RefreshGamepads() { if (!state.didSendArrival) { SendGamepadArrival(state); state.didSendArrival = true; - Utils::Logf("RefreshGamepads: sent arrival packet for Gamepad #%d\n", localId); + MLOGF(Utils::LogLevel::Info, "RefreshGamepads: sent arrival packet for Gamepad #%d\n", localId); } found = true; break; @@ -629,7 +671,7 @@ void moonlight_xbox_dxMain::RefreshGamepads() { state.previousReading = EmptyReading(); SendGamepadArrival(state); state.didSendArrival = true; - Utils::Logf("RefreshGamepads: added new Gamepad #%d in host slot %d\n", state.localId, state.hostId); + MLOGF(Utils::LogLevel::Info, "RefreshGamepads: added new Gamepad #%d in host slot %d\n", state.localId, state.hostId); break; } } @@ -644,7 +686,7 @@ void moonlight_xbox_dxMain::RefreshGamepads() { uint16_t activeMaskMinus = MakeActiveMask(); activeMaskMinus &= ~(1 << state.hostId); LiSendMultiControllerEvent(state.hostId, activeMaskMinus, 0, 0, 0, 0, 0, 0, 0); - Utils::Logf("RefreshGamepads: removed Gamepad #%d from host slot %d\n", state.localId, state.hostId); + MLOGF(Utils::LogLevel::Info, "RefreshGamepads: removed Gamepad #%d from host slot %d\n", state.localId, state.hostId); state.Reset(); } } @@ -659,7 +701,7 @@ void moonlight_xbox_dxMain::SendGamepadArrival(GamepadState &state) { uint32_t capabilities = LI_CCAP_ANALOG_TRIGGERS | LI_CCAP_RUMBLE | LI_CCAP_TRIGGER_RUMBLE; int rc = LiSendControllerArrivalEvent(state.hostId, MakeActiveMask(), type, supportedButtonFlags, capabilities); if (rc != 0) { - Utils::Logf("LiSendControllerArrivalEvent error: %d\n", rc); + MLOGF(Utils::LogLevel::Error, "LiSendControllerArrivalEvent error: %d\n", rc); } } @@ -734,13 +776,14 @@ void moonlight_xbox_dxMain::OnDeviceRestored() { CreateWindowSizeDependentResources(); } -void moonlight_xbox_dxMain::SetFlyoutOpened(bool value) { - insideFlyout = value; +void moonlight_xbox_dxMain::SetMenuVisible(bool value) { + insideMenu = value; } void moonlight_xbox_dxMain::Disconnect() { moonlightClient->StopStreaming(); m_sceneRenderer->Stop(); + moonlightClient->SetDisplayHDR(false, SS_HDR_METADATA{}); } void moonlight_xbox_dxMain::CloseApp() { @@ -752,7 +795,7 @@ void moonlight_xbox_dxMain::ExitStreamPage() { bool reachedAppPage = false; try { - auto rootFrame = dynamic_cast(Windows::UI::Xaml::Window::Current->Content); + auto rootFrame = App::GetRootFrame(); if (!rootFrame) return; auto current = dynamic_cast(rootFrame->Content); @@ -763,7 +806,7 @@ void moonlight_xbox_dxMain::ExitStreamPage() { try { rootFrame->GoBack(); } catch (...) { - Utils::Log("ExitStreamPage: Failed to GoBack()\n"); + MLOG(Utils::LogLevel::Error, "ExitStreamPage: Failed to GoBack()\n"); } if (!reachedAppPage) { @@ -772,14 +815,16 @@ void moonlight_xbox_dxMain::ExitStreamPage() { if (!reachedAppPage) { try { - rootFrame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSelectorPage::typeid)); + auto slideBack = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + slideBack->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromLeft; + rootFrame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSelectorPage::typeid), nullptr, slideBack); } catch (...) { rootFrame->Content = nullptr; - Utils::Log("ExitStreamPage: Failed to return to HostSelectorPage\n"); + MLOG(Utils::LogLevel::Error, "ExitStreamPage: Failed to return to HostSelectorPage\n"); } } } catch (...) { - Utils::Log("ExitStreamPage: An error occurred\n"); + MLOG(Utils::LogLevel::Error, "ExitStreamPage: An error occurred\n"); } } diff --git a/Streaming/moonlight_xbox_dxMain.h b/Streaming/moonlight_xbox_dxMain.h index 280c8593..fb23a327 100644 --- a/Streaming/moonlight_xbox_dxMain.h +++ b/Streaming/moonlight_xbox_dxMain.h @@ -5,7 +5,7 @@ #include "Streaming\VideoRenderer.h" #include "Streaming\LogRenderer.h" #include "Streaming\StatsRenderer.h" -#include "Pages\StreamPage.xaml.h" +#include "UI\Pages\StreamPage.xaml.h" // Xbox supports 8 controllers, this ought to be enough for anyone. #define MAX_GAMEPADS 8 @@ -23,7 +23,7 @@ namespace moonlight_xbox_dx void TrackingUpdate(float positionX) { m_pointerLocationX = positionX; } void StartRenderLoop(); void StopRenderLoop(); - void SetFlyoutOpened(bool value); + void SetMenuVisible(bool value); Concurrency::critical_section& GetCriticalSection() { return m_criticalSection; } bool keyboardMode = false; void OnKeyDown(unsigned short virtualKey, char modifiers); @@ -66,7 +66,7 @@ namespace moonlight_xbox_dx // Track current input pointer position. float m_pointerLocationX; - bool insideFlyout = false; + bool insideMenu = false; StreamPage^ m_streamPage; // Gamepad handling diff --git a/UI/Backgrounds/BackgroundRegistry.h b/UI/Backgrounds/BackgroundRegistry.h new file mode 100644 index 00000000..7fa02248 --- /dev/null +++ b/UI/Backgrounds/BackgroundRegistry.h @@ -0,0 +1,22 @@ +#pragma once + +namespace moonlight_xbox_dx { + + struct BackgroundEntry { + const wchar_t* key; + const wchar_t* displayName; + }; + + static const BackgroundEntry kBackgrounds[] = { + { L"streaks", L"Neon Streaks" }, + { L"particles", L"Floating Particles" }, + { L"spheres", L"Bouncing Bubbles" }, + { L"blobs", L"Morphing Blobs" }, + { L"swipereveal", L"Swipe Reveal" }, + { L"globegrid", L"Globe Grid" }, + { L"orbs", L"Dancing Orbs" }, + }; + + static const int kBackgroundCount = sizeof(kBackgrounds) / sizeof(kBackgrounds[0]); + +} diff --git a/UI/Backgrounds/BackgroundSettingsHelpers.h b/UI/Backgrounds/BackgroundSettingsHelpers.h new file mode 100644 index 00000000..34ec41f9 --- /dev/null +++ b/UI/Backgrounds/BackgroundSettingsHelpers.h @@ -0,0 +1,29 @@ +#pragma once +#include + +namespace moonlight_xbox_dx { + +inline Platform::String^ BgColorToHex(Windows::UI::Color c) +{ + wchar_t buf[7]; + swprintf_s(buf, L"%02X%02X%02X", (unsigned)c.R, (unsigned)c.G, (unsigned)c.B); + return ref new Platform::String(buf); +} + +inline Windows::UI::Color BgHexToColor(Platform::String^ s, Windows::UI::Color fallback) +{ + if (s == nullptr || s->Length() != 6) return fallback; + const wchar_t* p = s->Data(); + wchar_t buf[7]; wcsncpy_s(buf, p, 6); buf[6] = L'\0'; + wchar_t* end = nullptr; + unsigned long v = wcstoul(buf, &end, 16); + if (end != buf + 6) return fallback; + Windows::UI::Color c; + c.A = 255; + c.R = (uint8_t)((v >> 16) & 0xFF); + c.G = (uint8_t)((v >> 8) & 0xFF); + c.B = (uint8_t)( v & 0xFF); + return c; +} + +} diff --git a/UI/Backgrounds/Blobs/BlobsBackground.xaml b/UI/Backgrounds/Blobs/BlobsBackground.xaml new file mode 100644 index 00000000..85a76126 --- /dev/null +++ b/UI/Backgrounds/Blobs/BlobsBackground.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Blobs/BlobsBackground.xaml.cpp b/UI/Backgrounds/Blobs/BlobsBackground.xaml.cpp new file mode 100644 index 00000000..6aba4bf6 --- /dev/null +++ b/UI/Backgrounds/Blobs/BlobsBackground.xaml.cpp @@ -0,0 +1,255 @@ +#include "pch.h" +#include "UI\Backgrounds\Blobs\BlobsBackground.xaml.h" +#include + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Shapes; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; + +static const float kPi = 3.14159265358979f; + +static bool ParseHex6(Platform::String^ s, Color& out) +{ + if (s == nullptr || s->Length() != 6) return false; + const wchar_t* p = s->Data(); + wchar_t buf[7]; wcsncpy_s(buf, p, 6); buf[6] = L'\0'; + wchar_t* end = nullptr; + unsigned long v = wcstoul(buf, &end, 16); + if (end != buf + 6) return false; + out.R = (uint8_t)((v >> 16) & 0xFF); + out.G = (uint8_t)((v >> 8) & 0xFF); + out.B = (uint8_t)( v & 0xFF); + return true; +} + +void BlobsBackground::LoadPalette() +{ + using namespace Windows::Storage; + Platform::String^ scheme = L"crimson"; + auto ls = ApplicationData::Current->LocalSettings->Values; + if (ls->HasKey("blobs.scheme")) + scheme = safe_cast(ls->Lookup("blobs.scheme")); + + Color schemes[5][4] = { + {{170,210,10,55},{170,185,0,185},{170,100,0,200},{255,8,0,16}}, + {{170,0,180,200},{170,0,100,220},{170,0,210,150},{255,0,6,20}}, + {{170,0,200,120},{170,60,190,255},{170,140,40,230},{255,2,4,10}}, + {{170,255,70,0},{170,210,20,20},{170,255,140,20},{255,12,3,0}}, + {{170,170,40,230},{170,255,60,180},{170,40,90,255},{255,5,0,18}}, + }; + + int idx = -1; + if (scheme->Equals(L"crimson")) idx = 0; + else if (scheme->Equals(L"ocean")) idx = 1; + else if (scheme->Equals(L"aurora")) idx = 2; + else if (scheme->Equals(L"ember")) idx = 3; + else if (scheme->Equals(L"nebula")) idx = 4; + + if (idx >= 0) { + for (int i = 0; i < 4; ++i) m_palette[i] = schemes[idx][i]; + return; + } + + static const wchar_t* kKeys[] = { + L"blobs.custom.0", L"blobs.custom.1", L"blobs.custom.2", L"blobs.custom.3" + }; + Color defaults[4] = { + {170,210,10,55},{170,185,0,185},{170,100,0,200},{255,8,0,16} + }; + for (int i = 0; i < 4; ++i) { + m_palette[i] = defaults[i]; + auto key = ref new Platform::String(kKeys[i]); + if (ls->HasKey(key)) { + Color c = m_palette[i]; + if (ParseHex6(safe_cast(ls->Lookup(key)), c)) { + c.A = (i < 3) ? 170 : 255; + m_palette[i] = c; + } + } + } +} + +BlobsBackground::BlobsBackground() +{ + m_rng = std::mt19937(std::random_device{}()); + InitializeComponent(); + this->Loaded += ref new RoutedEventHandler(this, &BlobsBackground::OnLoaded); + + BlobCanvas->Background = ref new SolidColorBrush(ColorHelper::FromArgb(255, 8, 0, 16)); + + TimeSpan interval; + interval.Duration = 16 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = interval; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void BlobsBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + if (m_canvasW > 0 && m_canvasH > 0 && !m_ready) { + m_ready = true; + InitBlobs(); + } +} + +void BlobsBackground::InitBlobs() +{ + LoadPalette(); + BlobCanvas->Background = ref new SolidColorBrush(m_palette[3]); + m_blobs.clear(); + BlobCanvas->Children->Clear(); + + std::uniform_real_distribution distR(170.0f, 240.0f); + std::uniform_real_distribution distWobR(70.0f, 160.0f); + std::uniform_real_distribution distWobA(0.2f, 0.5f); + std::uniform_real_distribution distPhase(0.0f, kPi * 2.0f); + std::uniform_real_distribution distFreqR(0.03f, 0.08f); + std::uniform_real_distribution distFreqA(0.02f, 0.06f); + std::uniform_real_distribution distTime(0.0f, 200.0f); + std::uniform_real_distribution distSpeed(0.2f, 0.5f); + + float positions[kBlobCount][2] = { + { m_canvasW * 0.0f, m_canvasH * 0.7f }, + { m_canvasW * 0.5f, m_canvasH * 0.2f }, + { m_canvasW * 0.9f, m_canvasH * 0.7f }, + }; + + for (int i = 0; i < kBlobCount; i++) { + BlobData b{}; + b.cx = positions[i][0]; + b.cy = positions[i][1]; + b.baseRadius = distR(m_rng); + b.time = distTime(m_rng); + b.timeSpeed = distSpeed(m_rng); + + for (int j = 0; j < kBlobPts; j++) { + b.rAmp[j] = distWobR(m_rng); + b.rPhase[j] = distPhase(m_rng); + b.rFreq[j] = distFreqR(m_rng); + b.aAmp[j] = distWobA(m_rng); + b.aFreq[j] = distFreqA(m_rng); + b.aPhase[j] = distPhase(m_rng); + } + + auto s0 = ref new BezierSegment(); + auto s1 = ref new BezierSegment(); + auto s2 = ref new BezierSegment(); + auto s3 = ref new BezierSegment(); + auto s4 = ref new BezierSegment(); + auto s5 = ref new BezierSegment(); + b.s0 = s0; b.s1 = s1; b.s2 = s2; + b.s3 = s3; b.s4 = s4; b.s5 = s5; + + auto figure = ref new PathFigure(); + figure->IsClosed = true; + figure->IsFilled = true; + figure->Segments->Append(s0); + figure->Segments->Append(s1); + figure->Segments->Append(s2); + figure->Segments->Append(s3); + figure->Segments->Append(s4); + figure->Segments->Append(s5); + b.figure = figure; + + auto geom = ref new PathGeometry(); + geom->Figures->Append(figure); + + auto path = ref new Path(); + path->Data = geom; + path->Fill = ref new SolidColorBrush(m_palette[i]); + + BlobCanvas->Children->Append(path); + m_blobs.push_back(b); + UpdateBlob(m_blobs.back()); + } +} + +void BlobsBackground::UpdateBlob(BlobData& b) +{ + b.time += b.timeSpeed; + + float px[kBlobPts], py[kBlobPts]; + for (int i = 0; i < kBlobPts; i++) { + float baseAngle = i * (2.0f * kPi / kBlobPts); + float angle = baseAngle + b.aAmp[i] * sinf(b.time * b.aFreq[i] + b.aPhase[i]); + float r = b.baseRadius + b.rAmp[i] * sinf(b.time * b.rFreq[i] + b.rPhase[i]); + px[i] = b.cx + r * cosf(angle); + py[i] = b.cy + r * sinf(angle); + } + + auto cp1x = [&](int i) { return px[i] + (px[(i+1)%kBlobPts] - px[(i-1+kBlobPts)%kBlobPts]) / 6.0f; }; + auto cp1y = [&](int i) { return py[i] + (py[(i+1)%kBlobPts] - py[(i-1+kBlobPts)%kBlobPts]) / 6.0f; }; + auto cp2x = [&](int i) { return px[(i+1)%kBlobPts] - (px[(i+2)%kBlobPts] - px[i]) / 6.0f; }; + auto cp2y = [&](int i) { return py[(i+1)%kBlobPts] - (py[(i+2)%kBlobPts] - py[i]) / 6.0f; }; + + b.figure->StartPoint = Point(px[0], py[0]); + + b.s0->Point1 = Point(cp1x(0), cp1y(0)); b.s0->Point2 = Point(cp2x(0), cp2y(0)); b.s0->Point3 = Point(px[1], py[1]); + b.s1->Point1 = Point(cp1x(1), cp1y(1)); b.s1->Point2 = Point(cp2x(1), cp2y(1)); b.s1->Point3 = Point(px[2], py[2]); + b.s2->Point1 = Point(cp1x(2), cp1y(2)); b.s2->Point2 = Point(cp2x(2), cp2y(2)); b.s2->Point3 = Point(px[3], py[3]); + b.s3->Point1 = Point(cp1x(3), cp1y(3)); b.s3->Point2 = Point(cp2x(3), cp2y(3)); b.s3->Point3 = Point(px[4], py[4]); + b.s4->Point1 = Point(cp1x(4), cp1y(4)); b.s4->Point2 = Point(cp2x(4), cp2y(4)); b.s4->Point3 = Point(px[5], py[5]); + b.s5->Point1 = Point(cp1x(5), cp1y(5)); b.s5->Point2 = Point(cp2x(5), cp2y(5)); b.s5->Point3 = Point(px[0], py[0]); +} + +void BlobsBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_ready) return; + for (auto& b : m_blobs) UpdateBlob(b); +} + +void BlobsBackground::OnLoaded(Object^ sender, RoutedEventArgs^ e) +{ + auto anim = ref new DoubleAnimation(); + anim->From = 0.0; + anim->To = 1.0; + TimeSpan ts; + ts.Duration = 5000000LL; + anim->Duration = Windows::UI::Xaml::Duration(ts); + BlobCanvas->Opacity = 0.0; + auto sb = ref new Storyboard(); + Storyboard::SetTarget(anim, BlobCanvas); + Storyboard::SetTargetProperty(anim, "Opacity"); + sb->Children->Append(anim); + sb->Begin(); +} + +void BlobsBackground::ReloadColors() +{ + if (!m_ready) return; + LoadPalette(); + for (int i = 0; i < kBlobCount; ++i) { + safe_cast(BlobCanvas->Children->GetAt(i))->Fill = + ref new SolidColorBrush(m_palette[i]); + } + BlobCanvas->Background = ref new SolidColorBrush(m_palette[3]); +} + +void BlobsBackground::StartAnimations() +{ + if (m_timer != nullptr) m_timer->Start(); +} + +void BlobsBackground::StopAnimations() +{ + if (m_timer != nullptr) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } +} diff --git a/UI/Backgrounds/Blobs/BlobsBackground.xaml.h b/UI/Backgrounds/Blobs/BlobsBackground.xaml.h new file mode 100644 index 00000000..f8fb69fc --- /dev/null +++ b/UI/Backgrounds/Blobs/BlobsBackground.xaml.h @@ -0,0 +1,57 @@ +#pragma once +#include "UI\Backgrounds\Blobs\BlobsBackground.g.h" +#include +#include + +namespace moonlight_xbox_dx { + +static const int kBlobCount = 3; +static const int kBlobPts = 6; + +struct BlobData { + float cx, cy; + float baseRadius; + float time; + float timeSpeed; + + float rAmp[kBlobPts]; + float rPhase[kBlobPts]; + float rFreq[kBlobPts]; + float aAmp[kBlobPts]; + float aFreq[kBlobPts]; + float aPhase[kBlobPts]; + + Windows::UI::Xaml::Media::PathFigure^ figure; + Windows::UI::Xaml::Media::BezierSegment^ s0; + Windows::UI::Xaml::Media::BezierSegment^ s1; + Windows::UI::Xaml::Media::BezierSegment^ s2; + Windows::UI::Xaml::Media::BezierSegment^ s3; + Windows::UI::Xaml::Media::BezierSegment^ s4; + Windows::UI::Xaml::Media::BezierSegment^ s5; +}; + +public ref class BlobsBackground sealed { +public: + BlobsBackground(); + void StartAnimations(); + void StopAnimations(); + void ReloadColors(); +private: + Windows::UI::Color m_palette[4]; + void LoadPalette(); + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_blobs; + float m_canvasW = 0; + float m_canvasH = 0; + bool m_ready = false; + std::mt19937 m_rng; + + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void InitBlobs(); + void UpdateBlob(BlobData& b); +}; + +} diff --git a/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml b/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml new file mode 100644 index 00000000..c7236506 --- /dev/null +++ b/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml.cpp b/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml.cpp new file mode 100644 index 00000000..c498207b --- /dev/null +++ b/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml.cpp @@ -0,0 +1,166 @@ +#include "pch.h" +#include "BlobsSettingsControl.xaml.h" +#include "UI\Backgrounds\BackgroundSettingsHelpers.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +static Windows::UI::Color kCrimsonUI[] = { {170,210,10,55},{170,185,0,185},{170,100,0,200},{255,8,0,16} }; +static Windows::UI::Color kOceanUI[] = { {170,0,180,200},{170,0,100,220},{170,0,210,150},{255,0,6,20} }; +static Windows::UI::Color kAuroraUI[] = { {170,0,200,120},{170,60,190,255},{170,140,40,230},{255,2,4,10} }; +static Windows::UI::Color kEmberUI[] = { {170,255,70,0},{170,210,20,20},{170,255,140,20},{255,12,3,0} }; +static Windows::UI::Color kNebulaUI[] = { {170,170,40,230},{170,255,60,180},{170,40,90,255},{255,5,0,18} }; + +static Windows::UI::Color kColorSwatches[] = { + {255,210, 10, 55}, {255,185, 0,185}, {255,100, 0,200}, + {255, 0,180,200}, {255, 0,210,150}, {255, 0,100,220}, + {255, 0,200,120}, {255,140, 40,230}, {255,255, 70, 0}, + {255,255,140, 20}, {255,255, 60,180}, {255,255,255,255}, +}; +static const int kColorSwatchCount = 12; + +static Windows::UI::Color kBgSwatches[] = { + {255, 8, 0, 16}, {255, 0, 6, 20}, {255, 2, 4, 10}, + {255, 12, 3, 0}, {255, 5, 0, 18}, {255, 0, 0, 0}, + {255, 5, 8, 0}, {255, 10, 0, 5}, +}; +static const int kBgSwatchCount = 8; + +static const wchar_t* kCustomKeys[] = { + L"blobs.custom.0", L"blobs.custom.1", L"blobs.custom.2", L"blobs.custom.3" +}; + +BlobsSettingsControl::BlobsSettingsControl() +{ + InitializeComponent(); +} + +void BlobsSettingsControl::Initialize(DynamicBackgroundHost^ host) +{ + m_host = host; + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + Platform::String^ scheme = L"crimson"; + if (ls->HasKey("blobs.scheme")) + scheme = safe_cast(ls->Lookup("blobs.scheme")); + + struct { const wchar_t* key; const wchar_t* label; } schemes[] = { + { L"crimson", L"Crimson (Default)" }, + { L"ocean", L"Ocean" }, + { L"aurora", L"Aurora" }, + { L"ember", L"Ember" }, + { L"nebula", L"Nebula" }, + { L"custom", L"Custom" }, + }; + + int selectedIdx = 0; + for (int i = 0; i < 6; ++i) { + auto item = ref new ComboBoxItem(); + item->Content = ref new Platform::String(schemes[i].label); + item->DataContext = ref new Platform::String(schemes[i].key); + SchemeSelector->Items->Append(item); + if (scheme->Equals(ref new Platform::String(schemes[i].key))) selectedIdx = i; + } + SchemeSelector->SelectedIndex = selectedIdx; + UpdateCustomPanelVisibility(); + + Windows::UI::Color* defaults = kCrimsonUI; + if (scheme->Equals(L"ocean")) defaults = kOceanUI; + else if (scheme->Equals(L"aurora")) defaults = kAuroraUI; + else if (scheme->Equals(L"ember")) defaults = kEmberUI; + else if (scheme->Equals(L"nebula")) defaults = kNebulaUI; + + SwatchPicker^ blobPickers[] = { Color0, Color1, Color2 }; + for (int i = 0; i < 3; ++i) { + if (blobPickers[i] == nullptr) continue; + blobPickers[i]->SetSwatches(kColorSwatches, kColorSwatchCount); + Windows::UI::Color c = defaults[i]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[i]); + if (ls->HasKey(k)) c = BgHexToColor(safe_cast(ls->Lookup(k)), c); + } + blobPickers[i]->SelectColor(c, false); + } + + if (Color3 != nullptr) { + Color3->SetSwatches(kBgSwatches, kBgSwatchCount); + Windows::UI::Color c3 = defaults[3]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[3]); + if (ls->HasKey(k)) c3 = BgHexToColor(safe_cast(ls->Lookup(k)), c3); + } + Color3->SelectColor(c3, false); + } + + Color0->ColorChanged += ref new SwatchColorChangedHandler(this, &BlobsSettingsControl::Color0_ColorChanged); + Color1->ColorChanged += ref new SwatchColorChangedHandler(this, &BlobsSettingsControl::Color1_ColorChanged); + Color2->ColorChanged += ref new SwatchColorChangedHandler(this, &BlobsSettingsControl::Color2_ColorChanged); + Color3->ColorChanged += ref new SwatchColorChangedHandler(this, &BlobsSettingsControl::Color3_ColorChanged); + + m_initialized = true; +} + +void BlobsSettingsControl::UpdateCustomPanelVisibility() +{ + if (CustomPanel == nullptr || SchemeSelector == nullptr) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + bool isCustom = (item != nullptr && item->DataContext != nullptr && + item->DataContext->ToString()->Equals(L"custom")); + CustomPanel->Visibility = isCustom ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; +} + +void BlobsSettingsControl::SchemeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + if (item == nullptr) return; + auto key = item->DataContext->ToString(); + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("blobs.scheme", key); + + if (!key->Equals(L"custom")) { + Windows::UI::Color* defaults = kCrimsonUI; + if (key->Equals(L"ocean")) defaults = kOceanUI; + else if (key->Equals(L"aurora")) defaults = kAuroraUI; + else if (key->Equals(L"ember")) defaults = kEmberUI; + else if (key->Equals(L"nebula")) defaults = kNebulaUI; + SwatchPicker^ pickers[] = { Color0, Color1, Color2, Color3 }; + for (int i = 0; i < 4; ++i) { + if (pickers[i] != nullptr) pickers[i]->SelectColor(defaults[i], false); + } + } + + UpdateCustomPanelVisibility(); + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +static void SaveColor(int slot, Windows::UI::Color color) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[slot]), BgColorToHex(color)); +} + +void BlobsSettingsControl::Color0_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(0, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void BlobsSettingsControl::Color1_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(1, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void BlobsSettingsControl::Color2_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(2, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void BlobsSettingsControl::Color3_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(3, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } + +void BlobsSettingsControl::ResetButton_Click(Platform::Object^, RoutedEventArgs^) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Remove("blobs.scheme"); + for (int i = 0; i < 4; ++i) + ls->Remove(ref new Platform::String(kCustomKeys[i])); + + SchemeSelector->SelectedIndex = 0; + SwatchPicker^ pickers[] = { Color0, Color1, Color2, Color3 }; + for (int i = 0; i < 4; ++i) { + if (pickers[i] != nullptr) pickers[i]->SelectColor(kCrimsonUI[i], false); + } + CustomPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} diff --git a/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml.h b/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml.h new file mode 100644 index 00000000..d3c7fc7f --- /dev/null +++ b/UI/Backgrounds/Blobs/BlobsSettingsControl.xaml.h @@ -0,0 +1,25 @@ +#pragma once +#include "UI\Backgrounds\Blobs\BlobsSettingsControl.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +namespace moonlight_xbox_dx { + +public ref class BlobsSettingsControl sealed { +public: + BlobsSettingsControl(); + void Initialize(DynamicBackgroundHost^ host); +private: + DynamicBackgroundHost^ m_host; + bool m_initialized = false; + + void SchemeSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void Color0_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color1_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color2_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color3_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void ResetButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateCustomPanelVisibility(); +}; + +} diff --git a/UI/Backgrounds/DynamicBackgroundHost.xaml b/UI/Backgrounds/DynamicBackgroundHost.xaml new file mode 100644 index 00000000..d023ae19 --- /dev/null +++ b/UI/Backgrounds/DynamicBackgroundHost.xaml @@ -0,0 +1,20 @@ + + + + + + diff --git a/UI/Backgrounds/DynamicBackgroundHost.xaml.cpp b/UI/Backgrounds/DynamicBackgroundHost.xaml.cpp new file mode 100644 index 00000000..3524bc01 --- /dev/null +++ b/UI/Backgrounds/DynamicBackgroundHost.xaml.cpp @@ -0,0 +1,205 @@ +#include "pch.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Backgrounds\BackgroundRegistry.h" +#include "UI\Backgrounds\Particles\ParticleBackground.xaml.h" +#include "UI\Backgrounds\Spheres\SpheresBackground.xaml.h" +#include "UI\Backgrounds\Streaks\StreaksBackground.xaml.h" +#include "UI\Backgrounds\Blobs\BlobsBackground.xaml.h" +#include "UI\Backgrounds\SwipeReveal\SwipeRevealBackground.xaml.h" +#include "UI\Backgrounds\GlobeGrid\GlobeGridBackground.xaml.h" +#include "UI\Backgrounds\Orbs\OrbsBackground.xaml.h" + +using namespace moonlight_xbox_dx; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Media::Animation; +using namespace Windows::Storage; + +static void TryStartAnimations(UIElement^ el) +{ + if (auto p = dynamic_cast(el)) { p->StartAnimations(); return; } + if (auto s = dynamic_cast(el)) { s->StartAnimations(); return; } + if (auto k = dynamic_cast(el)) { k->StartAnimations(); return; } + if (auto l = dynamic_cast(el)) { l->StartAnimations(); return; } + if (auto sr = dynamic_cast(el)) { sr->StartAnimations(); return; } + if (auto gg = dynamic_cast(el)) { gg->StartAnimations(); return; } + if (auto ob = dynamic_cast(el)) { ob->StartAnimations(); return; } +} + +static void TryStopAnimations(UIElement^ el) +{ + if (auto p = dynamic_cast(el)) { p->StopAnimations(); return; } + if (auto s = dynamic_cast(el)) { s->StopAnimations(); return; } + if (auto k = dynamic_cast(el)) { k->StopAnimations(); return; } + if (auto l = dynamic_cast(el)) { l->StopAnimations(); return; } + if (auto sr = dynamic_cast(el)) { sr->StopAnimations(); return; } + if (auto gg = dynamic_cast(el)) { gg->StopAnimations(); return; } + if (auto ob = dynamic_cast(el)) { ob->StopAnimations(); return; } +} + +static UIElement^ CreateBackground(String^ key) +{ + if (key != nullptr) { + if (key->Equals(ref new String(L"particles"))) return ref new ParticleBackground(); + if (key->Equals(ref new String(L"spheres"))) return ref new SpheresBackground(); + if (key->Equals(ref new String(L"streaks"))) return ref new StreaksBackground(); + if (key->Equals(ref new String(L"blobs"))) return ref new BlobsBackground(); + if (key->Equals(ref new String(L"swipereveal"))) return ref new SwipeRevealBackground(); + if (key->Equals(ref new String(L"globegrid"))) return ref new GlobeGridBackground(); + if (key->Equals(ref new String(L"orbs"))) return ref new OrbsBackground(); + } + return ref new StreaksBackground(); +} + +DynamicBackgroundHost::DynamicBackgroundHost() +{ + InitializeComponent(); + this->Loaded += ref new RoutedEventHandler(this, &DynamicBackgroundHost::OnLoaded); +} + +void DynamicBackgroundHost::OnLoaded(Object^ sender, RoutedEventArgs^ e) +{ + Refresh(); +} + +void DynamicBackgroundHost::Refresh() +{ + try { + auto localSettings = ApplicationData::Current->LocalSettings->Values; + String^ key = L"streaks"; + if (localSettings->HasKey("background")) { + key = safe_cast(localSettings->Lookup("background")); + } + + m_discardedBg = nullptr; + + if (m_incomingKey != nullptr && m_incomingKey->Equals(key)) return; + if (m_incomingKey == nullptr && m_currentKey != nullptr && m_currentKey->Equals(key)) return; + + if (m_fadeStoryboard != nullptr) { + try { m_fadeStoryboard->Stop(); } catch (...) {} + m_fadeStoryboard = nullptr; + auto incomingEl = dynamic_cast(FadePresenter->Content); + if (incomingEl != nullptr) { + m_discardedBg = incomingEl; + TryStopAnimations(incomingEl); + } + FadePresenter->Content = nullptr; + FadePresenter->Opacity = 0.0; + m_incomingKey = nullptr; + } + + auto newBg = CreateBackground(key); + if (auto sr = dynamic_cast(newBg)) { sr->SetHosts(m_hosts); } + FadePresenter->Content = newBg; + FadePresenter->Opacity = 0.0; + m_incomingKey = key; + + auto anim = ref new DoubleAnimation(); + anim->From = 0.0; + anim->To = 1.0; + TimeSpan ts; ts.Duration = 3000000LL; + anim->Duration = DurationHelper::FromTimeSpan(ts); + + auto sb = ref new Storyboard(); + Storyboard::SetTarget(anim, FadePresenter); + Storyboard::SetTargetProperty(anim, "Opacity"); + sb->Children->Append(anim); + m_fadeStoryboard = sb; + + auto weakThis = WeakReference(this); + sb->Completed += ref new EventHandler( + [weakThis](Object^, Object^) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + + auto oldEl = dynamic_cast(that->BackgroundPresenter->Content); + if (oldEl != nullptr) { + that->m_discardedBg = oldEl; + TryStopAnimations(oldEl); + } + auto incoming = that->FadePresenter->Content; + that->FadePresenter->Content = nullptr; + that->FadePresenter->Opacity = 0.0; + that->BackgroundPresenter->Content = incoming; + that->m_currentKey = that->m_incomingKey; + that->m_incomingKey = nullptr; + that->m_fadeStoryboard = nullptr; + } catch (...) {} + }); + + sb->Begin(); + } catch (...) {} +} + +void DynamicBackgroundHost::SetHosts(Windows::Foundation::Collections::IVector^ hosts) +{ + m_hosts = hosts; + try { + auto el = dynamic_cast(BackgroundPresenter->Content); + if (auto sr = dynamic_cast(el)) { sr->SetHosts(hosts); } + } catch (...) {} + try { + auto el = dynamic_cast(FadePresenter->Content); + if (auto sr = dynamic_cast(el)) { sr->SetHosts(hosts); } + } catch (...) {} +} + +void DynamicBackgroundHost::StartAnimations() +{ + try { + auto el = dynamic_cast(BackgroundPresenter->Content); + if (el != nullptr) TryStartAnimations(el); + } catch (...) {} + try { + auto el = dynamic_cast(FadePresenter->Content); + if (el != nullptr) TryStartAnimations(el); + } catch (...) {} +} + +void DynamicBackgroundHost::StopAnimations() +{ + m_discardedBg = nullptr; + if (m_fadeStoryboard != nullptr) { + try { m_fadeStoryboard->Stop(); } catch (...) {} + m_fadeStoryboard = nullptr; + } + try { + auto el = dynamic_cast(BackgroundPresenter->Content); + if (el != nullptr) TryStopAnimations(el); + } catch (...) {} + try { + auto el = dynamic_cast(FadePresenter->Content); + if (el != nullptr) TryStopAnimations(el); + } catch (...) {} +} + +void DynamicBackgroundHost::ReloadBackgroundColors() +{ + try { + auto el = dynamic_cast(BackgroundPresenter->Content); + if (auto p = dynamic_cast(el)) { p->ReloadColors(); return; } + if (auto k = dynamic_cast(el)) { k->ReloadColors(); return; } + if (auto s = dynamic_cast(el)) { s->ReloadColors(); return; } + if (auto ob = dynamic_cast(el)) { ob->ReloadColors(); return; } + if (auto l = dynamic_cast(el)) { l->ReloadColors(); return; } + } catch (...) {} +} + +void DynamicBackgroundHost::ApplyBackground(Platform::String^ key, Platform::String^ scheme) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("background", key); + if (scheme != nullptr) + ls->Insert(key + ".scheme", scheme); + Refresh(); + StartAnimations(); +} + +void DynamicBackgroundHost::ResetBackground() +{ + ApplyBackground(DefaultKey, "neon"); +} diff --git a/UI/Backgrounds/DynamicBackgroundHost.xaml.h b/UI/Backgrounds/DynamicBackgroundHost.xaml.h new file mode 100644 index 00000000..e59a31e1 --- /dev/null +++ b/UI/Backgrounds/DynamicBackgroundHost.xaml.h @@ -0,0 +1,29 @@ +#pragma once +#include "UI\Backgrounds\DynamicBackgroundHost.g.h" +#include "State\MoonlightHost.h" + +namespace moonlight_xbox_dx { + +public ref class DynamicBackgroundHost sealed { +public: + DynamicBackgroundHost(); + static property Platform::String^ DefaultKey { + Platform::String^ get() { return ref new Platform::String(L"streaks"); } + } + void Refresh(); + void StartAnimations(); + void StopAnimations(); + void ApplyBackground(Platform::String^ key, Platform::String^ scheme); + void ResetBackground(); + void ReloadBackgroundColors(); + void SetHosts(Windows::Foundation::Collections::IVector^ hosts); +private: + Platform::String^ m_currentKey; + Platform::String^ m_incomingKey; + Windows::UI::Xaml::Media::Animation::Storyboard^ m_fadeStoryboard; + Platform::Object^ m_discardedBg; + Windows::Foundation::Collections::IVector^ m_hosts; + void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); +}; + +} diff --git a/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml b/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml new file mode 100644 index 00000000..a4b1e00b --- /dev/null +++ b/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml.cpp b/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml.cpp new file mode 100644 index 00000000..aba01bf4 --- /dev/null +++ b/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml.cpp @@ -0,0 +1,218 @@ +#include "pch.h" +#include "UI\Backgrounds\GlobeGrid\GlobeGridBackground.xaml.h" +#include +#include + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Shapes; + +static const int kLats = 9; +static const float kLatDeg[] = {-80,-60,-40,-20,0,20,40,60,80}; +static const int kLons = 12; +static const int kFrontSamp = 48; +static const int kLonSamp = 30; +static const float kPi = 3.14159265f; +static const float kRotSpeed = 0.0015f; +static const float kGlobeScale= 0.48f; +static const int kStars = 180; + +GlobeGridBackground::GlobeGridBackground() +{ + m_rng = std::mt19937(std::random_device{}()); + InitializeComponent(); + TimeSpan ts; + ts.Duration = 16 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = ts; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void GlobeGridBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + m_initialized = (m_canvasW > 0 && m_canvasH > 0); +} + +Point GlobeGridBackground::ProjectSphere(float latRad, float lonRad) +{ + float r = kGlobeScale * std::min(m_canvasW, m_canvasH) * 0.5f; + float lonView= lonRad + m_rotY; + float x3 = r * cosf(latRad) * sinf(lonView); + float y3 = r * sinf(latRad); + return Point(m_canvasW * 0.5f + x3, m_canvasH * 0.5f - y3); +} + +void GlobeGridBackground::InitItems() +{ + WireCanvas->Children->Clear(); + StarCanvas->Children->Clear(); + m_stars.clear(); + + Color gold = ColorHelper::FromArgb(255, 255, 196, 64); + Color cyan = ColorHelper::FromArgb(255, 64, 210, 255); + + for (int i = 0; i < kLats; ++i) { + auto pl = ref new Polyline(); + pl->Stroke = ref new SolidColorBrush(gold); + pl->StrokeThickness = 1.0; + pl->Opacity = 0.18; + auto pts = ref new PointCollection(); + for (int k = 0; k <= kFrontSamp; ++k) pts->Append(Point(0, 0)); + pl->Points = pts; + WireCanvas->Children->Append(pl); + } + + for (int i = 0; i < kLats; ++i) { + auto pl = ref new Polyline(); + if (i == 4) { + pl->Stroke = ref new SolidColorBrush(cyan); + pl->StrokeThickness = 2.0; + } else { + pl->Stroke = ref new SolidColorBrush(gold); + pl->StrokeThickness = 1.5; + } + pl->Opacity = 1.0; + auto pts = ref new PointCollection(); + for (int k = 0; k <= kFrontSamp; ++k) pts->Append(Point(0, 0)); + pl->Points = pts; + WireCanvas->Children->Append(pl); + } + + for (int j = 0; j < kLons; ++j) { + auto pl = ref new Polyline(); + pl->Stroke = ref new SolidColorBrush(gold); + pl->StrokeThickness = 1.5; + pl->Opacity = 1.0; + auto pts = ref new PointCollection(); + for (int k = 0; k <= kLonSamp; ++k) pts->Append(Point(0, 0)); + pl->Points = pts; + WireCanvas->Children->Append(pl); + } + + float maxR = hypotf(m_canvasW, m_canvasH) * 0.56f; + std::uniform_real_distribution distUnit(0.0f, 1.0f); + std::uniform_real_distribution distAngle(0.0f, 2.0f * kPi); + + for (int i = 0; i < kStars; ++i) { + GlobeStarState s; + s.radius = sqrtf(distUnit(m_rng)) * maxR; + s.angle = distAngle(m_rng); + + float speedBase = 0.00025f + distUnit(m_rng) * 0.00035f; + s.speed = speedBase * (1.0f + 0.6f * (1.0f - s.radius / maxR)); + s.size = 1.0f + distUnit(m_rng) * 2.2f; + s.baseOpacity = 0.25f + distUnit(m_rng) * 0.75f; + s.phase = distAngle(m_rng); + m_stars.push_back(s); + + auto star = ref new Ellipse(); + star->Width = s.size; + star->Height = s.size; + star->Fill = ref new SolidColorBrush(Colors::White); + star->Opacity = s.baseOpacity; + StarCanvas->Children->Append(star); + } + + m_itemsCreated = true; + UpdateGlobe(); + UpdateStars(); +} + +void GlobeGridBackground::UpdateGlobe() +{ + + for (int i = 0; i < kLats; ++i) { + float latR = kLatDeg[i] * kPi / 180.0f; + auto pl = safe_cast(WireCanvas->Children->GetAt(i)); + auto pts = pl->Points; + for (int k = 0; k <= kFrontSamp; ++k) { + float lonView = kPi * 0.5f + k * kPi / static_cast(kFrontSamp); + pts->SetAt(k, ProjectSphere(latR, lonView - m_rotY)); + } + } + + for (int i = 0; i < kLats; ++i) { + float latR = kLatDeg[i] * kPi / 180.0f; + auto pl = safe_cast(WireCanvas->Children->GetAt(kLats + i)); + auto pts = pl->Points; + for (int k = 0; k <= kFrontSamp; ++k) { + float lonView = -kPi * 0.5f + k * kPi / static_cast(kFrontSamp); + pts->SetAt(k, ProjectSphere(latR, lonView - m_rotY)); + } + } + + for (int j = 0; j < kLons; ++j) { + float worldLon = j * 2.0f * kPi / static_cast(kLons); + float lonView = worldLon + m_rotY; + bool isFront = (cosf(lonView) > 0.0f); + auto pl = safe_cast(WireCanvas->Children->GetAt(2 * kLats + j)); + pl->Opacity = isFront ? 1.0 : 0.18; + auto pts = pl->Points; + for (int k = 0; k <= kLonSamp; ++k) { + float latR = (-80.0f + k * 160.0f / static_cast(kLonSamp)) * kPi / 180.0f; + pts->SetAt(k, ProjectSphere(latR, worldLon)); + } + } +} + +void GlobeGridBackground::UpdateStars() +{ + int count = static_cast(m_stars.size()); + float cx = m_canvasW * 0.5f; + float cy = m_canvasH * 0.5f; + for (int i = 0; i < count; ++i) { + auto& s = m_stars[i]; + s.angle += s.speed; + float x = cx + cosf(s.angle) * s.radius; + float y = cy + sinf(s.angle) * s.radius; + auto star = safe_cast(StarCanvas->Children->GetAt(i)); + Canvas::SetLeft(star, x - s.size * 0.5f); + Canvas::SetTop(star, y - s.size * 0.5f); + + star->Opacity = s.baseOpacity * (0.60f + 0.40f * sinf(s.angle * 7.0f + s.phase)); + } +} + +void GlobeGridBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + + if (!m_itemsCreated) { + InitItems(); + return; + } + + m_rotY += kRotSpeed; + if (m_rotY > 2.0f * kPi) m_rotY -= 2.0f * kPi; + + UpdateGlobe(); + UpdateStars(); +} + +void GlobeGridBackground::StartAnimations() +{ + if (m_timer) m_timer->Start(); +} + +void GlobeGridBackground::StopAnimations() +{ + if (m_timer) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } +} diff --git a/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml.h b/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml.h new file mode 100644 index 00000000..10afba8d --- /dev/null +++ b/UI/Backgrounds/GlobeGrid/GlobeGridBackground.xaml.h @@ -0,0 +1,42 @@ +#pragma once +#include "UI\Backgrounds\GlobeGrid\GlobeGridBackground.g.h" +#include +#include + +namespace moonlight_xbox_dx { + +struct GlobeStarState { + float angle; + float radius; + float speed; + float size; + float baseOpacity; + float phase; +}; + +public ref class GlobeGridBackground sealed { +public: + GlobeGridBackground(); + void StartAnimations(); + void StopAnimations(); +private: + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_stars; + std::mt19937 m_rng; + + float m_canvasW = 0.0f; + float m_canvasH = 0.0f; + bool m_initialized = false; + bool m_itemsCreated = false; + float m_rotY = 0.0f; + + Windows::Foundation::Point ProjectSphere(float latRad, float lonRad); + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void InitItems(); + void UpdateGlobe(); + void UpdateStars(); +}; + +} diff --git a/UI/Backgrounds/Orbs/OrbsBackground.xaml b/UI/Backgrounds/Orbs/OrbsBackground.xaml new file mode 100644 index 00000000..7c056d24 --- /dev/null +++ b/UI/Backgrounds/Orbs/OrbsBackground.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Orbs/OrbsBackground.xaml.cpp b/UI/Backgrounds/Orbs/OrbsBackground.xaml.cpp new file mode 100644 index 00000000..b234d7d2 --- /dev/null +++ b/UI/Backgrounds/Orbs/OrbsBackground.xaml.cpp @@ -0,0 +1,373 @@ +#include "pch.h" +#include "UI\Backgrounds\Orbs\OrbsBackground.xaml.h" +#include + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Shapes; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; + +static const float kPi = 3.14159265358979f; + +static Color kOrbsDefaultGlow = { 255, 30, 160, 255 }; +static Color kOrbsDefaultBg = { 255, 0, 8, 20 }; + +static bool ParseHex6Orbs(Platform::String^ s, Color& out) +{ + if (s == nullptr || s->Length() != 6) return false; + const wchar_t* p = s->Data(); + wchar_t buf[7]; wcsncpy_s(buf, p, 6); buf[6] = L'\0'; + wchar_t* end = nullptr; + unsigned long v = wcstoul(buf, &end, 16); + if (end != buf + 6) return false; + out.A = 255; + out.R = (uint8_t)((v >> 16) & 0xFF); + out.G = (uint8_t)((v >> 8) & 0xFF); + out.B = (uint8_t)( v & 0xFF); + return true; +} + +static const float kOuterAuraR = 40.0f; +static const float kInnerGlowR = 30.0f; +static const float kCoreR = 20.0f; +static const float kMinTailR = 1.5f; +static const float kMaxTailR = 9.0f; +static const float kMaxTailOpacity = 0.88f; + +static const float kCircleTilt = 20.0f * kPi / 180.0f; +static const float kCircleRotSpeed = 0.007f; +static const int kDanceHoldTicks = 420; +static const int kCircleHoldTicks = 300; +static const int kTransitionTicks = 90; + +static const int kOuterAuraBase = kOrbCount * kOrbTailLen; +static const int kInnerGlowBase = kOrbCount * kOrbTailLen + kOrbCount; +static const int kCoreBase = kOrbCount * kOrbTailLen + kOrbCount * 2; + +static void OrbPos(const OrbState& o, float& outX, float& outY) +{ + float ca = cosf(o.angle), sa = sinf(o.angle); + float ct = cosf(o.tilt), st = sinf(o.tilt); + outX = o.cx + o.rx * ca * ct - o.ry * sa * st; + outY = o.cy + o.rx * ca * st + o.ry * sa * ct; +} + +OrbsBackground::OrbsBackground() +{ + InitializeComponent(); + this->Loaded += ref new RoutedEventHandler(this, &OrbsBackground::OnLoaded); + m_palette[0] = kOrbsDefaultGlow; + m_palette[1] = kOrbsDefaultBg; + OrbCanvas->Background = ref new SolidColorBrush(ColorHelper::FromArgb(255, 0, 8, 20)); + + TimeSpan interval; + interval.Duration = 16 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = interval; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void OrbsBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + if (!m_initialized && m_canvasW > 0 && m_canvasH > 0) { + InitOrbs(); + m_initialized = true; + } +} + +void OrbsBackground::LoadPalette() +{ + using namespace Windows::Storage; + Platform::String^ scheme = L"electric"; + auto ls = ApplicationData::Current->LocalSettings->Values; + if (ls->HasKey("orbs.scheme")) + scheme = safe_cast(ls->Lookup("orbs.scheme")); + + Color schemes[5][2] = { + { {255, 30,160,255}, {255, 0, 8, 20} }, + { {255, 0,220,150}, {255, 0, 10, 15} }, + { {255,255,140, 0}, {255, 12, 5, 0} }, + { {255,200, 80,255}, {255, 8, 0, 20} }, + { {255,255, 60,140}, {255, 15, 0, 12} }, + }; + + int idx = -1; + if (scheme->Equals(L"electric")) idx = 0; + else if (scheme->Equals(L"aurora")) idx = 1; + else if (scheme->Equals(L"solar")) idx = 2; + else if (scheme->Equals(L"nebula")) idx = 3; + else if (scheme->Equals(L"rose")) idx = 4; + + if (idx >= 0) { + m_palette[0] = schemes[idx][0]; + m_palette[1] = schemes[idx][1]; + return; + } + + static const wchar_t* kCustomKeys[] = { L"orbs.custom.0", L"orbs.custom.1" }; + Color defaults[2] = { kOrbsDefaultGlow, kOrbsDefaultBg }; + for (int i = 0; i < 2; ++i) { + Color c = defaults[i]; + auto key = ref new Platform::String(kCustomKeys[i]); + if (ls->HasKey(key)) + ParseHex6Orbs(safe_cast(ls->Lookup(key)), c); + m_palette[i] = c; + } +} + +void OrbsBackground::InitOrbs() +{ + LoadPalette(); + m_orbs.clear(); + OrbCanvas->Children->Clear(); + OrbCanvas->Background = ref new SolidColorBrush( + ColorHelper::FromArgb(m_palette[1].A, m_palette[1].R, m_palette[1].G, m_palette[1].B)); + + m_lerpT = 0.0f; + m_targetLerpT = 0.0f; + m_holdTicks = kDanceHoldTicks; + m_circleAngle = 0.0f; + + float W = m_canvasW, H = m_canvasH; + float cx = W * 0.5f, cy = H * 0.5f; + + struct OrbDef { float rxF, ryF, tilt, phase, speed; }; + static OrbDef kDefs[kOrbCount] = { + + { 0.13f, 0.09f, 30.0f * kPi/180.0f, 0.0f, 0.036f }, + { 0.13f, 0.09f, 30.0f * kPi/180.0f, kPi, -0.034f }, + { 0.13f, 0.09f, 30.0f * kPi/180.0f, kPi * 0.5f, 0.040f }, + + { 0.22f, 0.14f, 65.0f * kPi/180.0f, 0.0f, 0.024f }, + { 0.22f, 0.14f, 65.0f * kPi/180.0f, kPi * 2.0f/3.0f, -0.022f }, + { 0.22f, 0.14f, 65.0f * kPi/180.0f, kPi * 4.0f/3.0f, 0.026f }, + + { 0.32f, 0.20f, 15.0f * kPi/180.0f, kPi * 0.25f, 0.016f }, + { 0.32f, 0.20f, 15.0f * kPi/180.0f, kPi * 1.25f, 0.018f }, + }; + + m_orbs.reserve(kOrbCount); + for (int i = 0; i < kOrbCount; ++i) { + OrbState s; + s.cx = cx; + s.cy = cy; + s.rx = kDefs[i].rxF * W; + s.ry = kDefs[i].ryF * H; + s.tilt = kDefs[i].tilt; + s.angle = kDefs[i].phase; + s.speed = kDefs[i].speed; + + float startX, startY; + OrbPos(s, startX, startY); + for (int j = 0; j < kOrbTailLen; ++j) { + s.tailX[j] = startX; + s.tailY[j] = startY; + } + m_orbs.push_back(s); + } + + for (int i = 0; i < kOrbCount; ++i) { + const auto& s = m_orbs[i]; + for (int j = 0; j < kOrbTailLen; ++j) { + float t = static_cast(j) / (kOrbTailLen - 1); + float r = kMinTailR + t * (kMaxTailR - kMinTailR); + float op = t * kMaxTailOpacity; + + auto el = ref new Ellipse(); + el->Width = r * 2.0f; + el->Height = r * 2.0f; + Color gc = m_palette[0]; + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(255, gc.R, gc.G, gc.B)); + el->Opacity = op; + Canvas::SetLeft(el, s.tailX[j] - r); + Canvas::SetTop(el, s.tailY[j] - r); + OrbCanvas->Children->Append(el); + } + } + + for (int i = 0; i < kOrbCount; ++i) { + const auto& s = m_orbs[i]; + Color col = m_palette[0]; + float x, y; OrbPos(s, x, y); + + auto el = ref new Ellipse(); + el->Width = kOuterAuraR * 2.0f; + el->Height = kOuterAuraR * 2.0f; + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(80, col.R, col.G, col.B)); + Canvas::SetLeft(el, x - kOuterAuraR); + Canvas::SetTop(el, y - kOuterAuraR); + OrbCanvas->Children->Append(el); + } + + for (int i = 0; i < kOrbCount; ++i) { + const auto& s = m_orbs[i]; + Color col = m_palette[0]; + float x, y; OrbPos(s, x, y); + + auto el = ref new Ellipse(); + el->Width = kInnerGlowR * 2.0f; + el->Height = kInnerGlowR * 2.0f; + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(215, col.R, col.G, col.B)); + Canvas::SetLeft(el, x - kInnerGlowR); + Canvas::SetTop(el, y - kInnerGlowR); + OrbCanvas->Children->Append(el); + } + + for (int i = 0; i < kOrbCount; ++i) { + const auto& s = m_orbs[i]; + float x, y; OrbPos(s, x, y); + + auto el = ref new Ellipse(); + el->Width = kCoreR * 2.0f; + el->Height = kCoreR * 2.0f; + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(255, 255, 255, 255)); + Canvas::SetLeft(el, x - kCoreR); + Canvas::SetTop(el, y - kCoreR); + OrbCanvas->Children->Append(el); + } +} + +void OrbsBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + + float W = m_canvasW, H = m_canvasH; + float cx = W * 0.5f, cy = H * 0.5f; + float circleR = (W < H ? W : H) * 0.26f; + + if (--m_holdTicks <= 0) { + if (m_targetLerpT < 0.5f) { + m_targetLerpT = 1.0f; + m_holdTicks = kCircleHoldTicks + kTransitionTicks; + } else { + m_targetLerpT = 0.0f; + m_holdTicks = kDanceHoldTicks + kTransitionTicks; + } + } + + float step = 1.0f / static_cast(kTransitionTicks); + float remaining = m_targetLerpT - m_lerpT; + if ((remaining < 0.0f ? -remaining : remaining) < step) { + m_lerpT = m_targetLerpT; + } else { + m_lerpT += remaining > 0.0f ? step : -step; + } + + float st = m_lerpT * m_lerpT * (3.0f - 2.0f * m_lerpT); + + m_circleAngle += kCircleRotSpeed; + float ctilt = cosf(kCircleTilt), stilt = sinf(kCircleTilt); + + for (int i = 0; i < kOrbCount; ++i) { + auto& s = m_orbs[i]; + + for (int j = 0; j < kOrbTailLen - 1; ++j) { + s.tailX[j] = s.tailX[j + 1]; + s.tailY[j] = s.tailY[j + 1]; + } + + if (m_lerpT == 0.0f) s.angle += s.speed; + float dx, dy; + OrbPos(s, dx, dy); + + float cAngle = m_circleAngle + i * (2.0f * kPi / kOrbCount); + float cca = cosf(cAngle), csa = sinf(cAngle); + float px = cx + circleR * cca * ctilt - circleR * csa * stilt; + float py = cy + circleR * cca * stilt + circleR * csa * ctilt; + + float nx = dx + st * (px - dx); + float ny = dy + st * (py - dy); + + s.tailX[kOrbTailLen - 1] = nx; + s.tailY[kOrbTailLen - 1] = ny; + + for (int j = 0; j < kOrbTailLen; ++j) { + float t = static_cast(j) / (kOrbTailLen - 1); + float r = kMinTailR + t * (kMaxTailR - kMinTailR); + auto el = safe_cast(OrbCanvas->Children->GetAt(i * kOrbTailLen + j)); + Canvas::SetLeft(el, s.tailX[j] - r); + Canvas::SetTop(el, s.tailY[j] - r); + } + + auto aura = safe_cast(OrbCanvas->Children->GetAt(kOuterAuraBase + i)); + Canvas::SetLeft(aura, nx - kOuterAuraR); + Canvas::SetTop(aura, ny - kOuterAuraR); + + auto glow = safe_cast(OrbCanvas->Children->GetAt(kInnerGlowBase + i)); + Canvas::SetLeft(glow, nx - kInnerGlowR); + Canvas::SetTop(glow, ny - kInnerGlowR); + + auto core = safe_cast(OrbCanvas->Children->GetAt(kCoreBase + i)); + Canvas::SetLeft(core, nx - kCoreR); + Canvas::SetTop(core, ny - kCoreR); + } +} + +void OrbsBackground::OnLoaded(Object^ sender, RoutedEventArgs^ e) +{ + auto anim = ref new DoubleAnimation(); + anim->From = 0.0; + anim->To = 1.0; + TimeSpan ts; + ts.Duration = 5000000LL; + anim->Duration = Windows::UI::Xaml::Duration(ts); + OrbCanvas->Opacity = 0.0; + auto sb = ref new Storyboard(); + Storyboard::SetTarget(anim, OrbCanvas); + Storyboard::SetTargetProperty(anim, "Opacity"); + sb->Children->Append(anim); + sb->Begin(); +} + +void OrbsBackground::ReloadColors() +{ + if (!m_initialized) return; + LoadPalette(); + + Color gc = m_palette[0]; + Color bc = m_palette[1]; + + OrbCanvas->Background = ref new SolidColorBrush( + ColorHelper::FromArgb(bc.A, bc.R, bc.G, bc.B)); + + for (int i = 0; i < kOrbCount; ++i) { + for (int j = 0; j < kOrbTailLen; ++j) { + auto el = safe_cast(OrbCanvas->Children->GetAt(i * kOrbTailLen + j)); + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(255, gc.R, gc.G, gc.B)); + } + } + for (int i = 0; i < kOrbCount; ++i) { + auto el = safe_cast(OrbCanvas->Children->GetAt(kOuterAuraBase + i)); + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(80, gc.R, gc.G, gc.B)); + } + for (int i = 0; i < kOrbCount; ++i) { + auto el = safe_cast(OrbCanvas->Children->GetAt(kInnerGlowBase + i)); + el->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(215, gc.R, gc.G, gc.B)); + } + +} + +void OrbsBackground::StartAnimations() { if (m_timer) m_timer->Start(); } + +void OrbsBackground::StopAnimations() +{ + if (m_timer != nullptr) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } +} diff --git a/UI/Backgrounds/Orbs/OrbsBackground.xaml.h b/UI/Backgrounds/Orbs/OrbsBackground.xaml.h new file mode 100644 index 00000000..91de075e --- /dev/null +++ b/UI/Backgrounds/Orbs/OrbsBackground.xaml.h @@ -0,0 +1,47 @@ +#pragma once +#include "UI\Backgrounds\Orbs\OrbsBackground.g.h" +#include + +namespace moonlight_xbox_dx { + +static const int kOrbCount = 8; +static const int kOrbTailLen = 48; + +struct OrbState { + float cx, cy; + float rx, ry; + float tilt; + float angle; + float speed; + float tailX[kOrbTailLen]; + float tailY[kOrbTailLen]; +}; + +public ref class OrbsBackground sealed { +public: + OrbsBackground(); + void StartAnimations(); + void StopAnimations(); + void ReloadColors(); +private: + Windows::UI::Color m_palette[2]; + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_orbs; + float m_canvasW = 0; + float m_canvasH = 0; + bool m_initialized = false; + + float m_lerpT = 0.0f; + float m_targetLerpT = 0.0f; + int m_holdTicks = 0; + float m_circleAngle = 0.0f; + + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void InitOrbs(); + void LoadPalette(); +}; + +} diff --git a/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml b/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml new file mode 100644 index 00000000..d43e57df --- /dev/null +++ b/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml.cpp b/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml.cpp new file mode 100644 index 00000000..76ccf146 --- /dev/null +++ b/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml.cpp @@ -0,0 +1,156 @@ +#include "pch.h" +#include "OrbsSettingsControl.xaml.h" +#include "UI\Backgrounds\BackgroundSettingsHelpers.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +static Windows::UI::Color kElectricUI[] = { {255, 30,160,255}, {255, 0, 8, 20} }; +static Windows::UI::Color kAuroraUI[] = { {255, 0,220,150}, {255, 0, 10, 15} }; +static Windows::UI::Color kSolarUI[] = { {255,255,140, 0}, {255, 12, 5, 0} }; +static Windows::UI::Color kNebulaUI[] = { {255,200, 80,255}, {255, 8, 0, 20} }; +static Windows::UI::Color kRoseUI[] = { {255,255, 60,140}, {255, 15, 0, 12} }; + +static Windows::UI::Color kGlowSwatches[] = { + {255, 30,160,255}, {255, 0,220,150}, {255,255,140, 0}, {255,200, 80,255}, + {255,255, 60,140}, {255, 0,240,240}, {255,255, 0, 0}, {255, 0,255,128}, + {255,255,255, 0}, {255,255,128, 0}, {255,128, 0,255}, {255,255,255,255}, +}; +static const int kGlowSwatchCount = 12; + +static Windows::UI::Color kBgSwatches[] = { + {255, 0, 8, 20}, {255, 0, 10, 15}, {255, 12, 5, 0}, {255, 8, 0, 20}, + {255, 15, 0, 12}, {255, 0, 0, 0}, {255, 5, 10, 0}, {255, 10, 5, 5}, +}; +static const int kBgSwatchCount = 8; + +static const wchar_t* kCustomKeys[] = { L"orbs.custom.0", L"orbs.custom.1" }; + +OrbsSettingsControl::OrbsSettingsControl() +{ + InitializeComponent(); +} + +void OrbsSettingsControl::Initialize(DynamicBackgroundHost^ host) +{ + m_host = host; + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + Platform::String^ scheme = L"electric"; + if (ls->HasKey("orbs.scheme")) + scheme = safe_cast(ls->Lookup("orbs.scheme")); + + struct { const wchar_t* key; const wchar_t* label; } schemes[] = { + { L"electric", L"Electric (Default)" }, + { L"aurora", L"Aurora" }, + { L"solar", L"Solar" }, + { L"nebula", L"Nebula" }, + { L"rose", L"Rose" }, + { L"custom", L"Custom" }, + }; + + int selectedIdx = 0; + for (int i = 0; i < 6; ++i) { + auto item = ref new ComboBoxItem(); + item->Content = ref new Platform::String(schemes[i].label); + item->DataContext = ref new Platform::String(schemes[i].key); + SchemeSelector->Items->Append(item); + if (scheme->Equals(ref new Platform::String(schemes[i].key))) selectedIdx = i; + } + SchemeSelector->SelectedIndex = selectedIdx; + UpdateCustomPanelVisibility(); + + Windows::UI::Color* defaults = kElectricUI; + if (scheme->Equals(L"aurora")) defaults = kAuroraUI; + else if (scheme->Equals(L"solar")) defaults = kSolarUI; + else if (scheme->Equals(L"nebula")) defaults = kNebulaUI; + else if (scheme->Equals(L"rose")) defaults = kRoseUI; + + if (Color0 != nullptr) { + Color0->SetSwatches(kGlowSwatches, kGlowSwatchCount); + Windows::UI::Color c0 = defaults[0]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[0]); + if (ls->HasKey(k)) c0 = BgHexToColor(safe_cast(ls->Lookup(k)), c0); + } + Color0->SelectColor(c0, false); + Color0->ColorChanged += ref new SwatchColorChangedHandler(this, &OrbsSettingsControl::Color0_ColorChanged); + } + + if (Color1 != nullptr) { + Color1->SetSwatches(kBgSwatches, kBgSwatchCount); + Windows::UI::Color c1 = defaults[1]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[1]); + if (ls->HasKey(k)) c1 = BgHexToColor(safe_cast(ls->Lookup(k)), c1); + } + Color1->SelectColor(c1, false); + Color1->ColorChanged += ref new SwatchColorChangedHandler(this, &OrbsSettingsControl::Color1_ColorChanged); + } + + m_initialized = true; +} + +void OrbsSettingsControl::UpdateCustomPanelVisibility() +{ + if (CustomPanel == nullptr || SchemeSelector == nullptr) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + bool isCustom = (item != nullptr && item->DataContext != nullptr && + item->DataContext->ToString()->Equals(L"custom")); + CustomPanel->Visibility = isCustom ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; +} + +void OrbsSettingsControl::SchemeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + if (item == nullptr) return; + auto key = item->DataContext->ToString(); + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("orbs.scheme", key); + + if (!key->Equals(L"custom")) { + Windows::UI::Color* defaults = kElectricUI; + if (key->Equals(L"aurora")) defaults = kAuroraUI; + else if (key->Equals(L"solar")) defaults = kSolarUI; + else if (key->Equals(L"nebula")) defaults = kNebulaUI; + else if (key->Equals(L"rose")) defaults = kRoseUI; + if (Color0 != nullptr) Color0->SelectColor(defaults[0], false); + if (Color1 != nullptr) Color1->SelectColor(defaults[1], false); + } + + UpdateCustomPanelVisibility(); + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void OrbsSettingsControl::Color0_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[0]), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void OrbsSettingsControl::Color1_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[1]), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void OrbsSettingsControl::ResetButton_Click(Platform::Object^, RoutedEventArgs^) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Remove("orbs.scheme"); + ls->Remove(ref new Platform::String(kCustomKeys[0])); + ls->Remove(ref new Platform::String(kCustomKeys[1])); + + SchemeSelector->SelectedIndex = 0; + if (Color0 != nullptr) Color0->SelectColor(kElectricUI[0], false); + if (Color1 != nullptr) Color1->SelectColor(kElectricUI[1], false); + CustomPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} diff --git a/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml.h b/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml.h new file mode 100644 index 00000000..08d14fb4 --- /dev/null +++ b/UI/Backgrounds/Orbs/OrbsSettingsControl.xaml.h @@ -0,0 +1,23 @@ +#pragma once +#include "UI\Backgrounds\Orbs\OrbsSettingsControl.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +namespace moonlight_xbox_dx { + +public ref class OrbsSettingsControl sealed { +public: + OrbsSettingsControl(); + void Initialize(DynamicBackgroundHost^ host); +private: + DynamicBackgroundHost^ m_host; + bool m_initialized = false; + + void SchemeSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void Color0_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color1_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void ResetButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateCustomPanelVisibility(); +}; + +} diff --git a/UI/Backgrounds/Particles/ParticleBackground.xaml b/UI/Backgrounds/Particles/ParticleBackground.xaml new file mode 100644 index 00000000..0002d93e --- /dev/null +++ b/UI/Backgrounds/Particles/ParticleBackground.xaml @@ -0,0 +1,12 @@ + + + diff --git a/UI/Backgrounds/Particles/ParticleBackground.xaml.cpp b/UI/Backgrounds/Particles/ParticleBackground.xaml.cpp new file mode 100644 index 00000000..428023f1 --- /dev/null +++ b/UI/Backgrounds/Particles/ParticleBackground.xaml.cpp @@ -0,0 +1,312 @@ +#include "pch.h" +#include "UI\Backgrounds\Particles\ParticleBackground.xaml.h" +#include + +using namespace moonlight_xbox_dx; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Shapes; +using namespace Windows::UI::Xaml::Media; + +static Color LerpRGB(Color a, Color b, float t, uint8_t alpha = 255) { + auto ch = [](uint8_t x, uint8_t y, float f) -> uint8_t { + return static_cast(x + (static_cast(y) - static_cast(x)) * f); + }; + return ColorHelper::FromArgb(alpha, ch(a.R, b.R, t), ch(a.G, b.G, t), ch(a.B, b.B, t)); +} + +static Color ScaleRGB(Color c, float s, uint8_t alpha = 255) { + return ColorHelper::FromArgb(alpha, + static_cast(fminf(c.R * s, 255.0f)), + static_cast(fminf(c.G * s, 255.0f)), + static_cast(fminf(c.B * s, 255.0f))); +} + +static bool ParseHex6(Platform::String^ s, Color& out) +{ + if (s == nullptr || s->Length() != 6) return false; + const wchar_t* p = s->Data(); + wchar_t buf[7]; wcsncpy_s(buf, p, 6); buf[6] = L'\0'; + wchar_t* end = nullptr; + unsigned long v = wcstoul(buf, &end, 16); + if (end != buf + 6) return false; + out.A = 255; + out.R = (uint8_t)((v >> 16) & 0xFF); + out.G = (uint8_t)((v >> 8) & 0xFF); + out.B = (uint8_t)( v & 0xFF); + return true; +} + +static const int kBokehCount = 50; +static const int kSmallCount = 130; +static const int kParticleCount = kBokehCount + kSmallCount; +static const float kPi = 3.14159265f; +static const float kSpreadRange = 0.18f; + +float ParticleBackground::WaveY(float t) { + float centerY = m_canvasH * (0.80f - t * 0.60f); + return centerY + m_canvasH * 0.10f * sinf(t * kPi * 3.0f + m_wavePhase); +} + +void ParticleBackground::LoadPalette() +{ + using namespace Windows::Storage; + Platform::String^ scheme = L"champagne"; + auto ls = ApplicationData::Current->LocalSettings->Values; + if (ls->HasKey("particles.scheme")) + scheme = safe_cast(ls->Lookup("particles.scheme")); + + struct { Color particle; Color gradient; } presets[] = { + { {255,210,165, 75}, {255, 50, 30,120} }, + { {255,255,120, 40}, {255, 15, 80, 90} }, + { {255, 80,215,190}, {255, 90, 15,150} }, + { {255,180, 90,230}, {255, 10, 20,100} }, + { {255,235,110,150}, {255, 15, 85, 60} }, + }; + int idx = 0; + if (scheme->Equals(L"ember")) idx = 1; + else if (scheme->Equals(L"aurora")) idx = 2; + else if (scheme->Equals(L"nebula")) idx = 3; + else if (scheme->Equals(L"blossom")) idx = 4; + + if (scheme->Equals(L"custom")) { + m_colorA = presets[0].particle; + m_gradientColor = presets[0].gradient; + if (ls->HasKey("particles.custom.0")) + ParseHex6(safe_cast(ls->Lookup("particles.custom.0")), m_colorA); + if (ls->HasKey("particles.custom.1")) + ParseHex6(safe_cast(ls->Lookup("particles.custom.1")), m_gradientColor); + } else { + m_colorA = presets[idx].particle; + m_gradientColor = presets[idx].gradient; + } + m_colorB = ScaleRGB(m_colorA, 0.35f); +} + +ParticleBackground::ParticleBackground() +{ + m_rng = std::mt19937(std::random_device{}()); + InitializeComponent(); + + TimeSpan interval; + interval.Duration = 33 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = interval; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void ParticleBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + + if (!m_initialized && m_canvasW > 0 && m_canvasH > 0) { + InitParticles(); + m_initialized = true; + } +} + +void ParticleBackground::ApplyGradient() +{ + Color stop0 = ScaleRGB(m_gradientColor, 0.15f); stop0.A = 255; + Color stop1 = { 255, 5, 5, 10 }; + + if (m_bgBrush == nullptr) { + auto gs0 = ref new GradientStop(); gs0->Color = stop0; gs0->Offset = 0.0; + auto gs1 = ref new GradientStop(); gs1->Color = stop1; gs1->Offset = 1.0; + m_bgBrush = ref new LinearGradientBrush(); + m_bgBrush->StartPoint = { 0.0, 1.0 }; + m_bgBrush->EndPoint = { 1.0, 0.0 }; + m_bgBrush->GradientStops->Append(gs0); + m_bgBrush->GradientStops->Append(gs1); + ParticleCanvas->Background = m_bgBrush; + } else { + m_bgBrush->GradientStops->GetAt(0)->Color = stop0; + m_bgBrush->GradientStops->GetAt(1)->Color = stop1; + } +} + +void ParticleBackground::ReloadColors() +{ + if (!m_initialized) return; + LoadPalette(); + ApplyGradient(); + + SolidColorBrush^ bokehBrushes[2]; + bokehBrushes[0] = ref new SolidColorBrush(LerpRGB(m_colorA, m_colorB, 0.3f, 45)); + bokehBrushes[1] = ref new SolidColorBrush(LerpRGB(m_colorA, m_colorB, 0.7f, 40)); + + Color white = { 255, 255, 255, 255 }; + Color coreCol = LerpRGB(m_colorA, white, 0.45f, 220); + SolidColorBrush^ smallBrushes[5]; + for (int b = 0; b < 5; ++b) { + float t = b / 4.0f; + uint8_t a = static_cast(220 - t * 65); + smallBrushes[b] = ref new SolidColorBrush(LerpRGB(coreCol, m_colorB, t, a)); + } + + int count = static_cast(m_particles.size()); + for (int i = 0; i < count; ++i) { + auto& s = m_particles[i]; + auto el = safe_cast(ParticleCanvas->Children->GetAt(i)); + el->Fill = s.isBokeh ? bokehBrushes[s.colorSlot] : smallBrushes[s.colorSlot]; + } +} + +void ParticleBackground::InitParticles() +{ + LoadPalette(); + ApplyGradient(); + m_particles.clear(); + m_particles.reserve(kParticleCount); + ParticleCanvas->Children->Clear(); + + SolidColorBrush^ bokehBrushes[2]; + bokehBrushes[0] = ref new SolidColorBrush(LerpRGB(m_colorA, m_colorB, 0.3f, 45)); + bokehBrushes[1] = ref new SolidColorBrush(LerpRGB(m_colorA, m_colorB, 0.7f, 40)); + + Color white = { 255, 255, 255, 255 }; + Color coreCol = LerpRGB(m_colorA, white, 0.45f, 220); + SolidColorBrush^ smallBrushes[5]; + for (int b = 0; b < 5; ++b) { + float t = b / 4.0f; + uint8_t a = static_cast(220 - t * 65); + smallBrushes[b] = ref new SolidColorBrush(LerpRGB(coreCol, m_colorB, t, a)); + } + + std::uniform_real_distribution distT(0.0f, 1.0f); + + std::uniform_real_distribution bSpread(-0.35f, 0.35f); + std::uniform_real_distribution bSpeed(0.0005f, 0.0018f); + std::uniform_real_distribution bSize(20.0f, 80.0f); + std::uniform_real_distribution bOp(0.03f, 0.15f); + std::uniform_real_distribution bDelta(0.001f, 0.003f); + + for (int i = 0; i < kBokehCount; ++i) { + ParticleState s; + s.isBokeh = true; + s.t = distT(m_rng); + s.tSpeed = bSpeed(m_rng); + s.spreadY = bSpread(m_rng) * m_canvasH; + s.size = bSize(m_rng); + s.opacityMin = 0.16f; + s.opacityMax = 0.32f; + s.opacity = bOp(m_rng); + s.opacityDelta = bDelta(m_rng) * (m_rng() % 2 == 0 ? 1.0f : -1.0f); + s.colorSlot = m_rng() % 2; + m_particles.push_back(s); + + float x = s.t * m_canvasW; + float y = WaveY(s.t) + s.spreadY; + auto el = ref new Ellipse(); + el->Width = s.size; el->Height = s.size; + el->Fill = bokehBrushes[s.colorSlot]; + el->Opacity = s.opacity; + Canvas::SetLeft(el, x - s.size * 0.5f); + Canvas::SetTop(el, y - s.size * 0.5f); + ParticleCanvas->Children->Append(el); + } + + std::uniform_real_distribution sSpread(-kSpreadRange, kSpreadRange); + std::uniform_real_distribution sSpeed(0.0010f, 0.0030f); + std::uniform_real_distribution sSize(1.5f, 5.5f); + std::uniform_real_distribution sDelta(0.004f, 0.012f); + + for (int i = 0; i < kSmallCount; ++i) { + float spread = sSpread(m_rng) * m_canvasH; + float normDist = fabsf(spread) / (kSpreadRange * m_canvasH); + + ParticleState s; + s.isBokeh = false; + s.t = distT(m_rng); + s.tSpeed = sSpeed(m_rng); + s.spreadY = spread; + s.size = sSize(m_rng); + s.opacityMax = fmaxf(0.28f, 0.95f - normDist * 0.65f); + s.opacityMin = s.opacityMax * 0.15f; + s.opacity = s.opacityMin + (s.opacityMax - s.opacityMin) + * static_cast(m_rng() % 100) / 100.0f; + s.opacityDelta = sDelta(m_rng) * (m_rng() % 2 == 0 ? 1.0f : -1.0f); + int band = static_cast(normDist * 5.0f); + if (band >= 5) band = 4; + s.colorSlot = band; + m_particles.push_back(s); + + float x = s.t * m_canvasW; + float y = WaveY(s.t) + s.spreadY; + auto el = ref new Ellipse(); + el->Width = s.size; el->Height = s.size; + el->Fill = smallBrushes[band]; + el->Opacity = s.opacity; + Canvas::SetLeft(el, x - s.size * 0.5f); + Canvas::SetTop(el, y - s.size * 0.5f); + ParticleCanvas->Children->Append(el); + } +} + +void ParticleBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + + m_wavePhase += 0.008f; + if (m_wavePhase > kPi * 2.0f) m_wavePhase -= kPi * 2.0f; + + std::uniform_real_distribution bSpread(-0.35f, 0.35f); + std::uniform_real_distribution sSpread(-kSpreadRange, kSpreadRange); + + int count = static_cast(m_particles.size()); + for (int i = 0; i < count; ++i) { + auto& s = m_particles[i]; + + s.t += s.tSpeed; + if (s.t > 1.05f) { + s.t = -0.05f; + if (s.isBokeh) { + s.spreadY = bSpread(m_rng) * m_canvasH; + } else { + float spread = sSpread(m_rng) * m_canvasH; + s.spreadY = spread; + float normDist = fabsf(spread) / (kSpreadRange * m_canvasH); + s.opacityMax = fmaxf(0.28f, 0.95f - normDist * 0.65f); + s.opacityMin = s.opacityMax * 0.15f; + } + } + + float x = s.t * m_canvasW; + float y = WaveY(s.t) + s.spreadY; + + s.opacity += s.opacityDelta; + if (s.opacity > s.opacityMax) { s.opacity = s.opacityMax; s.opacityDelta = -fabsf(s.opacityDelta); } + if (s.opacity < s.opacityMin) { s.opacity = s.opacityMin; s.opacityDelta = fabsf(s.opacityDelta); } + + auto el = safe_cast(ParticleCanvas->Children->GetAt(i)); + Canvas::SetLeft(el, x - s.size * 0.5f); + Canvas::SetTop(el, y - s.size * 0.5f); + el->Opacity = s.opacity; + } +} + +void ParticleBackground::StartAnimations() +{ + if (m_timer != nullptr) m_timer->Start(); +} + +void ParticleBackground::StopAnimations() +{ + if (m_timer != nullptr) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } +} diff --git a/UI/Backgrounds/Particles/ParticleBackground.xaml.h b/UI/Backgrounds/Particles/ParticleBackground.xaml.h new file mode 100644 index 00000000..e5262f2e --- /dev/null +++ b/UI/Backgrounds/Particles/ParticleBackground.xaml.h @@ -0,0 +1,49 @@ +#pragma once +#include "UI\Backgrounds\Particles\ParticleBackground.g.h" +#include +#include + +namespace moonlight_xbox_dx { + +struct ParticleState { + float t; + float tSpeed; + float spreadY; + float opacity; + float opacityDelta; + float opacityMin; + float opacityMax; + float size; + bool isBokeh; + int colorSlot; +}; + +public ref class ParticleBackground sealed { +public: + ParticleBackground(); + void StartAnimations(); + void StopAnimations(); + void ReloadColors(); +private: + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_particles; + float m_canvasW = 0; + float m_canvasH = 0; + float m_wavePhase = 0.0f; + bool m_initialized = false; + std::mt19937 m_rng; + Windows::UI::Color m_colorA; + Windows::UI::Color m_colorB; + Windows::UI::Color m_gradientColor; + Windows::UI::Xaml::Media::LinearGradientBrush^ m_bgBrush; + + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void InitParticles(); + void LoadPalette(); + void ApplyGradient(); + float WaveY(float t); +}; + +} diff --git a/UI/Backgrounds/Particles/ParticleSettingsControl.xaml b/UI/Backgrounds/Particles/ParticleSettingsControl.xaml new file mode 100644 index 00000000..4957af37 --- /dev/null +++ b/UI/Backgrounds/Particles/ParticleSettingsControl.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Particles/ParticleSettingsControl.xaml.cpp b/UI/Backgrounds/Particles/ParticleSettingsControl.xaml.cpp new file mode 100644 index 00000000..6468b80d --- /dev/null +++ b/UI/Backgrounds/Particles/ParticleSettingsControl.xaml.cpp @@ -0,0 +1,146 @@ +#include "pch.h" +#include "ParticleSettingsControl.xaml.h" +#include "UI\Backgrounds\BackgroundSettingsHelpers.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +static Windows::UI::Color kChampagneScheme[] = { {255,210,165, 75}, {255, 50, 30,120} }; +static Windows::UI::Color kEmberScheme[] = { {255,255,120, 40}, {255, 15, 80, 90} }; +static Windows::UI::Color kAuroraScheme[] = { {255, 80,215,190}, {255, 90, 15,150} }; +static Windows::UI::Color kNebulaScheme[] = { {255,180, 90,230}, {255, 10, 20,100} }; +static Windows::UI::Color kBlossomScheme[] = { {255,235,110,150}, {255, 15, 85, 60} }; + +static Windows::UI::Color kSwatches[] = { + {255,210,165, 75}, {255,230,120,140}, {255, 80,200,180}, + {255,160,100,220}, {255,160,200,255}, {255,255,100, 80}, + {255,100,200,100}, {255,255,180, 80}, {255, 80,120,255}, + {255,220,180,255}, {255,255,255,255}, {255,180,180,180}, +}; +static const int kSwatchCount = 12; + +static const wchar_t* kKey0 = L"particles.custom.0"; +static const wchar_t* kKey1 = L"particles.custom.1"; + +ParticleSettingsControl::ParticleSettingsControl() +{ + InitializeComponent(); +} + +void ParticleSettingsControl::Initialize(DynamicBackgroundHost^ host) +{ + m_host = host; + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + Platform::String^ scheme = L"champagne"; + if (ls->HasKey("particles.scheme")) + scheme = safe_cast(ls->Lookup("particles.scheme")); + + struct { const wchar_t* key; const wchar_t* label; } schemes[] = { + { L"champagne", L"Champagne (Default)" }, + { L"ember", L"Ember" }, + { L"aurora", L"Aurora" }, + { L"nebula", L"Nebula" }, + { L"blossom", L"Blossom" }, + { L"custom", L"Custom" }, + }; + + int selectedIdx = 0; + for (int i = 0; i < 6; ++i) { + auto item = ref new ComboBoxItem(); + item->Content = ref new Platform::String(schemes[i].label); + item->DataContext = ref new Platform::String(schemes[i].key); + SchemeSelector->Items->Append(item); + if (scheme->Equals(ref new Platform::String(schemes[i].key))) selectedIdx = i; + } + SchemeSelector->SelectedIndex = selectedIdx; + UpdateCustomPanelVisibility(); + + Windows::UI::Color* defaults = kChampagneScheme; + if (scheme->Equals(L"ember")) defaults = kEmberScheme; + else if (scheme->Equals(L"aurora")) defaults = kAuroraScheme; + else if (scheme->Equals(L"nebula")) defaults = kNebulaScheme; + else if (scheme->Equals(L"blossom")) defaults = kBlossomScheme; + + Color0->SetSwatches(kSwatches, kSwatchCount); + Color1->SetSwatches(kSwatches, kSwatchCount); + + Windows::UI::Color c0 = defaults[0], c1 = defaults[1]; + if (scheme->Equals(L"custom")) { + auto k0 = ref new Platform::String(kKey0); + auto k1 = ref new Platform::String(kKey1); + if (ls->HasKey(k0)) c0 = BgHexToColor(safe_cast(ls->Lookup(k0)), c0); + if (ls->HasKey(k1)) c1 = BgHexToColor(safe_cast(ls->Lookup(k1)), c1); + } + Color0->SelectColor(c0, false); + Color1->SelectColor(c1, false); + + Color0->ColorChanged += ref new SwatchColorChangedHandler(this, &ParticleSettingsControl::Color0_ColorChanged); + Color1->ColorChanged += ref new SwatchColorChangedHandler(this, &ParticleSettingsControl::Color1_ColorChanged); + + m_initialized = true; +} + +void ParticleSettingsControl::UpdateCustomPanelVisibility() +{ + if (CustomPanel == nullptr || SchemeSelector == nullptr) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + bool isCustom = (item != nullptr && item->DataContext != nullptr && + item->DataContext->ToString()->Equals(L"custom")); + CustomPanel->Visibility = isCustom ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; +} + +void ParticleSettingsControl::SchemeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + if (item == nullptr) return; + auto key = item->DataContext->ToString(); + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("particles.scheme", key); + + if (!key->Equals(L"custom")) { + Windows::UI::Color* defaults = kChampagneScheme; + if (key->Equals(L"ember")) defaults = kEmberScheme; + else if (key->Equals(L"aurora")) defaults = kAuroraScheme; + else if (key->Equals(L"nebula")) defaults = kNebulaScheme; + else if (key->Equals(L"blossom")) defaults = kBlossomScheme; + if (Color0 != nullptr) Color0->SelectColor(defaults[0], false); + if (Color1 != nullptr) Color1->SelectColor(defaults[1], false); + } + + UpdateCustomPanelVisibility(); + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void ParticleSettingsControl::Color0_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kKey0), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void ParticleSettingsControl::Color1_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kKey1), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void ParticleSettingsControl::ResetButton_Click(Platform::Object^, RoutedEventArgs^) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Remove("particles.scheme"); + ls->Remove(ref new Platform::String(kKey0)); + ls->Remove(ref new Platform::String(kKey1)); + + SchemeSelector->SelectedIndex = 0; + if (Color0 != nullptr) Color0->SelectColor(kChampagneScheme[0], false); + if (Color1 != nullptr) Color1->SelectColor(kChampagneScheme[1], false); + CustomPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} diff --git a/UI/Backgrounds/Particles/ParticleSettingsControl.xaml.h b/UI/Backgrounds/Particles/ParticleSettingsControl.xaml.h new file mode 100644 index 00000000..5ea3a39d --- /dev/null +++ b/UI/Backgrounds/Particles/ParticleSettingsControl.xaml.h @@ -0,0 +1,23 @@ +#pragma once +#include "UI\Backgrounds\Particles\ParticleSettingsControl.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +namespace moonlight_xbox_dx { + +public ref class ParticleSettingsControl sealed { +public: + ParticleSettingsControl(); + void Initialize(DynamicBackgroundHost^ host); +private: + DynamicBackgroundHost^ m_host; + bool m_initialized = false; + + void SchemeSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void Color0_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color1_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void ResetButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateCustomPanelVisibility(); +}; + +} diff --git a/UI/Backgrounds/README.md b/UI/Backgrounds/README.md new file mode 100644 index 00000000..227ee52e --- /dev/null +++ b/UI/Backgrounds/README.md @@ -0,0 +1,519 @@ +# Dynamic Backgrounds + +This directory contains all animated backgrounds for the moonlight-xbox app. Each background is an +independent C++/CX XAML UserControl that plugs into `DynamicBackgroundHost`. + +Each background lives in its own subfolder: + +``` +UI\Backgrounds\ + Particles\ ParticleBackground + ParticleSettingsControl + Streaks\ StreaksBackground + StreaksSettingsControl + Spheres\ SpheresBackground + SpheresSettingsControl + Blobs\ BlobsBackground + BlobsSettingsControl + Orbs\ OrbsBackground + OrbsSettingsControl + SwipeReveal\ SwipeRevealBackground + GlobeGrid\ GlobeGridBackground +``` + +--- + +## Architecture + +`DynamicBackgroundHost` is a `UserControl` that manages two `ContentPresenter` slots: + +- **`BackgroundPresenter`** — the currently visible, stable background. +- **`FadePresenter`** — the incoming background, crossfaded in over 300ms, then promoted to stable. + +On load (and when `Refresh()` is called) the host reads +`ApplicationData::Current->LocalSettings->Values["background"]`, calls `CreateBackground(key)` to +instantiate the right class, and fades it in. It routes `StartAnimations()` / `StopAnimations()` to +the active child via `TryStartAnimations` / `TryStopAnimations` — manual `dynamic_cast` chains +because there is **no base class or interface**. + +Each background exposes exactly two required public methods: + +```cpp +void StartAnimations(); +void StopAnimations(); +``` + +Optional public methods (only implement if needed): + +```cpp +void ReloadOptions(); // apply updated LocalSettings without recreating canvas +void SetHosts(IVector^ hosts); // only if background renders app art +``` + +### Per-background settings controls + +Backgrounds that expose user-configurable settings (colors, presets, shape, etc.) each own a +companion **settings UserControl** in the same subfolder — e.g. `Orbs\OrbsSettingsControl.xaml`. +`HostSettingsPage` instantiates the right one dynamically via `UpdateBackgroundSettingsContent(key)` +and places it in a `ContentControl`. No changes to `HostSettingsPage` are needed when adding a new +background with settings. + +--- + +## Adding a New Background — Step-by-Step + +### 1. Register the key + +Open `BackgroundRegistry.h` and add one entry to the `kBackgrounds` array: + +```cpp +{ L"mykey", L"My Display Name" }, +``` + +`kBackgroundCount` is computed automatically via `sizeof` — do not touch it. + +--- + +### 2. Create a subfolder and the three source files + +``` +UI\Backgrounds\Foo\FooBackground.xaml +UI\Backgrounds\Foo\FooBackground.xaml.h +UI\Backgrounds\Foo\FooBackground.xaml.cpp +``` + +See the [canonical templates](#canonical-templates) below. + +--- + +### 3. Wire into DynamicBackgroundHost.xaml.cpp + +Add the `#include` at the top: + +```cpp +#include "UI\Backgrounds\Foo\FooBackground.xaml.h" +``` + +Add one branch to each of the three static functions: + +```cpp +// TryStartAnimations +if (auto f = dynamic_cast(el)) { f->StartAnimations(); return; } + +// TryStopAnimations +if (auto f = dynamic_cast(el)) { f->StopAnimations(); return; } + +// CreateBackground +if (key->Equals(ref new String(L"mykey"))) return ref new FooBackground(); +``` + +--- + +### 4. Register in moonlight-xbox-dx.vcxproj + +Add three XML entries (copy the pattern from any existing background): + +```xml + + + UI\Backgrounds\Foo\FooBackground.xaml + + + + + + + + UI\Backgrounds\Foo\FooBackground.xaml + +``` + +--- + +### 5. Register in moonlight-xbox-dx.vcxproj.filters + +Only the `Page` item needs an explicit entry (the `.h` and `.cpp` are pulled in via `DependentUpon`): + +```xml + +``` + +--- + +### 6. (Optional) SetHosts — only if your background renders app art + +If your background needs the list of `MoonlightHost^` objects (like `SwipeRevealBackground`), add +to **both** `Refresh()` and `SetHosts()` in `DynamicBackgroundHost.xaml.cpp`: + +```cpp +if (auto f = dynamic_cast(newBg)) { f->SetHosts(m_hosts); } +``` + +And declare the method on the class: + +```cpp +void SetHosts(Windows::Foundation::Collections::IVector^ hosts); +``` + +--- + +### 7. (Optional) Custom options — only if your background exposes user-configurable settings + +If users can adjust settings (colors, speed, density, presets, etc.), create a companion +**settings UserControl** in the same subfolder. `HostSettingsPage` picks it up automatically — +no edits to `HostSettingsPage` are required. + +See [Adding Custom Options](#adding-custom-options) for the full pattern. + +--- + +## Canonical Templates + +### FooBackground.xaml + +```xml + + + + + +``` + +### FooBackground.xaml.h + +```cpp +#pragma once +#include "UI\Backgrounds\Foo\FooBackground.g.h" +#include +#include + +namespace moonlight_xbox_dx { + +struct FooState { + float x, y; + float vx, vy; +}; + +public ref class FooBackground sealed { +public: + FooBackground(); + void StartAnimations(); + void StopAnimations(); +private: + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_items; + std::mt19937 m_rng; + float m_canvasW = 0.0f; + float m_canvasH = 0.0f; + bool m_initialized = false; + + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void InitItems(); +}; + +} +``` + +### FooBackground.xaml.cpp + +```cpp +#include "pch.h" +#include "UI\Backgrounds\Foo\FooBackground.xaml.h" +#include + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Shapes; +using namespace Windows::UI::Xaml::Media; + +FooBackground::FooBackground() +{ + m_rng = std::mt19937(std::random_device{}()); + InitializeComponent(); + + TimeSpan interval; + interval.Duration = 16 * 10000LL; // 16ms ≈ 60fps; use 33*10000LL for 30fps + m_timer = ref new DispatcherTimer(); + m_timer->Interval = interval; + m_tickToken = m_timer->Tick += ref new EventHandler(this, &FooBackground::OnTick); +} + +void FooBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + if (!m_initialized && m_canvasW > 0 && m_canvasH > 0) { + InitItems(); + m_initialized = true; + } +} + +void FooBackground::InitItems() +{ + m_items.clear(); + FooCanvas->Children->Clear(); + // Create shapes, push to m_items, append to FooCanvas->Children in a fixed order. + // OnTick retrieves children by index — order here must match retrieval order there. +} + +void FooBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + int count = static_cast(m_items.size()); + for (int i = 0; i < count; ++i) { + auto& s = m_items[i]; + // update s.x, s.y, etc. + auto el = safe_cast(FooCanvas->Children->GetAt(i)); + Canvas::SetLeft(el, s.x); + Canvas::SetTop(el, s.y); + } +} + +void FooBackground::StartAnimations() { if (m_timer) m_timer->Start(); } +void FooBackground::StopAnimations() { if (m_timer) m_timer->Stop(); } +``` + +--- + +## Key Patterns and Invariants + +### Timer intervals + +| Duration constant | Frame rate | Used by | +|---|---|---| +| `16 * 10000LL` | ~60 fps | Streaks, Spheres, Orbs | +| `33 * 10000LL` | ~30 fps | Particles, GlobeGrid | + +### Canvas children index contract + +`InitItems()` appends children in a **fixed, known order**. `OnTick` retrieves them by index via +`safe_cast`. Never shuffle or conditionally skip insertions. If each item has multiple visual layers +(e.g., glow + core), push all glows first (indices `0..N-1`) then all cores (indices `N..2N-1`). +See `StreaksBackground` for an example using `kGlowBase` / `kCoreBase` constants. + +### SizeChanged / lazy initialization + +Canvas dimensions are unknown at construction time. Always gate `InitItems()`: + +```cpp +if (!m_initialized && m_canvasW > 0 && m_canvasH > 0) { + InitItems(); + m_initialized = true; +} +``` + +### RNG + +```cpp +// Seed in constructor: +m_rng = std::mt19937(std::random_device{}()); + +// Use: +std::uniform_real_distribution dist(minVal, maxVal); +float val = dist(m_rng); +``` + +### WinRT color construction + +```cpp +ColorHelper::FromArgb(alpha, r, g, b); // all uint8_t; A, R, G, B order +``` + +### Color helpers (copy from Particles\ParticleBackground.xaml.cpp if needed) + +```cpp +// Linear interpolate two Colors +static Color LerpRGB(Color a, Color b, float t, uint8_t alpha = 255); + +// Scale (darken) a Color +static Color ScaleRGB(Color c, float s, uint8_t alpha = 255); +``` + +### C++/CX palette array restrictions + +Palette arrays **must be non-const** — `const Windows::UI::Color` fails to compile in C++/CX. +Also do **not** use `ARRAYSIZE` on member arrays; use a literal count instead. + +```cpp +// .h +Windows::UI::Color m_palette[4]; // non-const, literal count + +// .cpp — OK +int n = 4; +``` + +--- + +## Adding Custom Options + +Backgrounds with user-configurable settings (colors, speed, density, presets, etc.) expose them +via a companion **settings UserControl** that lives in the same subfolder: + +``` +UI\Backgrounds\Foo\FooSettingsControl.xaml +UI\Backgrounds\Foo\FooSettingsControl.xaml.h +UI\Backgrounds\Foo\FooSettingsControl.xaml.cpp +``` + +`HostSettingsPage` calls `UpdateBackgroundSettingsContent(key)` whenever the background selection +changes. That function instantiates the right settings control and places it in a `ContentControl` +— **you do not touch `HostSettingsPage` at all**. Just register the new control in +`UpdateBackgroundSettingsContent` (in `HostSettingsPage.xaml.cpp`): + +```cpp +} else if (key->Equals(L"mykey")) { + auto c = ref new FooSettingsControl(); + c->Initialize(BackgroundHost); + ctrl = c; +} +``` + +And add the corresponding `#include` at the top of `HostSettingsPage.xaml.cpp` and +`HostSettingsPage.xaml.h`. + +### Settings control structure + +**FooSettingsControl.xaml** — A `UserControl` with a scheme `ComboBox`, a collapsed `CustomPanel` +with `SwatchPicker` rows, and a Reset button. See `OrbsSettingsControl.xaml` for a minimal 2-color +example and `BlobsSettingsControl.xaml` for a 4-color example. + +**FooSettingsControl.xaml.h** + +```cpp +#pragma once +#include "UI\Backgrounds\Foo\FooSettingsControl.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +namespace moonlight_xbox_dx { + +public ref class FooSettingsControl sealed { +public: + FooSettingsControl(); + void Initialize(DynamicBackgroundHost^ host); +private: + DynamicBackgroundHost^ m_host = nullptr; + bool m_initialized = false; + void SchemeSelector_SelectionChanged(Platform::Object^, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^); + void Color0_ColorChanged(Platform::Object^, Windows::UI::Color, bool); + void ResetButton_Click(Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^); + void UpdateCustomPanelVisibility(); +}; + +} +``` + +**FooSettingsControl.xaml.cpp** + +```cpp +#include "pch.h" +#include "FooSettingsControl.xaml.h" +#include "UI\Backgrounds\BackgroundSettingsHelpers.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +// Scheme palette arrays — must be non-const (C++/CX restriction) +static Windows::UI::Color kDefaultScheme[] = { {255, 30,160,255}, {255, 0, 8, 20} }; +// ... additional schemes ... + +static const wchar_t* kCustomKeys[] = { L"mykey.custom.0", L"mykey.custom.1" }; + +FooSettingsControl::FooSettingsControl() { InitializeComponent(); } + +void FooSettingsControl::Initialize(DynamicBackgroundHost^ host) +{ + m_host = host; + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + + // Populate scheme ComboBox, restore saved index + // Set up SwatchPicker swatches and call SelectColor(savedColor, false) + // Wire ColorChanged events + + m_initialized = true; +} + +void FooSettingsControl::SchemeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + // Save scheme key to LocalSettings, update pickers, call UpdateCustomPanelVisibility() + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void FooSettingsControl::Color0_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[0]), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void FooSettingsControl::ResetButton_Click(Platform::Object^, RoutedEventArgs^) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Remove("mykey.scheme"); + for (int i = 0; i < 2; ++i) ls->Remove(ref new Platform::String(kCustomKeys[i])); + SchemeSelector->SelectedIndex = 0; + // Reset pickers to default scheme colors + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} +``` + +### Init guard pattern + +Gate every `SelectionChanged` / `ColorChanged` handler with `if (!m_initialized) return;` to +prevent spurious LocalSettings writes while controls are being programmatically initialized. + +### Color hex helpers + +Use the shared utilities from `BackgroundSettingsHelpers.h` (already included above): + +```cpp +Platform::String^ BgColorToHex(Windows::UI::Color c); // Color → "RRGGBB" +Windows::UI::Color BgHexToColor(Platform::String^ s, Windows::UI::Color fallback); +``` + +### Reloading colors in the background + +When a setting changes, call `m_host->ReloadBackgroundColors()`. This dispatches to the active +background's `ReloadOptions()` (or equivalent) without recreating the canvas. + +> **Important:** Never call `Refresh()` to apply option changes. `Refresh()` is a no-op when the +> background key is unchanged. Always go through the dedicated reload path. + +--- + +## Files Modified for Every New Background + +| File | What to add | +|---|---| +| `UI\Backgrounds\BackgroundRegistry.h` | New `{ L"key", L"Display Name" }` entry | +| `UI\Backgrounds\DynamicBackgroundHost.xaml.cpp` | `#include` + branches in `TryStartAnimations`, `TryStopAnimations`, `CreateBackground` | +| `UI\Backgrounds\Foo\FooBackground.xaml` | New file | +| `UI\Backgrounds\Foo\FooBackground.xaml.h` | New file | +| `UI\Backgrounds\Foo\FooBackground.xaml.cpp` | New file | +| `moonlight-xbox-dx.vcxproj` | `ClInclude` + `Page` + `ClCompile` entries (subfolder paths) | +| `moonlight-xbox-dx.vcxproj.filters` | `Page` entry (subfolder path) | + +## Additional Files for Backgrounds with Custom Settings + +| File | What to add | +|---|---| +| `UI\Backgrounds\Foo\FooSettingsControl.xaml` | New file | +| `UI\Backgrounds\Foo\FooSettingsControl.xaml.h` | New file | +| `UI\Backgrounds\Foo\FooSettingsControl.xaml.cpp` | New file | +| `UI\Pages\HostSettingsPage.xaml.h` | `#include` for `FooSettingsControl.xaml.h` | +| `UI\Pages\HostSettingsPage.xaml.cpp` | `#include` + new `else if` branch in `UpdateBackgroundSettingsContent()` | +| `moonlight-xbox-dx.vcxproj` | `ClInclude` + `Page` + `ClCompile` entries for the settings control | +| `moonlight-xbox-dx.vcxproj.filters` | `Page` entry for the settings control | diff --git a/UI/Backgrounds/Spheres/SpheresBackground.xaml b/UI/Backgrounds/Spheres/SpheresBackground.xaml new file mode 100644 index 00000000..0c41050d --- /dev/null +++ b/UI/Backgrounds/Spheres/SpheresBackground.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/UI/Backgrounds/Spheres/SpheresBackground.xaml.cpp b/UI/Backgrounds/Spheres/SpheresBackground.xaml.cpp new file mode 100644 index 00000000..e99cc602 --- /dev/null +++ b/UI/Backgrounds/Spheres/SpheresBackground.xaml.cpp @@ -0,0 +1,306 @@ +#include "pch.h" +#include "UI\Backgrounds\Spheres\SpheresBackground.xaml.h" +#include + +using namespace moonlight_xbox_dx; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Shapes; +using namespace Windows::UI::Xaml::Media; + +static const int kSphereCount = 14; + +static Color kSpheresClassicScheme[3] = { {255,255,255,255}, {255, 0, 68,170}, {255, 0, 34,119} }; +static Color kSpheresNeonScheme[3] = { {255, 0,238,255}, {255, 0, 32,128}, {255, 0, 8, 48} }; +static Color kSpheresSunsetScheme[3] = { {255,255,112, 64}, {255,139, 21, 0}, {255, 32, 0, 8} }; +static Color kSpheresOceanScheme[3] = { {255, 0,200,176}, {255, 0, 64, 96}, {255, 0, 16, 32} }; +static Color kSpheresNebulaScheme[3] = { {255,192, 96,255}, {255, 64, 0,128}, {255, 13, 0, 32} }; + +static void SetSphereCanvasBg(Canvas^ canvas, Windows::UI::Color top, Windows::UI::Color bottom) +{ + auto lgb = ref new LinearGradientBrush(); + lgb->StartPoint = Point(0.5, 0.0); + lgb->EndPoint = Point(0.5, 1.1); + auto gs0 = ref new GradientStop(); + gs0->Color = ColorHelper::FromArgb(255, top.R, top.G, top.B); + gs0->Offset = 0.0; + auto gs1 = ref new GradientStop(); + gs1->Color = ColorHelper::FromArgb(255, bottom.R, bottom.G, bottom.B); + gs1->Offset = 1.0; + lgb->GradientStops->Append(gs0); + lgb->GradientStops->Append(gs1); + canvas->Background = lgb; +} + +static bool ParseHex6Spheres(Platform::String^ s, Color& out) +{ + if (s == nullptr || s->Length() != 6) return false; + const wchar_t* p = s->Data(); + wchar_t buf[7]; wcsncpy_s(buf, p, 6); buf[6] = L'\0'; + wchar_t* end = nullptr; + unsigned long v = wcstoul(buf, &end, 16); + if (end != buf + 6) return false; + out.A = 255; + out.R = (uint8_t)((v >> 16) & 0xFF); + out.G = (uint8_t)((v >> 8) & 0xFF); + out.B = (uint8_t)( v & 0xFF); + return true; +} + +SpheresBackground::SpheresBackground() +{ + m_rng = std::mt19937(std::random_device{}()); + InitializeComponent(); + + TimeSpan interval; + interval.Duration = 16 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = interval; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void SpheresBackground::LoadPalette() +{ + using namespace Windows::Storage; + Platform::String^ scheme = L"classic"; + auto ls = ApplicationData::Current->LocalSettings->Values; + if (ls->HasKey("spheres.scheme")) + scheme = safe_cast(ls->Lookup("spheres.scheme")); + + Color* src = kSpheresClassicScheme; + if (scheme->Equals(L"neon")) src = kSpheresNeonScheme; + else if (scheme->Equals(L"sunset")) src = kSpheresSunsetScheme; + else if (scheme->Equals(L"ocean")) src = kSpheresOceanScheme; + else if (scheme->Equals(L"nebula")) src = kSpheresNebulaScheme; + + if (!scheme->Equals(L"custom")) { + m_palette[0] = src[0]; + m_palette[1] = src[1]; + m_palette[2] = src[2]; + return; + } + + static const wchar_t* kCustomKeys[] = { L"spheres.custom.0", L"spheres.custom.1" }; + for (int i = 0; i < 2; ++i) { + Color c = kSpheresClassicScheme[i]; + auto key = ref new Platform::String(kCustomKeys[i]); + if (ls->HasKey(key)) + ParseHex6Spheres(safe_cast(ls->Lookup(key)), c); + m_palette[i] = c; + } + Color top = m_palette[1]; + m_palette[2] = ColorHelper::FromArgb(255, + static_cast(top.R * 0.40f), + static_cast(top.G * 0.40f), + static_cast(top.B * 0.40f)); +} + +void SpheresBackground::LoadShapeMode() +{ + using namespace Windows::Storage; + auto ls = ApplicationData::Current->LocalSettings->Values; + if (!ls->HasKey("spheres.shape")) { m_shapeMode = 0; return; } + auto val = safe_cast(ls->Lookup("spheres.shape")); + if (val->Equals(L"squares")) m_shapeMode = 1; + else if (val->Equals(L"triangles")) m_shapeMode = 2; + else if (val->Equals(L"all")) m_shapeMode = 3; + else m_shapeMode = 0; +} + +void SpheresBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + + if (!m_initialized && m_canvasW > 0 && m_canvasH > 0) { + InitSpheres(); + m_initialized = true; + } +} + +static UIElement^ MakeBubbleShape(int shapeType, float r, + Windows::UI::Color shapeColor, float opacity) +{ + auto xf = ref new RotateTransform(); + xf->CenterX = static_cast(r); + xf->CenterY = static_cast(r); + + if (shapeType == kShapeSquare) { + auto rect = ref new Rectangle(); + rect->Width = r * 2.0f; + rect->Height = r * 2.0f; + rect->Stroke = ref new SolidColorBrush( + ColorHelper::FromArgb(255, shapeColor.R, shapeColor.G, shapeColor.B)); + rect->StrokeThickness = 1.5; + rect->Opacity = opacity; + rect->RenderTransform = xf; + return rect; + } + + if (shapeType == kShapeTriangle) { + + auto tri = ref new Polygon(); + auto pts = ref new PointCollection(); + pts->Append(Point(r, 0.0f )); + pts->Append(Point(r + r * 0.866f, r + r * 0.5f )); + pts->Append(Point(r - r * 0.866f, r + r * 0.5f )); + tri->Points = pts; + tri->Stroke = ref new SolidColorBrush( + ColorHelper::FromArgb(255, shapeColor.R, shapeColor.G, shapeColor.B)); + tri->StrokeThickness = 1.5; + tri->Opacity = opacity; + tri->RenderTransform = xf; + return tri; + } + + auto el = ref new Ellipse(); + el->Width = r * 2.0f; + el->Height = r * 2.0f; + el->Stroke = ref new SolidColorBrush( + ColorHelper::FromArgb(255, shapeColor.R, shapeColor.G, shapeColor.B)); + el->StrokeThickness = 1.0; + el->Opacity = opacity; + el->RenderTransform = xf; + return el; +} + +void SpheresBackground::InitSpheres() +{ + LoadPalette(); + LoadShapeMode(); + + m_spheres.clear(); + m_spheres.reserve(kSphereCount); + SphereCanvas->Children->Clear(); + SetSphereCanvasBg(SphereCanvas, m_palette[1], m_palette[2]); + + std::uniform_real_distribution distAngle(0.0f, 6.28318f); + std::uniform_real_distribution distOpacity(0.05f, 0.25f); + + for (int i = 0; i < kSphereCount; ++i) { + SphereState s; + float minSpeed, maxSpeed, minSpin, maxSpin; + + if (i < 3) { + s.radius = 110.0f + static_cast(m_rng() % 70); + minSpeed = 0.15f; maxSpeed = 0.45f; + minSpin = 0.2f; maxSpin = 0.6f; + } else if (i < 8) { + s.radius = 50.0f + static_cast(m_rng() % 50); + minSpeed = 0.40f; maxSpeed = 1.0f; + minSpin = 0.5f; maxSpin = 1.2f; + } else { + s.radius = 18.0f + static_cast(m_rng() % 28); + minSpeed = 0.75f; maxSpeed = 1.6f; + minSpin = 1.0f; maxSpin = 2.5f; + } + + std::uniform_real_distribution distX(s.radius, m_canvasW - s.radius); + std::uniform_real_distribution distY(s.radius, m_canvasH - s.radius); + std::uniform_real_distribution distSpeed(minSpeed, maxSpeed); + std::uniform_real_distribution distSpin(minSpin, maxSpin); + + s.x = distX(m_rng); + s.y = distY(m_rng); + s.opacity = distOpacity(m_rng); + s.spinAngle = distAngle(m_rng) * (180.0f / 3.14159265f); + float spinMag = distSpin(m_rng); + s.spinSpeed = (m_rng() % 2 == 0) ? spinMag : -spinMag; + + float moveAngle = distAngle(m_rng); + float speed = distSpeed(m_rng); + s.vx = cosf(moveAngle) * speed; + s.vy = sinf(moveAngle) * speed; + + s.shapeType = (m_shapeMode == 3) ? (i % 3) : m_shapeMode; + + m_spheres.push_back(s); + + auto el = MakeBubbleShape(s.shapeType, s.radius, m_palette[0], s.opacity); + auto rt = dynamic_cast(el->RenderTransform); + if (rt != nullptr) rt->Angle = static_cast(s.spinAngle); + Canvas::SetLeft(el, s.x - s.radius); + Canvas::SetTop(el, s.y - s.radius); + SphereCanvas->Children->Append(el); + } +} + +void SpheresBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + + int count = static_cast(m_spheres.size()); + for (int i = 0; i < count; ++i) { + auto& s = m_spheres[i]; + + s.x += s.vx; + s.y += s.vy; + + if (s.x - s.radius < 0.0f) { s.x = s.radius; s.vx = fabsf(s.vx); } + else if (s.x + s.radius > m_canvasW) { s.x = m_canvasW - s.radius; s.vx = -fabsf(s.vx); } + if (s.y - s.radius < 0.0f) { s.y = s.radius; s.vy = fabsf(s.vy); } + else if (s.y + s.radius > m_canvasH) { s.y = m_canvasH - s.radius; s.vy = -fabsf(s.vy); } + + s.spinAngle += s.spinSpeed; + + auto el = SphereCanvas->Children->GetAt(i); + Canvas::SetLeft(el, s.x - s.radius); + Canvas::SetTop(el, s.y - s.radius); + auto rt = dynamic_cast(el->RenderTransform); + if (rt != nullptr) rt->Angle = static_cast(s.spinAngle); + } +} + +void SpheresBackground::ReloadColors() +{ + if (!m_initialized) return; + + int prevShapeMode = m_shapeMode; + LoadShapeMode(); + + if (m_shapeMode != prevShapeMode) { + InitSpheres(); + return; + } + + LoadPalette(); + + SetSphereCanvasBg(SphereCanvas, m_palette[1], m_palette[2]); + + Color shapeCol = m_palette[0]; + auto brush = ref new SolidColorBrush( + ColorHelper::FromArgb(255, shapeCol.R, shapeCol.G, shapeCol.B)); + + int count = static_cast(m_spheres.size()); + for (int i = 0; i < count; ++i) { + auto el = SphereCanvas->Children->GetAt(i); + if (auto shape = dynamic_cast(el)) { + shape->Stroke = brush; + } + } +} + +void SpheresBackground::StartAnimations() +{ + if (m_timer != nullptr) m_timer->Start(); +} + +void SpheresBackground::StopAnimations() +{ + if (m_timer != nullptr) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } +} diff --git a/UI/Backgrounds/Spheres/SpheresBackground.xaml.h b/UI/Backgrounds/Spheres/SpheresBackground.xaml.h new file mode 100644 index 00000000..3d267e68 --- /dev/null +++ b/UI/Backgrounds/Spheres/SpheresBackground.xaml.h @@ -0,0 +1,46 @@ +#pragma once +#include "UI\Backgrounds\Spheres\SpheresBackground.g.h" +#include +#include + +namespace moonlight_xbox_dx { + +static const int kShapeCircle = 0; +static const int kShapeSquare = 1; +static const int kShapeTriangle = 2; + +struct SphereState { + float x, y; + float vx, vy; + float radius; + float opacity; + float spinAngle; + float spinSpeed; + int shapeType; +}; + +public ref class SpheresBackground sealed { +public: + SpheresBackground(); + void StartAnimations(); + void StopAnimations(); + void ReloadColors(); +private: + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_spheres; + float m_canvasW = 0; + float m_canvasH = 0; + bool m_initialized = false; + std::mt19937 m_rng; + Windows::UI::Color m_palette[3]; + int m_shapeMode = 0; + + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void InitSpheres(); + void LoadPalette(); + void LoadShapeMode(); +}; + +} diff --git a/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml b/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml new file mode 100644 index 00000000..d55ecae7 --- /dev/null +++ b/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml.cpp b/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml.cpp new file mode 100644 index 00000000..b90c0788 --- /dev/null +++ b/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml.cpp @@ -0,0 +1,192 @@ +#include "pch.h" +#include "SpheresSettingsControl.xaml.h" +#include "UI\Backgrounds\BackgroundSettingsHelpers.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +static Windows::UI::Color kClassicUI[] = { {255,255,255,255}, {255, 0, 68,170} }; +static Windows::UI::Color kNeonUI[] = { {255, 0,238,255}, {255, 0, 32,128} }; +static Windows::UI::Color kSunsetUI[] = { {255,255,112, 64}, {255,139, 21, 0} }; +static Windows::UI::Color kOceanUI[] = { {255, 0,200,176}, {255, 0, 64, 96} }; +static Windows::UI::Color kNebulaUI[] = { {255,192, 96,255}, {255, 64, 0,128} }; + +static Windows::UI::Color kShapeSwatches[] = { + {255,255,255,255}, {255, 0,238,255}, {255,255,112, 64}, {255, 0,200,176}, + {255,192, 96,255}, {255,255,224, 0}, {255, 0,255,128}, {255,255, 80,120}, + {255,100,200,255}, {255,220,180,255}, {255,255,200,100}, {255,180,255,180}, +}; +static const int kShapeSwatchCount = 12; + +static Windows::UI::Color kBgSwatches[] = { + {255, 0, 68,170}, {255, 0, 32,128}, {255,139, 21, 0}, {255, 0, 64, 96}, + {255, 64, 0,128}, {255, 0,100, 60}, {255,120, 60, 0}, {255, 80, 20, 80}, +}; +static const int kBgSwatchCount = 8; + +static const wchar_t* kCustomKeys[] = { L"spheres.custom.0", L"spheres.custom.1" }; + +SpheresSettingsControl::SpheresSettingsControl() +{ + InitializeComponent(); +} + +void SpheresSettingsControl::Initialize(DynamicBackgroundHost^ host) +{ + m_host = host; + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + Platform::String^ scheme = L"classic"; + if (ls->HasKey("spheres.scheme")) + scheme = safe_cast(ls->Lookup("spheres.scheme")); + + struct { const wchar_t* key; const wchar_t* label; } schemes[] = { + { L"classic", L"Classic (Default)" }, + { L"neon", L"Neon" }, + { L"sunset", L"Sunset" }, + { L"ocean", L"Ocean" }, + { L"nebula", L"Nebula" }, + { L"custom", L"Custom" }, + }; + + int selectedIdx = 0; + for (int i = 0; i < 6; ++i) { + auto item = ref new ComboBoxItem(); + item->Content = ref new Platform::String(schemes[i].label); + item->DataContext = ref new Platform::String(schemes[i].key); + SchemeSelector->Items->Append(item); + if (scheme->Equals(ref new Platform::String(schemes[i].key))) selectedIdx = i; + } + SchemeSelector->SelectedIndex = selectedIdx; + UpdateCustomPanelVisibility(); + + struct { const wchar_t* key; const wchar_t* label; } shapes[] = { + { L"circles", L"Circles (Default)" }, + { L"squares", L"Squares" }, + { L"triangles", L"Triangles" }, + { L"all", L"All Shapes" }, + }; + + Platform::String^ savedShape = L"circles"; + if (ls->HasKey("spheres.shape")) + savedShape = safe_cast(ls->Lookup("spheres.shape")); + + int shapeIdx = 0; + for (int i = 0; i < 4; ++i) { + auto item = ref new ComboBoxItem(); + item->Content = ref new Platform::String(shapes[i].label); + item->DataContext = ref new Platform::String(shapes[i].key); + ShapeSelector->Items->Append(item); + if (savedShape->Equals(ref new Platform::String(shapes[i].key))) shapeIdx = i; + } + ShapeSelector->SelectedIndex = shapeIdx; + + Windows::UI::Color* defaults = kClassicUI; + if (scheme->Equals(L"neon")) defaults = kNeonUI; + else if (scheme->Equals(L"sunset")) defaults = kSunsetUI; + else if (scheme->Equals(L"ocean")) defaults = kOceanUI; + else if (scheme->Equals(L"nebula")) defaults = kNebulaUI; + + if (Color0 != nullptr) { + Color0->SetSwatches(kShapeSwatches, kShapeSwatchCount); + Windows::UI::Color c0 = defaults[0]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[0]); + if (ls->HasKey(k)) c0 = BgHexToColor(safe_cast(ls->Lookup(k)), c0); + } + Color0->SelectColor(c0, false); + Color0->ColorChanged += ref new SwatchColorChangedHandler(this, &SpheresSettingsControl::Color0_ColorChanged); + } + + if (Color1 != nullptr) { + Color1->SetSwatches(kBgSwatches, kBgSwatchCount); + Windows::UI::Color c1 = defaults[1]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[1]); + if (ls->HasKey(k)) c1 = BgHexToColor(safe_cast(ls->Lookup(k)), c1); + } + Color1->SelectColor(c1, false); + Color1->ColorChanged += ref new SwatchColorChangedHandler(this, &SpheresSettingsControl::Color1_ColorChanged); + } + + m_initialized = true; +} + +void SpheresSettingsControl::UpdateCustomPanelVisibility() +{ + if (CustomPanel == nullptr || SchemeSelector == nullptr) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + bool isCustom = (item != nullptr && item->DataContext != nullptr && + item->DataContext->ToString()->Equals(L"custom")); + CustomPanel->Visibility = isCustom ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; +} + +void SpheresSettingsControl::SchemeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + if (item == nullptr) return; + auto key = item->DataContext->ToString(); + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("spheres.scheme", key); + + if (!key->Equals(L"custom")) { + Windows::UI::Color* defaults = kClassicUI; + if (key->Equals(L"neon")) defaults = kNeonUI; + else if (key->Equals(L"sunset")) defaults = kSunsetUI; + else if (key->Equals(L"ocean")) defaults = kOceanUI; + else if (key->Equals(L"nebula")) defaults = kNebulaUI; + if (Color0 != nullptr) Color0->SelectColor(defaults[0], false); + if (Color1 != nullptr) Color1->SelectColor(defaults[1], false); + } + + UpdateCustomPanelVisibility(); + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void SpheresSettingsControl::ShapeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + auto item = dynamic_cast(ShapeSelector->SelectedItem); + if (item == nullptr) return; + auto key = item->DataContext->ToString(); + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("spheres.shape", key); + + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void SpheresSettingsControl::Color0_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[0]), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void SpheresSettingsControl::Color1_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[1]), BgColorToHex(color)); + try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +void SpheresSettingsControl::ResetButton_Click(Platform::Object^, RoutedEventArgs^) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Remove("spheres.scheme"); + ls->Remove("spheres.shape"); + ls->Remove(ref new Platform::String(kCustomKeys[0])); + ls->Remove(ref new Platform::String(kCustomKeys[1])); + + SchemeSelector->SelectedIndex = 0; + ShapeSelector->SelectedIndex = 0; + if (Color0 != nullptr) Color0->SelectColor(kClassicUI[0], false); + if (Color1 != nullptr) Color1->SelectColor(kClassicUI[1], false); + CustomPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} diff --git a/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml.h b/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml.h new file mode 100644 index 00000000..3f96188e --- /dev/null +++ b/UI/Backgrounds/Spheres/SpheresSettingsControl.xaml.h @@ -0,0 +1,24 @@ +#pragma once +#include "UI\Backgrounds\Spheres\SpheresSettingsControl.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +namespace moonlight_xbox_dx { + +public ref class SpheresSettingsControl sealed { +public: + SpheresSettingsControl(); + void Initialize(DynamicBackgroundHost^ host); +private: + DynamicBackgroundHost^ m_host; + bool m_initialized = false; + + void SchemeSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void ShapeSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void Color0_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color1_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void ResetButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateCustomPanelVisibility(); +}; + +} diff --git a/UI/Backgrounds/Streaks/StreaksBackground.xaml b/UI/Backgrounds/Streaks/StreaksBackground.xaml new file mode 100644 index 00000000..39fde889 --- /dev/null +++ b/UI/Backgrounds/Streaks/StreaksBackground.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Streaks/StreaksBackground.xaml.cpp b/UI/Backgrounds/Streaks/StreaksBackground.xaml.cpp new file mode 100644 index 00000000..7a3eb8db --- /dev/null +++ b/UI/Backgrounds/Streaks/StreaksBackground.xaml.cpp @@ -0,0 +1,280 @@ +#include "pch.h" +#include "UI\Backgrounds\Streaks\StreaksBackground.xaml.h" +#include + +using namespace moonlight_xbox_dx; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Shapes; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; + +static const float kSqrt2 = 1.41421356f; +static const float kGlowExtend = 5.0f; +static const int kStreakCount = 24; +static const int kGlowBase = 0; +static const int kCoreBase = kStreakCount; + +static const Color kStreakPalette[] = { + { 255, 255, 0, 0 }, + { 255, 0, 60, 255 }, + { 255, 255, 0, 220 }, + { 255, 140, 0, 255 }, + { 255, 0, 220, 255 }, +}; +static const int kStreakColors = 5; + +static bool ParseHex6(Platform::String^ s, Color& out) +{ + if (s == nullptr || s->Length() != 6) return false; + const wchar_t* p = s->Data(); + wchar_t buf[7]; wcsncpy_s(buf, p, 6); buf[6] = L'\0'; + wchar_t* end = nullptr; + unsigned long v = wcstoul(buf, &end, 16); + if (end != buf + 6) return false; + out.A = 255; + out.R = (uint8_t)((v >> 16) & 0xFF); + out.G = (uint8_t)((v >> 8) & 0xFF); + out.B = (uint8_t)( v & 0xFF); + return true; +} + +StreaksBackground::StreaksBackground() +{ + m_rng = std::mt19937(std::random_device{}()); + InitializeComponent(); + this->Loaded += ref new RoutedEventHandler(this, &StreaksBackground::OnLoaded); + StreakCanvas->Background = ref new SolidColorBrush(ColorHelper::FromArgb(255, 3, 3, 15)); + + TimeSpan interval; + interval.Duration = 16 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = interval; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void StreaksBackground::Canvas_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + + if (!m_initialized && m_canvasW > 0 && m_canvasH > 0) { + InitStreaks(); + m_initialized = true; + } +} + +static float ExitT(const StreakState& s, float W, float H) +{ + float txW = 2.0f * W + 2.0f * s.halfLen - s.lane; + float txH = 2.0f * H + 2.0f * s.halfLen + s.lane; + return txW > txH ? txW : txH; +} + +static float EntryT(const StreakState& s) +{ + return fabsf(s.lane) - 2.0f * s.halfLen; +} + +static Rectangle^ MakeStreakRect(float rectW, float rectH, Color col, float opacity) +{ + auto rect = ref new Rectangle(); + rect->Width = rectW; + rect->Height = rectH; + rect->RadiusX = rectH * 0.5; + rect->RadiusY = rectH * 0.5; + rect->Fill = ref new SolidColorBrush(ColorHelper::FromArgb(255, col.R, col.G, col.B)); + rect->Opacity = opacity; + + auto xf = ref new RotateTransform(); + xf->CenterX = rectW * 0.5; + xf->CenterY = rectH * 0.5; + xf->Angle = 45.0; + rect->RenderTransform = xf; + + return rect; +} + +void StreaksBackground::LoadPalette() +{ + using namespace Windows::Storage; + Platform::String^ scheme = L"neon"; + auto ls = ApplicationData::Current->LocalSettings->Values; + if (ls->HasKey("streaks.scheme")) + scheme = safe_cast(ls->Lookup("streaks.scheme")); + + Color schemes[5][5] = { + {{255,255,0,0},{255,0,60,255},{255,255,0,220},{255,140,0,255},{255,0,220,255}}, + {{255,0,80,255},{255,0,200,200},{255,0,229,255},{255,0,184,122},{255,26,58,255}}, + {{255,255,106,0},{255,255,45,120},{255,255,26,26},{255,255,194,0},{255,160,32,240}}, + {{255,255,224,0},{255,255,140,0},{255,255,173,0},{255,255,215,0},{255,255,69,0}}, + {{255,224,224,224},{255,255,255,255},{255,191,191,191},{255,160,160,160},{255,207,207,207}}, + }; + + int schemeIdx = -1; + if (scheme->Equals(L"neon")) schemeIdx = 0; + else if (scheme->Equals(L"ocean")) schemeIdx = 1; + else if (scheme->Equals(L"sunset")) schemeIdx = 2; + else if (scheme->Equals(L"warm")) schemeIdx = 3; + else if (scheme->Equals(L"mono")) schemeIdx = 4; + + if (schemeIdx >= 0) { + for (int i = 0; i < kStreakColors; ++i) m_palette[i] = schemes[schemeIdx][i]; + return; + } + + static const wchar_t* kCustomKeys[] = { + L"streaks.custom.0", L"streaks.custom.1", L"streaks.custom.2", + L"streaks.custom.3", L"streaks.custom.4" + }; + for (int i = 0; i < kStreakColors; ++i) { + Color c = kStreakPalette[i]; + auto key = ref new Platform::String(kCustomKeys[i]); + if (ls->HasKey(key)) { + ParseHex6(safe_cast(ls->Lookup(key)), c); + } + m_palette[i] = c; + } +} + +void StreaksBackground::InitStreaks() +{ + LoadPalette(); + m_streaks.clear(); + m_streaks.reserve(kStreakCount); + StreakCanvas->Children->Clear(); + + float W = m_canvasW, H = m_canvasH; + + std::uniform_real_distribution distLane(-H, W); + std::uniform_real_distribution distSpeed(3.0f, 9.5f); + + for (int i = 0; i < kStreakCount; ++i) { + StreakState s; + s.colorIndex = m_rng() % kStreakColors; + s.lane = distLane(m_rng); + s.halfLen = 120.0f + static_cast(m_rng() % 380); + s.speed = distSpeed(m_rng); + s.glowH = 10.0f + static_cast(m_rng() % 22); + s.coreH = 2.0f + static_cast(m_rng() % 3); + + float tMin = EntryT(s); + float tMax = ExitT(s, W, H); + std::uniform_real_distribution distT(tMin, tMax); + s.t = distT(m_rng); + + m_streaks.push_back(s); + } + + for (int i = 0; i < kStreakCount; ++i) { + const auto& s = m_streaks[i]; + Color col = m_palette[s.colorIndex]; + float glowRectW = 2.0f * (s.halfLen + kGlowExtend) * kSqrt2; + float opacity = 0.55f + static_cast(m_rng() % 45) / 100.0f; + + auto glow = MakeStreakRect(glowRectW, s.glowH, col, opacity); + float cx = (s.t + s.lane) * 0.5f; + float cy = (s.t - s.lane) * 0.5f; + Canvas::SetLeft(glow, cx - glowRectW * 0.5f); + Canvas::SetTop(glow, cy - s.glowH * 0.5f); + StreakCanvas->Children->Append(glow); + } + + for (int i = 0; i < kStreakCount; ++i) { + const auto& s = m_streaks[i]; + Color col = kStreakPalette[s.colorIndex]; + float rectW = 2.0f * s.halfLen * kSqrt2; + float opacity = 0.90f + static_cast(m_rng() % 10) / 100.0f; + + auto core = MakeStreakRect(rectW, s.coreH, col, opacity); + float cx = (s.t + s.lane) * 0.5f; + float cy = (s.t - s.lane) * 0.5f; + Canvas::SetLeft(core, cx - rectW * 0.5f); + Canvas::SetTop(core, cy - s.coreH * 0.5f); + StreakCanvas->Children->Append(core); + } + +} + +void StreaksBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + + float W = m_canvasW, H = m_canvasH; + int count = static_cast(m_streaks.size()); + + for (int i = 0; i < count; ++i) { + auto& s = m_streaks[i]; + s.t += s.speed; + + if (s.t > ExitT(s, W, H)) { + s.t = EntryT(s) - 50.0f - static_cast(m_rng() % 200); + } + + float cx = (s.t + s.lane) * 0.5f; + float cy = (s.t - s.lane) * 0.5f; + float coreRectW = 2.0f * s.halfLen * kSqrt2; + float glowRectW = 2.0f * (s.halfLen + kGlowExtend) * kSqrt2; + + auto glow = safe_cast(StreakCanvas->Children->GetAt(kGlowBase + i)); + Canvas::SetLeft(glow, cx - glowRectW * 0.5f); + Canvas::SetTop(glow, cy - s.glowH * 0.5f); + + auto core = safe_cast(StreakCanvas->Children->GetAt(kCoreBase + i)); + Canvas::SetLeft(core, cx - coreRectW * 0.5f); + Canvas::SetTop(core, cy - s.coreH * 0.5f); + } +} + +void StreaksBackground::OnLoaded(Object^ sender, RoutedEventArgs^ e) +{ + StreakCanvas->Opacity = 0.0; + auto anim = ref new DoubleAnimation(); + anim->From = 0.0; + anim->To = 1.0; + TimeSpan ts; + ts.Duration = 5000000LL; + anim->Duration = Windows::UI::Xaml::Duration(ts); + auto sb = ref new Storyboard(); + Storyboard::SetTarget(anim, StreakCanvas); + Storyboard::SetTargetProperty(anim, "Opacity"); + sb->Children->Append(anim); + sb->Begin(); +} + +void StreaksBackground::ReloadColors() +{ + if (!m_initialized) return; + LoadPalette(); + for (int i = 0; i < kStreakCount; ++i) { + Color col = m_palette[m_streaks[i].colorIndex]; + auto brush = ref new SolidColorBrush(ColorHelper::FromArgb(255, col.R, col.G, col.B)); + safe_cast(StreakCanvas->Children->GetAt(kGlowBase + i))->Fill = brush; + safe_cast(StreakCanvas->Children->GetAt(kCoreBase + i))->Fill = brush; + } +} + +void StreaksBackground::StartAnimations() +{ + if (m_timer != nullptr) m_timer->Start(); +} + +void StreaksBackground::StopAnimations() +{ + if (m_timer != nullptr) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } +} diff --git a/UI/Backgrounds/Streaks/StreaksBackground.xaml.h b/UI/Backgrounds/Streaks/StreaksBackground.xaml.h new file mode 100644 index 00000000..9d4954ef --- /dev/null +++ b/UI/Backgrounds/Streaks/StreaksBackground.xaml.h @@ -0,0 +1,41 @@ +#pragma once +#include "UI\Backgrounds\Streaks\StreaksBackground.g.h" +#include +#include + +namespace moonlight_xbox_dx { + +struct StreakState { + float t; + float lane; + float halfLen; + float speed; + float glowH; + float coreH; + int colorIndex; +}; + +public ref class StreaksBackground sealed { +public: + StreaksBackground(); + void StartAnimations(); + void StopAnimations(); + void ReloadColors(); +private: + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + std::vector m_streaks; + Windows::UI::Color m_palette[5]; + float m_canvasW = 0; + float m_canvasH = 0; + bool m_initialized = false; + std::mt19937 m_rng; + + void Canvas_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void InitStreaks(); + void LoadPalette(); +}; + +} diff --git a/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml b/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml new file mode 100644 index 00000000..8cc9caa3 --- /dev/null +++ b/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml.cpp b/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml.cpp new file mode 100644 index 00000000..2b2d52e1 --- /dev/null +++ b/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml.cpp @@ -0,0 +1,151 @@ +#include "pch.h" +#include "StreaksSettingsControl.xaml.h" +#include "UI\Backgrounds\BackgroundSettingsHelpers.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +static Windows::UI::Color kNeonScheme[] = { {255,255,0,0},{255,0,60,255},{255,255,0,220},{255,140,0,255},{255,0,220,255} }; +static Windows::UI::Color kOceanScheme[] = { {255,0,80,255},{255,0,200,200},{255,0,229,255},{255,0,184,122},{255,26,58,255} }; +static Windows::UI::Color kSunsetScheme[] = { {255,255,106,0},{255,255,45,120},{255,255,26,26},{255,255,194,0},{255,160,32,240} }; +static Windows::UI::Color kWarmScheme[] = { {255,255,224,0},{255,255,140,0},{255,255,173,0},{255,255,215,0},{255,255,69,0} }; +static Windows::UI::Color kMonoScheme[] = { {255,224,224,224},{255,255,255,255},{255,191,191,191},{255,160,160,160},{255,207,207,207} }; + +static Windows::UI::Color kSwatches[] = { + {255,255, 0, 0}, {255,255,106, 0}, {255,255,224, 0}, {255,128,255, 0}, + {255, 0,192, 96}, {255, 0,220,255}, {255, 0, 60,255}, {255,140, 0,255}, + {255,255, 0,220}, {255,255, 45,120}, {255,255,255,255}, {255,128,128,128}, +}; +static const int kSwatchCount = 12; + +static const wchar_t* kCustomKeys[] = { + L"streaks.custom.0", L"streaks.custom.1", L"streaks.custom.2", + L"streaks.custom.3", L"streaks.custom.4" +}; + +StreaksSettingsControl::StreaksSettingsControl() +{ + InitializeComponent(); +} + +void StreaksSettingsControl::Initialize(DynamicBackgroundHost^ host) +{ + m_host = host; + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + Platform::String^ scheme = L"neon"; + if (ls->HasKey("streaks.scheme")) + scheme = safe_cast(ls->Lookup("streaks.scheme")); + + struct { const wchar_t* key; const wchar_t* label; } schemes[] = { + { L"neon", L"Neon (Default)" }, + { L"ocean", L"Ocean" }, + { L"sunset", L"Sunset" }, + { L"warm", L"Warm" }, + { L"mono", L"Monochrome" }, + { L"custom", L"Custom" }, + }; + + int selectedIdx = 0; + for (int i = 0; i < 6; ++i) { + auto item = ref new ComboBoxItem(); + item->Content = ref new Platform::String(schemes[i].label); + item->DataContext = ref new Platform::String(schemes[i].key); + SchemeSelector->Items->Append(item); + if (scheme->Equals(ref new Platform::String(schemes[i].key))) selectedIdx = i; + } + SchemeSelector->SelectedIndex = selectedIdx; + UpdateCustomPanelVisibility(); + + Windows::UI::Color* defaults = kNeonScheme; + if (scheme->Equals(L"ocean")) defaults = kOceanScheme; + else if (scheme->Equals(L"sunset")) defaults = kSunsetScheme; + else if (scheme->Equals(L"warm")) defaults = kWarmScheme; + else if (scheme->Equals(L"mono")) defaults = kMonoScheme; + + SwatchPicker^ pickers[] = { Color0, Color1, Color2, Color3, Color4 }; + for (int i = 0; i < 5; ++i) { + if (pickers[i] == nullptr) continue; + pickers[i]->SetSwatches(kSwatches, kSwatchCount); + Windows::UI::Color c = defaults[i]; + if (scheme->Equals(L"custom")) { + auto k = ref new Platform::String(kCustomKeys[i]); + if (ls->HasKey(k)) c = BgHexToColor(safe_cast(ls->Lookup(k)), c); + } + pickers[i]->SelectColor(c, false); + } + + Color0->ColorChanged += ref new SwatchColorChangedHandler(this, &StreaksSettingsControl::Color0_ColorChanged); + Color1->ColorChanged += ref new SwatchColorChangedHandler(this, &StreaksSettingsControl::Color1_ColorChanged); + Color2->ColorChanged += ref new SwatchColorChangedHandler(this, &StreaksSettingsControl::Color2_ColorChanged); + Color3->ColorChanged += ref new SwatchColorChangedHandler(this, &StreaksSettingsControl::Color3_ColorChanged); + Color4->ColorChanged += ref new SwatchColorChangedHandler(this, &StreaksSettingsControl::Color4_ColorChanged); + + m_initialized = true; +} + +void StreaksSettingsControl::UpdateCustomPanelVisibility() +{ + if (CustomPanel == nullptr || SchemeSelector == nullptr) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + bool isCustom = (item != nullptr && item->DataContext != nullptr && + item->DataContext->ToString()->Equals(L"custom")); + CustomPanel->Visibility = isCustom ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; +} + +void StreaksSettingsControl::SchemeSelector_SelectionChanged(Platform::Object^, SelectionChangedEventArgs^) +{ + if (!m_initialized) return; + auto item = dynamic_cast(SchemeSelector->SelectedItem); + if (item == nullptr) return; + auto key = item->DataContext->ToString(); + + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert("streaks.scheme", key); + + if (!key->Equals(L"custom")) { + Windows::UI::Color* defaults = kNeonScheme; + if (key->Equals(L"ocean")) defaults = kOceanScheme; + else if (key->Equals(L"sunset")) defaults = kSunsetScheme; + else if (key->Equals(L"warm")) defaults = kWarmScheme; + else if (key->Equals(L"mono")) defaults = kMonoScheme; + SwatchPicker^ pickers[] = { Color0, Color1, Color2, Color3, Color4 }; + for (int i = 0; i < 5; ++i) { + if (pickers[i] != nullptr) pickers[i]->SelectColor(defaults[i], false); + } + } + + UpdateCustomPanelVisibility(); + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} + +static void SaveColor(int slot, Windows::UI::Color color) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Insert(ref new Platform::String(kCustomKeys[slot]), BgColorToHex(color)); +} + +void StreaksSettingsControl::Color0_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(0, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void StreaksSettingsControl::Color1_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(1, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void StreaksSettingsControl::Color2_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(2, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void StreaksSettingsControl::Color3_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(3, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } +void StreaksSettingsControl::Color4_ColorChanged(Platform::Object^, Windows::UI::Color color, bool) { SaveColor(4, color); try { if (m_host) m_host->ReloadBackgroundColors(); } catch (...) {} } + +void StreaksSettingsControl::ResetButton_Click(Platform::Object^, RoutedEventArgs^) +{ + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + ls->Remove("streaks.scheme"); + for (int i = 0; i < 5; ++i) + ls->Remove(ref new Platform::String(kCustomKeys[i])); + + SchemeSelector->SelectedIndex = 0; + SwatchPicker^ pickers[] = { Color0, Color1, Color2, Color3, Color4 }; + for (int i = 0; i < 5; ++i) { + if (pickers[i] != nullptr) pickers[i]->SelectColor(kNeonScheme[i], false); + } + CustomPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + + try { if (m_host != nullptr) m_host->ReloadBackgroundColors(); } catch (...) {} +} diff --git a/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml.h b/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml.h new file mode 100644 index 00000000..409d15c6 --- /dev/null +++ b/UI/Backgrounds/Streaks/StreaksSettingsControl.xaml.h @@ -0,0 +1,26 @@ +#pragma once +#include "UI\Backgrounds\Streaks\StreaksSettingsControl.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +namespace moonlight_xbox_dx { + +public ref class StreaksSettingsControl sealed { +public: + StreaksSettingsControl(); + void Initialize(DynamicBackgroundHost^ host); +private: + DynamicBackgroundHost^ m_host; + bool m_initialized = false; + + void SchemeSelector_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void Color0_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color1_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color2_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color3_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void Color4_ColorChanged(Platform::Object^ sender, Windows::UI::Color color, bool useSystemAccent); + void ResetButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateCustomPanelVisibility(); +}; + +} diff --git a/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml b/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml new file mode 100644 index 00000000..f14a23c4 --- /dev/null +++ b/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml.cpp b/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml.cpp new file mode 100644 index 00000000..7637756b --- /dev/null +++ b/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml.cpp @@ -0,0 +1,444 @@ +#include "pch.h" +#include "UI\Backgrounds\SwipeReveal\SwipeRevealBackground.xaml.h" +#include +#include +#include +#include + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Platform::Collections; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Shapes; +using namespace concurrency; + +static const int kHoldTicks = 240; +static const int kWipeTicks = 100; +static const float kGlassWidth = 64.0f; +static const float kPanMax = 0.04f; +static const float kSlantH = 180.0f; + +static const float kWipeMargin = kGlassWidth * 0.5f + kSlantH * 0.5f; +static const int kLoadRetry = 500; +static const int kIntroBufferTicks = 30; + +static float EaseInOut(float t) +{ + + return t < 0.5f ? 4.0f * t * t * t + : 1.0f - powf(-2.0f * t + 2.0f, 3.0f) * 0.5f; +} +static const float kPi = 3.14159265f; + +SwipeRevealBackground::SwipeRevealBackground() +{ + InitializeComponent(); + + m_frontPan = ref new CompositeTransform(); + m_frontPan->ScaleX = 1.1; + m_frontPan->ScaleY = 1.1; + m_frontPan->CenterX = 0.5; + m_frontPan->CenterY = 0.5; + + m_frontBrush = ref new ImageBrush(); + m_frontBrush->Stretch = Stretch::UniformToFill; + m_frontBrush->RelativeTransform = m_frontPan; + + m_frontClipRect = ref new RectangleGeometry(); + FrontGrid->Clip = m_frontClipRect; + + m_frontGridSkew = ref new SkewTransform(); + FrontGrid->RenderTransform = m_frontGridSkew; + + m_frontRectInvSkew = ref new SkewTransform(); + + auto frontRect = ref new Rectangle(); + frontRect->Fill = m_frontBrush; + frontRect->RenderTransform = m_frontRectInvSkew; + FrontGrid->Children->Append(frontRect); + + TimeSpan ts; + ts.Duration = 16 * 10000LL; + m_timer = ref new DispatcherTimer(); + m_timer->Interval = ts; + Platform::WeakReference weakSelf(this); + m_tickToken = m_timer->Tick += ref new EventHandler( + [weakSelf](Object^, Object^) { + try { + auto self = weakSelf.Resolve(); + if (self) self->OnTick(nullptr, nullptr); + } catch (Platform::DisconnectedException^) {} + catch (...) {} + }); +} + +void SwipeRevealBackground::SetHosts(IVector^ hosts) +{ + m_hosts = hosts; + m_appsLoaded = false; + m_apps = nullptr; + m_frontAppIdx = -1; + m_backAppIdx = -1; + m_introTick = 0; +} + +void SwipeRevealBackground::Grid_SizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + m_canvasW = static_cast(e->NewSize.Width); + m_canvasH = static_cast(e->NewSize.Height); + m_initialized = (m_canvasW > 0 && m_canvasH > 0); + if (m_initialized) { + GlassEdgeSkew->CenterY = m_canvasH * 0.5f; + m_frontGridSkew->CenterY = m_canvasH * 0.5f; + m_frontRectInvSkew->CenterY = m_canvasH * 0.5f; + + auto r = m_frontClipRect->Rect; + m_frontClipRect->Rect = Rect(r.X, 0.0f, r.Width, m_canvasH); + UpdateGlassEdgeSkew(); + } +} + +void SwipeRevealBackground::ZeroFrontClips() +{ + m_frontClipRect->Rect = Rect(0.0f, 0.0f, 0.0f, m_canvasH > 0.0f ? m_canvasH : 1.0f); +} + +void SwipeRevealBackground::UpdateGlassEdgeSkew() +{ + if (m_canvasH <= 0) return; + + float angle = atanf(kSlantH / m_canvasH) * 180.0f / kPi; + float gridAngle = (m_wipeDir > 0) ? -angle : angle; + GlassEdgeSkew->AngleX = gridAngle; + m_frontGridSkew->AngleX = gridAngle; + m_frontRectInvSkew->AngleX = -gridAngle; +} + +void SwipeRevealBackground::ShuffleAndApply(Platform::Collections::Vector^ collected) +{ + std::vector vec; + vec.reserve(collected->Size); + for (auto a : collected) vec.push_back(a); + std::mt19937 rng(std::random_device{}()); + std::shuffle(vec.begin(), vec.end(), rng); + auto shuffled = ref new Platform::Collections::Vector(); + for (auto a : vec) shuffled->Append(a); + m_apps = shuffled; + m_appsLoaded = true; + if (m_introTick >= kIntroBufferTicks) + InitSlidesWithWipe(); + else + InitSlides(); +} + +void SwipeRevealBackground::LoadAppsAsync() +{ + if (m_hosts == nullptr) return; + + { + auto inMemory = ref new Vector(); + for (auto h : m_hosts) + if (h->Paired) + for (auto a : h->Apps) inMemory->Append(a); + if (inMemory->Size > 0) { + ShuffleAndApply(inMemory); + + auto targets = ref new Vector(); + for (auto h : m_hosts) + if (h->Paired && h->Connected) targets->Append(h); + if (targets->Size > 0) + create_task([targets]() { + for (auto h : targets) + try { h->UpdateApps(); } catch (...) {} + }); + return; + } + } + + Platform::WeakReference weakThis(this); + Platform::String^ baseImages = Windows::Storage::ApplicationData::Current->LocalFolder->Path; + baseImages = Platform::String::Concat(baseImages, L"\\images\\"); + + auto hostDirs = std::make_shared>(); + for (auto h : m_hosts) { + if (h->InstanceId != nullptr && !h->InstanceId->IsEmpty()) + hostDirs->push_back(std::wstring(Platform::String::Concat(baseImages, + Platform::String::Concat(h->InstanceId, L"\\"))->Data())); + } + + if (hostDirs->empty()) { + LoadFromNetworkAsync(); + return; + } + + auto imagePaths = std::make_shared>(); + create_task([hostDirs, imagePaths]() { + for (auto& dir : *hostDirs) { + std::wstring search = dir + L"*.png"; + WIN32_FIND_DATAW fd; + HANDLE h = FindFirstFileW(search.c_str(), &fd); + if (h == INVALID_HANDLE_VALUE) continue; + do { + imagePaths->push_back(dir + fd.cFileName); + } while (FindNextFileW(h, &fd)); + FindClose(h); + } + }).then([weakThis, imagePaths]() { + auto dispatcher = Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher; + dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Low, + ref new Windows::UI::Core::DispatchedHandler([weakThis, imagePaths]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + + if (!imagePaths->empty()) { + auto fromDisk = ref new Platform::Collections::Vector(); + for (auto& path : *imagePaths) { + auto app = ref new MoonlightApp(); + app->ImagePath = ref new Platform::String(path.c_str()); + fromDisk->Append(app); + } + that->ShuffleAndApply(fromDisk); + + if (that->m_hosts != nullptr) { + auto targets = ref new Platform::Collections::Vector(); + for (auto h : that->m_hosts) + if (h->Paired && h->Connected) targets->Append(h); + if (targets->Size > 0) + create_task([targets]() { + for (auto h : targets) + try { h->UpdateApps(); } catch (...) {} + }); + } + return; + } + + that->LoadFromNetworkAsync(); + })); + }); +} + +void SwipeRevealBackground::LoadFromNetworkAsync() +{ + if (m_hosts == nullptr) return; + auto targets = ref new Vector(); + for (auto h : m_hosts) + if (h->Paired && h->Connected) targets->Append(h); + if (targets->Size == 0) return; + + Platform::WeakReference weakThis(this); + create_task([targets]() { + for (auto h : targets) + try { h->UpdateApps(); } catch (...) {} + }).then([weakThis, targets]() { + auto dispatcher = Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher; + dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Low, + ref new Windows::UI::Core::DispatchedHandler([weakThis, targets]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + auto collected = ref new Vector(); + for (auto h : targets) + for (auto a : h->Apps) collected->Append(a); + if (collected->Size > 0) + that->ShuffleAndApply(collected); + })); + }); +} + +int SwipeRevealBackground::FindNextAppWithImage(int startIdx) +{ + if (!m_appsLoaded || m_apps == nullptr || m_apps->Size == 0) return -1; + int sz = static_cast(m_apps->Size); + startIdx = ((startIdx % sz) + sz) % sz; + for (int i = 0; i < sz; i++) { + int idx = (startIdx + i) % sz; + if (m_apps->GetAt(idx)->Image != nullptr) return idx; + } + return -1; +} + +void SwipeRevealBackground::InitPanForLayer(float& px, float& py, float& vx, float& vy) +{ + static const float dirX[] = { 1, -1, 1, -1 }; + static const float dirY[] = { 1, -1, -1, 1 }; + int d = (m_panDirIdx++) % 4; + float sx = dirX[d], sy = dirY[d]; + float speed = 2.0f * kPanMax / static_cast(kHoldTicks + kWipeTicks); + px = -sx * kPanMax; + py = -sy * kPanMax; + vx = sx * speed; + vy = sy * speed; +} + +void SwipeRevealBackground::AdvancePan(float& px, float& py, float vx, float vy, CompositeTransform^ xf) +{ + px += vx; + py += vy; + xf->TranslateX = px; + xf->TranslateY = py; +} + +void SwipeRevealBackground::UpdateDiagonalClip(float swept) +{ + if (m_wipeDir > 0) { + + float clipW = swept + kSlantH * 0.5f; + if (clipW <= 0.0f) + m_frontClipRect->Rect = Rect(0.0f, 0.0f, 0.0f, m_canvasH); + else + m_frontClipRect->Rect = Rect(-kSlantH * 0.5f, 0.0f, clipW, m_canvasH); + } else { + + float clipW = swept + kSlantH * 0.5f; + if (clipW <= 0.0f) + m_frontClipRect->Rect = Rect(m_canvasW, 0.0f, 0.0f, m_canvasH); + else + m_frontClipRect->Rect = Rect(m_canvasW - swept, 0.0f, clipW, m_canvasH); + } +} + +void SwipeRevealBackground::InitSlides() +{ + + m_backAppIdx = FindNextAppWithImage(0); + if (m_backAppIdx < 0) return; + + BackBrush->ImageSource = m_apps->GetAt(m_backAppIdx)->Image; + + m_frontAppIdx = FindNextAppWithImage(m_backAppIdx + 1); + m_frontBrush->ImageSource = (m_frontAppIdx >= 0) + ? m_apps->GetAt(m_frontAppIdx)->Image : nullptr; + + InitPanForLayer(m_backPanX, m_backPanY, m_backVX, m_backVY); + InitPanForLayer(m_frontPanX, m_frontPanY, m_frontVX, m_frontVY); + BackPan->TranslateX = m_backPanX; BackPan->TranslateY = m_backPanY; + m_frontPan->TranslateX = m_frontPanX; m_frontPan->TranslateY = m_frontPanY; + + m_wipeDir = 1; + m_wipeTick = 0; + m_phase = 0; + m_holdTick = 0; + GlassEdge->Opacity = 0.0; + ZeroFrontClips(); + UpdateGlassEdgeSkew(); +} + +void SwipeRevealBackground::InitSlidesWithWipe() +{ + + m_frontAppIdx = FindNextAppWithImage(0); + if (m_frontAppIdx < 0) { InitSlides(); return; } + + BackBrush->ImageSource = nullptr; + m_backAppIdx = -1; + m_frontBrush->ImageSource = m_apps->GetAt(m_frontAppIdx)->Image; + + InitPanForLayer(m_backPanX, m_backPanY, m_backVX, m_backVY); + InitPanForLayer(m_frontPanX, m_frontPanY, m_frontVX, m_frontVY); + BackPan->TranslateX = m_backPanX; BackPan->TranslateY = m_backPanY; + m_frontPan->TranslateX = m_frontPanX; m_frontPan->TranslateY = m_frontPanY; + + m_wipeDir = 1; + m_wipeTick = 0; + m_phase = 1; + m_holdTick = 0; + GlassEdge->Opacity = 1.0; + ZeroFrontClips(); + UpdateGlassEdgeSkew(); +} + +void SwipeRevealBackground::AdvanceSlide() +{ + + BackBrush->ImageSource = m_frontBrush->ImageSource; + BackPan->TranslateX = m_frontPan->TranslateX; + BackPan->TranslateY = m_frontPan->TranslateY; + m_backPanX = m_frontPanX; m_backPanY = m_frontPanY; + m_backVX = m_frontVX; m_backVY = m_frontVY; + m_backAppIdx = m_frontAppIdx; + + m_frontAppIdx = FindNextAppWithImage(m_backAppIdx + 1); + if (m_frontAppIdx >= 0) + m_frontBrush->ImageSource = m_apps->GetAt(m_frontAppIdx)->Image; + + InitPanForLayer(m_frontPanX, m_frontPanY, m_frontVX, m_frontVY); + m_frontPan->TranslateX = m_frontPanX; + m_frontPan->TranslateY = m_frontPanY; + + m_wipeDir = -m_wipeDir; + m_wipeTick = 0; + m_phase = 0; + m_holdTick = 0; + GlassEdge->Opacity = 0.0; + ZeroFrontClips(); + UpdateGlassEdgeSkew(); +} + +void SwipeRevealBackground::OnTick(Object^ sender, Object^ args) +{ + if (!m_initialized) return; + + m_introTick++; + + if (!m_appsLoaded) { + if (++m_loadRetryTick >= kLoadRetry) { + m_loadRetryTick = 0; + LoadAppsAsync(); + } + return; + } + + if (m_frontAppIdx < 0) { + if (++m_imageRetryTick >= 60) { + m_imageRetryTick = 0; + if (m_introTick >= kIntroBufferTicks) + InitSlidesWithWipe(); + else + InitSlides(); + } + return; + } + + AdvancePan(m_backPanX, m_backPanY, m_backVX, m_backVY, BackPan); + + if (m_phase == 0) { + if (++m_holdTick >= kHoldTicks) { + m_phase = 1; + m_wipeTick = 0; + GlassEdge->Opacity = 1.0; + } + } else { + AdvancePan(m_frontPanX, m_frontPanY, m_frontVX, m_frontVY, m_frontPan); + + m_wipeTick++; + float t = std::min(static_cast(m_wipeTick) / static_cast(kWipeTicks), 1.0f); + + float swept = EaseInOut(t) * (m_canvasW + 2.0f * kWipeMargin) - kWipeMargin; + + UpdateDiagonalClip(swept); + + float cx = (m_wipeDir > 0) ? swept : (m_canvasW - swept); + GlassEdgeTranslate->X = cx - kGlassWidth * 0.5f; + + if (m_wipeTick >= kWipeTicks) + AdvanceSlide(); + } +} + +void SwipeRevealBackground::StartAnimations() +{ + LoadAppsAsync(); + if (m_timer) m_timer->Start(); +} + +void SwipeRevealBackground::StopAnimations() +{ + if (m_timer) { + m_timer->Stop(); + m_timer->Tick -= m_tickToken; + } + GlassEdge->Opacity = 0.0; +} diff --git a/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml.h b/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml.h new file mode 100644 index 00000000..17c4c372 --- /dev/null +++ b/UI/Backgrounds/SwipeReveal/SwipeRevealBackground.xaml.h @@ -0,0 +1,64 @@ +#pragma once +#include "UI\Backgrounds\SwipeReveal\SwipeRevealBackground.g.h" +#include "State\MoonlightHost.h" +#include + +namespace moonlight_xbox_dx { + +public ref class SwipeRevealBackground sealed { +public: + SwipeRevealBackground(); + void StartAnimations(); + void StopAnimations(); + void SetHosts(Windows::Foundation::Collections::IVector^ hosts); +private: + Windows::UI::Xaml::DispatcherTimer^ m_timer; + Windows::Foundation::EventRegistrationToken m_tickToken; + Windows::Foundation::Collections::IVector^ m_hosts; + Platform::Collections::Vector^ m_apps; + + Windows::UI::Xaml::Media::ImageBrush^ m_frontBrush; + Windows::UI::Xaml::Media::CompositeTransform^ m_frontPan; + Windows::UI::Xaml::Media::RectangleGeometry^ m_frontClipRect; + Windows::UI::Xaml::Media::SkewTransform^ m_frontGridSkew; + Windows::UI::Xaml::Media::SkewTransform^ m_frontRectInvSkew; + + float m_canvasW = 0.0f; + float m_canvasH = 0.0f; + bool m_initialized = false; + bool m_appsLoaded = false; + + int m_phase = 0; + int m_holdTick = 0; + int m_wipeTick = 0; + int m_wipeDir = 1; + int m_loadRetryTick = 0; + int m_imageRetryTick = 0; + int m_introTick = 0; + int m_backAppIdx = -1; + int m_frontAppIdx = -1; + int m_panDirIdx = 0; + + float m_backPanX = 0.0f, m_backPanY = 0.0f; + float m_backVX = 0.0f, m_backVY = 0.0f; + float m_frontPanX = 0.0f, m_frontPanY = 0.0f; + float m_frontVX = 0.0f, m_frontVY = 0.0f; + + void Grid_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void OnTick(Platform::Object^ sender, Platform::Object^ args); + void LoadAppsAsync(); + void LoadFromNetworkAsync(); + void ShuffleAndApply(Platform::Collections::Vector^ apps); + void InitSlides(); + void InitSlidesWithWipe(); + void AdvanceSlide(); + int FindNextAppWithImage(int startIdx); + void InitPanForLayer(float& px, float& py, float& vx, float& vy); + void AdvancePan(float& px, float& py, float vx, float vy, + Windows::UI::Xaml::Media::CompositeTransform^ xf); + void UpdateDiagonalClip(float swept); + void ZeroFrontClips(); + void UpdateGlassEdgeSkew(); +}; + +} diff --git a/UI/Controls/AspectRatioBox.xaml b/UI/Controls/AspectRatioBox.xaml new file mode 100644 index 00000000..72719856 --- /dev/null +++ b/UI/Controls/AspectRatioBox.xaml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/UI/Controls/AspectRatioBox.xaml.cpp b/UI/Controls/AspectRatioBox.xaml.cpp new file mode 100644 index 00000000..ba15bce5 --- /dev/null +++ b/UI/Controls/AspectRatioBox.xaml.cpp @@ -0,0 +1,85 @@ +#include "pch.h" +#include "UI\Controls\AspectRatioBox.xaml.h" +#include "Utils.hpp" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::Foundation; +using namespace Windows::UI::Xaml::Controls; + +Windows::UI::Xaml::DependencyProperty^ AspectRatioBox::m_ratioProperty = + Windows::UI::Xaml::DependencyProperty::Register( + "Ratio", + double::typeid, + AspectRatioBox::typeid, + ref new Windows::UI::Xaml::PropertyMetadata(1.0) + ); + +AspectRatioBox::AspectRatioBox() { + InitializeComponent(); +} + +double AspectRatioBox::Ratio::get() { + return (double)this->GetValue(AspectRatioBox::m_ratioProperty); +} + +void AspectRatioBox::Ratio::set(double v) { + this->SetValue(AspectRatioBox::m_ratioProperty, v); +} + +Windows::Foundation::Size AspectRatioBox::MeasureOverride(Windows::Foundation::Size availableSize) { + + double ratio = this->Ratio; + double width = availableSize.Width; + double height = availableSize.Height; + + if (isnan(width) || isnan(height) || (width == std::numeric_limits::infinity() && height == std::numeric_limits::infinity())) { + return Windows::Foundation::Size(100 * ratio, 100); + } + + if (!(height == std::numeric_limits::infinity())) { + width = height * ratio; + } else if (!(width == std::numeric_limits::infinity())) { + + height = width / ratio; + } + + auto content = this->Content; + if (content != nullptr) { + auto fe = dynamic_cast(content); + if (fe != nullptr) { + fe->Measure(Size(width, height)); + } + } + + return Size(width, height); +} + +Windows::Foundation::Size AspectRatioBox::ArrangeOverride(Windows::Foundation::Size finalSize) { + + double ratio = this->Ratio; + + double targetWidth = finalSize.Width; + double targetHeight = finalSize.Height; + + double idealWidth = targetHeight * ratio; + double idealHeight = targetHeight; + if (idealWidth > targetWidth) { + idealWidth = targetWidth; + idealHeight = targetWidth / ratio; + } + + double offsetX = (targetWidth - idealWidth) / 2.0; + double offsetY = (targetHeight - idealHeight) / 2.0; + + auto content = this->Content; + if (content != nullptr) { + auto fe = dynamic_cast(content); + if (fe != nullptr) { + fe->Arrange(Rect(offsetX, offsetY, idealWidth, idealHeight)); + } + } + + return finalSize; +} diff --git a/UI/Controls/AspectRatioBox.xaml.h b/UI/Controls/AspectRatioBox.xaml.h new file mode 100644 index 00000000..ed3692bc --- /dev/null +++ b/UI/Controls/AspectRatioBox.xaml.h @@ -0,0 +1,29 @@ +#pragma once + +#include "UI\Controls\AspectRatioBox.g.h" + +namespace moonlight_xbox_dx { + + [Windows::UI::Xaml::Data::Bindable] + public ref class AspectRatioBox sealed + { + public: + AspectRatioBox(); + + static property Windows::UI::Xaml::DependencyProperty^ RatioProperty { + Windows::UI::Xaml::DependencyProperty^ get() { return m_ratioProperty; } + } + + property double Ratio { + double get(); + void set(double v); + } + + protected: + virtual Windows::Foundation::Size MeasureOverride(Windows::Foundation::Size availableSize) override; + virtual Windows::Foundation::Size ArrangeOverride(Windows::Foundation::Size finalSize) override; + + private: + static Windows::UI::Xaml::DependencyProperty^ m_ratioProperty; + }; +} diff --git a/UI/Controls/LunarPhaseControl.xaml b/UI/Controls/LunarPhaseControl.xaml new file mode 100644 index 00000000..7f342b55 --- /dev/null +++ b/UI/Controls/LunarPhaseControl.xaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Controls/LunarPhaseControl.xaml.cpp b/UI/Controls/LunarPhaseControl.xaml.cpp new file mode 100644 index 00000000..c2625ee8 --- /dev/null +++ b/UI/Controls/LunarPhaseControl.xaml.cpp @@ -0,0 +1,424 @@ +#include "pch.h" +#include "UI\Controls\LunarPhaseControl.xaml.h" +#define MLOG_TAG_OVERRIDE "LunarPhaseControl" +#include "..\..\Utils.hpp" +#include + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; +using namespace Windows::UI::Xaml::Media::Imaging; +using namespace Windows::Foundation; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace concurrency; + +static constexpr long long kAnimationMs = 150; +static constexpr double kShadowTravelPx = 100.0; +static constexpr long long kOrbitLoopMs = 2040; + +static std::atomic s_orbitLoopMs { kOrbitLoopMs }; +static std::once_flag s_orbitDurationFlag; + +static void ReadOrbitGifDurationAsync() +{ + std::call_once(s_orbitDurationFlag, []() { + auto uri = ref new Uri(L"ms-appx:///Assets/orbit_load3.gif"); + create_task(StorageFile::GetFileFromApplicationUriAsync(uri)) + .then([](task fileTask) -> task { + try { + return create_task(FileIO::ReadBufferAsync(fileTask.get())); + } catch (Platform::COMException^ ex) { + MLOGF(Utils::LogLevel::Warning, "failed to open orbit gif hr=0x%08x — using fallback %lldms\n", ex->HResult, kOrbitLoopMs); + return task_from_result(nullptr); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "failed to open orbit gif (unknown) — using fallback %lldms\n", kOrbitLoopMs); + return task_from_result(nullptr); + } + }) + .then([](IBuffer^ buf) -> long long { + if (buf == nullptr) return kOrbitLoopMs; + + auto reader = DataReader::FromBuffer(buf); + unsigned int len = buf->Length; + auto bytes = ref new Array(len); + reader->ReadBytes(bytes); + + int frameCount = 0; + long long totalMs = 0; + for (unsigned int i = 0; i + 5 < len; i++) { + if (bytes[i] == 0x21 && bytes[i+1] == 0xF9 && bytes[i+2] == 0x04) { + unsigned short cs = bytes[i+4] | (unsigned short)(bytes[i+5] << 8); + totalMs += (long long)cs * 10; + frameCount++; + i += 7; + } + } + + if (totalMs > 0) { + return totalMs; + } + MLOGF(Utils::LogLevel::Warning, "orbit gif parse found no GCE frames (len=%u) — using fallback %lldms\n", len, kOrbitLoopMs); + return kOrbitLoopMs; + }) + .then([](task t) { + try { s_orbitLoopMs = t.get(); } catch (...) {} + }); + }); +} + +Windows::UI::Xaml::DependencyProperty^ LunarPhaseControl::m_showOrbitProperty = + DependencyProperty::Register( + "ShowOrbit", bool::typeid, LunarPhaseControl::typeid, + ref new PropertyMetadata(false, + ref new PropertyChangedCallback(&LunarPhaseControl::OnShowOrbitChanged))); + +Windows::UI::Xaml::DependencyProperty^ LunarPhaseControl::m_showLockProperty = + DependencyProperty::Register( + "ShowLock", Windows::UI::Xaml::Visibility::typeid, LunarPhaseControl::typeid, + ref new PropertyMetadata(Windows::UI::Xaml::Visibility::Collapsed, + ref new PropertyChangedCallback(&LunarPhaseControl::OnShowLockChanged))); + +Windows::UI::Xaml::DependencyProperty^ LunarPhaseControl::m_showDisconnectedProperty = + DependencyProperty::Register( + "ShowDisconnected", Windows::UI::Xaml::Visibility::typeid, LunarPhaseControl::typeid, + ref new PropertyMetadata(Windows::UI::Xaml::Visibility::Collapsed, + ref new PropertyChangedCallback(&LunarPhaseControl::OnShowDisconnectedChanged))); + +Windows::UI::Xaml::DependencyProperty^ LunarPhaseControl::m_shadowCenterXProperty = + DependencyProperty::Register( + "ShadowCenterX", double::typeid, LunarPhaseControl::typeid, + ref new PropertyMetadata(80.0, + ref new PropertyChangedCallback(&LunarPhaseControl::OnShadowCenterXChanged))); + +Windows::UI::Xaml::DependencyProperty^ LunarPhaseControl::m_isDashedProperty = + DependencyProperty::Register( + "IsDashed", bool::typeid, LunarPhaseControl::typeid, + ref new PropertyMetadata(false, + ref new PropertyChangedCallback(&LunarPhaseControl::OnIsDashedChanged))); + +LunarPhaseControl::LunarPhaseControl() +{ + InitializeComponent(); + m_phaseStoryboard = nullptr; + m_selectionStoryboard = nullptr; + m_orbitHideTimer = nullptr; + m_orbitShownTick = 0; + ReadOrbitGifDurationAsync(); + + m_outerArc = ref new ArcSegment(); + m_outerArc->Size = Size(76.0f, 76.0f); + m_outerArc->IsLargeArc = true; + m_outerArc->SweepDirection = SweepDirection::Clockwise; + + m_innerArc = ref new ArcSegment(); + m_innerArc->Size = Size(76.0f, 76.0f); + m_innerArc->IsLargeArc = false; + m_innerArc->SweepDirection = SweepDirection::Counterclockwise; + + m_crescentFigure = ref new PathFigure(); + m_crescentFigure->IsClosed = true; + m_crescentFigure->IsFilled = true; + m_crescentFigure->Segments->Append(m_outerArc); + m_crescentFigure->Segments->Append(m_innerArc); + + auto pathGeo = ref new PathGeometry(); + pathGeo->Figures->Append(m_crescentFigure); + + CrescentPath->Data = pathGeo; + + m_dashedOuterArc = ref new ArcSegment(); + m_dashedOuterArc->Size = Size(76.0f, 76.0f); + m_dashedOuterArc->IsLargeArc = true; + m_dashedOuterArc->SweepDirection = SweepDirection::Clockwise; + + m_dashedInnerArc = ref new ArcSegment(); + m_dashedInnerArc->Size = Size(76.0f, 76.0f); + m_dashedInnerArc->IsLargeArc = false; + m_dashedInnerArc->SweepDirection = SweepDirection::Counterclockwise; + + m_dashedCrescentFigure = ref new PathFigure(); + m_dashedCrescentFigure->IsClosed = true; + m_dashedCrescentFigure->IsFilled = true; + m_dashedCrescentFigure->Segments->Append(m_dashedOuterArc); + m_dashedCrescentFigure->Segments->Append(m_dashedInnerArc); + + auto dashedPathGeo = ref new PathGeometry(); + dashedPathGeo->Figures->Append(m_dashedCrescentFigure); + DashedCrescentPath->Data = dashedPathGeo; + + CrescentPath->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + DashedCrescentPath->Visibility = Windows::UI::Xaml::Visibility::Collapsed; +} + +bool LunarPhaseControl::ShowOrbit::get() +{ + return (bool)GetValue(m_showOrbitProperty); +} +void LunarPhaseControl::ShowOrbit::set(bool v) +{ + SetValue(m_showOrbitProperty, v); +} + +Windows::UI::Xaml::Visibility LunarPhaseControl::ShowLock::get() +{ + return (Windows::UI::Xaml::Visibility)GetValue(m_showLockProperty); +} +void LunarPhaseControl::ShowLock::set(Windows::UI::Xaml::Visibility v) +{ + SetValue(m_showLockProperty, v); +} + +Windows::UI::Xaml::Visibility LunarPhaseControl::ShowDisconnected::get() +{ + return (Windows::UI::Xaml::Visibility)GetValue(m_showDisconnectedProperty); +} +void LunarPhaseControl::ShowDisconnected::set(Windows::UI::Xaml::Visibility v) +{ + SetValue(m_showDisconnectedProperty, v); +} + +double LunarPhaseControl::ShadowCenterX::get() +{ + return (double)GetValue(m_shadowCenterXProperty); +} +void LunarPhaseControl::ShadowCenterX::set(double v) +{ + SetValue(m_shadowCenterXProperty, v); +} + +void LunarPhaseControl::OnShowOrbitChanged( + DependencyObject^ d, DependencyPropertyChangedEventArgs^ e) +{ + auto ctrl = dynamic_cast(d); + if (ctrl == nullptr) return; + bool show = (bool)e->NewValue; + + if (show) { + if (ctrl->m_orbitHideTimer != nullptr) { + try { ctrl->m_orbitHideTimer->Stop(); } catch (...) {} + ctrl->m_orbitHideTimer = nullptr; + } + + ctrl->OrbitGif->Source = ref new BitmapImage( + ref new Uri(L"ms-appx:///Assets/orbit_load3.gif")); + ctrl->OrbitGif->Visibility = Windows::UI::Xaml::Visibility::Visible; + ctrl->m_orbitShownTick = QpcNow(); + return; + } + + static constexpr long long kTimerMarginMs = 20; + long long loopMs = s_orbitLoopMs.load(); + long long rem; + if (ctrl->m_orbitShownTick == 0) { + rem = loopMs - kTimerMarginMs; + } else { + long long elapsed = (long long)QpcToMs(QpcNow() - ctrl->m_orbitShownTick); + if (elapsed < 0) elapsed = 0; + rem = loopMs - (elapsed % loopMs) - kTimerMarginMs; + if (rem <= 50) { + + ctrl->OrbitGif->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + return; + } + } + + if (ctrl->m_orbitHideTimer != nullptr) { + try { ctrl->m_orbitHideTimer->Stop(); } catch (...) {} + } + + auto timer = ref new Windows::UI::Xaml::DispatcherTimer(); + TimeSpan ts; + ts.Duration = rem * 10000LL; + timer->Interval = ts; + + Platform::WeakReference weakCtrl(ctrl); + timer->Tick += ref new Windows::Foundation::EventHandler( + [weakCtrl](Platform::Object^ sender, Platform::Object^) { + auto that = weakCtrl.Resolve(); + try { dynamic_cast(sender)->Stop(); } catch (...) {} + if (that == nullptr) return; + that->m_orbitHideTimer = nullptr; + that->OrbitGif->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + }); + + ctrl->m_orbitHideTimer = timer; + timer->Start(); +} + +void LunarPhaseControl::OnShowLockChanged( + DependencyObject^ d, DependencyPropertyChangedEventArgs^ e) +{ + auto ctrl = dynamic_cast(d); + if (ctrl == nullptr) return; + ctrl->LockIcon->Visibility = (Windows::UI::Xaml::Visibility)e->NewValue; +} + +void LunarPhaseControl::OnShowDisconnectedChanged( + DependencyObject^ d, DependencyPropertyChangedEventArgs^ e) +{ + auto ctrl = dynamic_cast(d); + if (ctrl == nullptr) return; + ctrl->DisconnectedIcon->Visibility = (Windows::UI::Xaml::Visibility)e->NewValue; +} + +void LunarPhaseControl::OnShadowCenterXChanged( + DependencyObject^ d, DependencyPropertyChangedEventArgs^ e) +{ + auto ctrl = dynamic_cast(d); + if (ctrl == nullptr) return; + ctrl->SetCrescentPath((double)e->NewValue); +} + +bool LunarPhaseControl::IsDashed::get() +{ + return (bool)GetValue(m_isDashedProperty); +} +void LunarPhaseControl::IsDashed::set(bool v) +{ + SetValue(m_isDashedProperty, v); +} + +void LunarPhaseControl::OnIsDashedChanged( + DependencyObject^ d, DependencyPropertyChangedEventArgs^ e) +{ + auto ctrl = dynamic_cast(d); + if (ctrl == nullptr) return; + VisualStateManager::GoToState(ctrl, (bool)e->NewValue ? "Dashed" : "Solid", true); +} + +void LunarPhaseControl::SetCrescentPath(double sx) +{ + const double cx = 80.0, cy = 80.0, r = 76.0; + double d = std::abs(sx - cx); + + if (d < 0.5) { + CrescentPath->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + DashedCrescentPath->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + return; + } + + CrescentPath->Visibility = Windows::UI::Xaml::Visibility::Visible; + DashedCrescentPath->Visibility = Windows::UI::Xaml::Visibility::Visible; + + if (d > 2.0 * r - 0.5) d = 2.0 * r - 0.5; + + sx = (sx < cx) ? cx - d : cx + d; + + double ix = (cx + sx) / 2.0; + double h = std::sqrt(r * r - (d / 2.0) * (d / 2.0)); + double iy1 = cy - h; + double iy2 = cy + h; + + bool crescentOnRight = (sx < cx); + + Point topPt((float)ix, (float)iy1); + Point botPt((float)ix, (float)iy2); + SweepDirection outerSweep = crescentOnRight ? SweepDirection::Clockwise : SweepDirection::Counterclockwise; + SweepDirection innerSweep = crescentOnRight ? SweepDirection::Counterclockwise : SweepDirection::Clockwise; + + m_crescentFigure->StartPoint = topPt; + m_outerArc->Point = botPt; + m_outerArc->IsLargeArc = true; + m_outerArc->SweepDirection = outerSweep; + m_innerArc->Point = topPt; + m_innerArc->IsLargeArc = false; + m_innerArc->SweepDirection = innerSweep; + + m_dashedCrescentFigure->StartPoint = topPt; + m_dashedOuterArc->Point = botPt; + m_dashedOuterArc->IsLargeArc = true; + m_dashedOuterArc->SweepDirection = outerSweep; + m_dashedInnerArc->Point = topPt; + m_dashedInnerArc->IsLargeArc = false; + m_dashedInnerArc->SweepDirection = innerSweep; +} + +void LunarPhaseControl::SetSelected(bool selected, bool animated) +{ + VisualStateManager::GoToState(this, selected ? "Selected" : "Normal", animated); + + double targetWidth = selected ? 160.0 : 96.0; + + double fromWidth = this->Width; + + if (m_selectionStoryboard != nullptr) { + try { m_selectionStoryboard->Stop(); } catch (...) {} + m_selectionStoryboard = nullptr; + } + + if (!animated) { + this->Width = targetWidth; + return; + } + + this->Width = fromWidth; + + auto anim = ref new DoubleAnimation(); + anim->From = fromWidth; + anim->To = targetWidth; + + TimeSpan ts; + ts.Duration = 125LL * 10000LL; + anim->Duration = Windows::UI::Xaml::Duration(ts); + anim->EnableDependentAnimation = true; + + auto sb = ref new Storyboard(); + Storyboard::SetTarget(anim, this); + Storyboard::SetTargetProperty(anim, "Width"); + sb->Children->Append(anim); + + m_selectionStoryboard = sb; + sb->Begin(); +} + +void LunarPhaseControl::UpdatePhase(double fillAmount, int side, bool animated) +{ + + double targetX; + if (side == 0) { + targetX = 80.0; + } else if (side > 0) { + targetX = 80.0 - fillAmount * kShadowTravelPx; + } else { + targetX = 80.0 + fillAmount * kShadowTravelPx; + } + + double fromX = ShadowCenterX; + + if (m_phaseStoryboard != nullptr) { + try { m_phaseStoryboard->Stop(); } catch (...) {} + m_phaseStoryboard = nullptr; + } + + if (!animated) { + ShadowCenterX = targetX; + return; + } + + ShadowCenterX = fromX; + + auto anim = ref new DoubleAnimation(); + anim->From = fromX; + anim->To = targetX; + + TimeSpan ts; + ts.Duration = kAnimationMs * 10000LL; + anim->Duration = Windows::UI::Xaml::Duration(ts); + anim->EnableDependentAnimation = true; + + auto ease = ref new CubicEase(); + ease->EasingMode = EasingMode::EaseOut; + anim->EasingFunction = ease; + + auto sb = ref new Storyboard(); + Storyboard::SetTarget(anim, this); + Storyboard::SetTargetProperty(anim, "ShadowCenterX"); + sb->Children->Append(anim); + + m_phaseStoryboard = sb; + sb->Begin(); +} diff --git a/UI/Controls/LunarPhaseControl.xaml.h b/UI/Controls/LunarPhaseControl.xaml.h new file mode 100644 index 00000000..6acaa0cf --- /dev/null +++ b/UI/Controls/LunarPhaseControl.xaml.h @@ -0,0 +1,92 @@ +#pragma once + +#include "UI\Controls\LunarPhaseControl.g.h" + +namespace moonlight_xbox_dx { + + [Windows::Foundation::Metadata::WebHostHidden] + public ref class LunarPhaseControl sealed + { + public: + LunarPhaseControl(); + + static property Windows::UI::Xaml::DependencyProperty^ ShowOrbitProperty { + Windows::UI::Xaml::DependencyProperty^ get() { return m_showOrbitProperty; } + } + property bool ShowOrbit { + bool get(); + void set(bool v); + } + + static property Windows::UI::Xaml::DependencyProperty^ IsDashedProperty { + Windows::UI::Xaml::DependencyProperty^ get() { return m_isDashedProperty; } + } + property bool IsDashed { + bool get(); + void set(bool v); + } + + static property Windows::UI::Xaml::DependencyProperty^ ShowLockProperty { + Windows::UI::Xaml::DependencyProperty^ get() { return m_showLockProperty; } + } + property Windows::UI::Xaml::Visibility ShowLock { + Windows::UI::Xaml::Visibility get(); + void set(Windows::UI::Xaml::Visibility v); + } + + static property Windows::UI::Xaml::DependencyProperty^ ShowDisconnectedProperty { + Windows::UI::Xaml::DependencyProperty^ get() { return m_showDisconnectedProperty; } + } + property Windows::UI::Xaml::Visibility ShowDisconnected { + Windows::UI::Xaml::Visibility get(); + void set(Windows::UI::Xaml::Visibility v); + } + + static property Windows::UI::Xaml::DependencyProperty^ ShadowCenterXProperty { + Windows::UI::Xaml::DependencyProperty^ get() { return m_shadowCenterXProperty; } + } + property double ShadowCenterX { + double get(); + void set(double v); + } + + void UpdatePhase(double fillAmount, int side, bool animated); + void SetSelected(bool selected, bool animated); + + private: + static Windows::UI::Xaml::DependencyProperty^ m_showOrbitProperty; + static Windows::UI::Xaml::DependencyProperty^ m_showLockProperty; + static Windows::UI::Xaml::DependencyProperty^ m_showDisconnectedProperty; + static Windows::UI::Xaml::DependencyProperty^ m_shadowCenterXProperty; + static Windows::UI::Xaml::DependencyProperty^ m_isDashedProperty; + + static void OnShowOrbitChanged( + Windows::UI::Xaml::DependencyObject^ d, + Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e); + static void OnShowLockChanged( + Windows::UI::Xaml::DependencyObject^ d, + Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e); + static void OnShowDisconnectedChanged( + Windows::UI::Xaml::DependencyObject^ d, + Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e); + static void OnShadowCenterXChanged( + Windows::UI::Xaml::DependencyObject^ d, + Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e); + static void OnIsDashedChanged( + Windows::UI::Xaml::DependencyObject^ d, + Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e); + + void SetCrescentPath(double shadowCenterX); + + Windows::UI::Xaml::Media::PathFigure^ m_crescentFigure; + Windows::UI::Xaml::Media::ArcSegment^ m_outerArc; + Windows::UI::Xaml::Media::ArcSegment^ m_innerArc; + Windows::UI::Xaml::Media::PathFigure^ m_dashedCrescentFigure; + Windows::UI::Xaml::Media::ArcSegment^ m_dashedOuterArc; + Windows::UI::Xaml::Media::ArcSegment^ m_dashedInnerArc; + Windows::UI::Xaml::Media::Animation::Storyboard^ m_phaseStoryboard; + Windows::UI::Xaml::Media::Animation::Storyboard^ m_selectionStoryboard; + Windows::UI::Xaml::DispatcherTimer^ m_orbitHideTimer; + long long m_orbitShownTick; + }; +} diff --git a/UI/Controls/SlidingMenu.xaml b/UI/Controls/SlidingMenu.xaml new file mode 100644 index 00000000..006d5a4b --- /dev/null +++ b/UI/Controls/SlidingMenu.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Controls/SlidingMenu.xaml.cpp b/UI/Controls/SlidingMenu.xaml.cpp new file mode 100644 index 00000000..252a8c9c --- /dev/null +++ b/UI/Controls/SlidingMenu.xaml.cpp @@ -0,0 +1,134 @@ +#include "pch.h" +#include "SlidingMenu.xaml.h" +#include "Utils.hpp" + +using namespace moonlight_xbox_dx::Controls; +using namespace Windows::UI::Xaml::Media::Animation; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Core; +using namespace concurrency; + +SlidingMenu::SlidingMenu() +{ + InitializeComponent(); + auto app = dynamic_cast(Application::Current); + + if (app != nullptr && app->GlobalMenuItems != nullptr) { + GlobalItems = app->GlobalMenuItems; + } + else { + GlobalItems = ref new Platform::Collections::Vector(); + } + PageItems = ref new Platform::Collections::Vector(); + + try { GlobalItemsList->ItemsSource = GlobalItems; } catch(...) {} + try { PageItemsList->ItemsSource = PageItems; } catch(...) {} +} + +void SlidingMenu::OnApplyTemplate() +{ + __super::OnApplyTemplate(); +} + +bool SlidingMenu::IsOpen::get() { + return m_isOpen; +} + +void SlidingMenu::AddPageItem(moonlight_xbox_dx::MenuItem^ item) { + if (item == nullptr) return; + PageItems->Append(item); +} + +void SlidingMenu::ClearPageItems() { + PageItems->Clear(); +} + +void SlidingMenu::Open() +{ + if (m_isOpen) return; + m_isOpen = true; + try { + + if (this->Dispatcher != nullptr && this->Dispatcher->HasThreadAccess) { + auto asyncOp = this->ShowAsync(); + + try { VisualStateManager::GoToState(this, "Opened", true); } catch(...) {} + concurrency::create_task(asyncOp).then([this](Windows::UI::Xaml::Controls::ContentDialogResult result) { + m_isOpen = false; + try { VisualStateManager::GoToState(this, "Closed", false); } catch(...) {} + }); + } else { + + Platform::WeakReference weakThis(this); + auto disp = this->Dispatcher; + if (disp == nullptr) { + try { auto coreView = Windows::ApplicationModel::Core::CoreApplication::MainView; if (coreView != nullptr && coreView->CoreWindow != nullptr) disp = coreView->CoreWindow->Dispatcher; } catch(...) { disp = nullptr; } + } + if (disp != nullptr) { + disp->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([weakThis]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + auto asyncOp = that->ShowAsync(); + try { VisualStateManager::GoToState(that, "Opened", true); } catch(...) {} + concurrency::create_task(asyncOp).then([that](Windows::UI::Xaml::Controls::ContentDialogResult result) { + that->m_isOpen = false; + try { VisualStateManager::GoToState(that, "Closed", false); } catch(...) {} + }); + } catch(...) { + that->m_isOpen = false; + } + })); + } else { + + try { + auto asyncOp = this->ShowAsync(); + try { VisualStateManager::GoToState(this, "Opened", true); } catch(...) {} + concurrency::create_task(asyncOp).then([this](Windows::UI::Xaml::Controls::ContentDialogResult result) { + m_isOpen = false; + try { VisualStateManager::GoToState(this, "Closed", false); } catch(...) {} + }); + } catch(...) { + m_isOpen = false; + } + } + } + } catch(...) { + m_isOpen = false; + } +} + +void SlidingMenu::Close() +{ + if (!m_isOpen) return; + + try { + + try { VisualStateManager::GoToState(this, "Closed", true); } catch(...) {} + this->Hide(); + } catch(...) {} + m_isOpen = false; +} + +void SlidingMenu::OnMenuItemClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) +{ + auto btn = safe_cast(sender); + auto item = safe_cast(btn->DataContext); + if (item != nullptr) { + + try { + if (item->ClickAction != nullptr) { + try { item->ClickAction(item, nullptr); } catch(...) {} + } + } catch(...) {} + + try { MenuItemInvoked(this, item); } catch(...) {} + } + + Close(); +} + +void SlidingMenu::Overlay_Tapped(Platform::Object^ sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs^ e) +{ + Close(); +} diff --git a/UI/Controls/SlidingMenu.xaml.h b/UI/Controls/SlidingMenu.xaml.h new file mode 100644 index 00000000..61e9595c --- /dev/null +++ b/UI/Controls/SlidingMenu.xaml.h @@ -0,0 +1,31 @@ +#pragma once +#include "UI\Controls\SlidingMenu.g.h" +#include "UI\Models\MenuItem.h" + +namespace moonlight_xbox_dx::Controls +{ + public ref class SlidingMenu sealed + { + public: + SlidingMenu(); + + property Windows::Foundation::Collections::IObservableVector^ GlobalItems; + + property Windows::Foundation::Collections::IObservableVector^ PageItems; + + void AddPageItem(moonlight_xbox_dx::MenuItem^ item); + void ClearPageItems(); + + void Open(); + void Close(); + property bool IsOpen { bool get(); } + event Windows::Foundation::TypedEventHandler^ MenuItemInvoked; + protected: + virtual void OnApplyTemplate() override; + private: + Platform::Object^ m_prevFocusedElement = nullptr; + bool m_isOpen = false; + void OnMenuItemClicked(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void Overlay_Tapped(Platform::Object^ sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs^ e); + }; +} diff --git a/UI/Controls/SwatchPicker.xaml b/UI/Controls/SwatchPicker.xaml new file mode 100644 index 00000000..2c34f5dd --- /dev/null +++ b/UI/Controls/SwatchPicker.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Controls/SwatchPicker.xaml.cpp b/UI/Controls/SwatchPicker.xaml.cpp new file mode 100644 index 00000000..696b5421 --- /dev/null +++ b/UI/Controls/SwatchPicker.xaml.cpp @@ -0,0 +1,274 @@ +#include "pch.h" +#include "UI\Controls\SwatchPicker.xaml.h" + +using namespace moonlight_xbox_dx; +using namespace Platform; +using namespace Platform::Collections; +using namespace Windows::UI; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::ViewManagement; +using namespace Windows::Foundation::Collections; + +static const Windows::UI::Color kDefaultSwatches[] = { + { 255, 255, 185, 0 }, + { 255, 247, 99, 12 }, + { 255, 202, 80, 16 }, + { 255, 232, 17, 35 }, + { 255, 234, 0, 94 }, + { 255, 195, 0, 82 }, + { 255, 135, 100, 184 }, + { 255, 116, 77, 169 }, + { 255, 107, 105, 214 }, + { 255, 0, 120, 215 }, + { 255, 0, 153, 188 }, + { 255, 3, 131, 135 }, + { 255, 73, 130, 5 }, + { 255, 16, 137, 62 }, + { 255, 105, 121, 126 }, +}; +static const int kDefaultSwatchCount = sizeof(kDefaultSwatches) / sizeof(kDefaultSwatches[0]); + +SwatchPicker::SwatchPicker() +{ + InitializeComponent(); + m_entries = ref new Vector(); + SwatchGrid->ItemsSource = m_entries; + BuildDefaultSwatches(); +} + +Windows::UI::Color SwatchPicker::GetSystemAccentColor() +{ + auto uiSettings = ref new UISettings(); + return uiSettings->GetColorValue(UIColorType::Accent); +} + +void SwatchPicker::BuildDefaultSwatches() +{ + m_entries->Clear(); + m_selectedEntry = nullptr; + m_customEntry = nullptr; + + auto sysColor = GetSystemAccentColor(); + auto sysEntry = ref new SwatchEntry(sysColor, true, false); + m_entries->Append(sysEntry); + + for (int i = 0; i < kDefaultSwatchCount; ++i) + m_entries->Append(ref new SwatchEntry(kDefaultSwatches[i], false, false)); + + m_customEntry = ref new SwatchEntry(Windows::UI::Color{ 255, 108, 108, 108 }, false, true); + m_entries->Append(m_customEntry); + + SelectEntry(sysEntry, false); +} + +void SwatchPicker::SelectEntry(SwatchEntry^ entry, bool fireEvent) +{ + if (m_selectedEntry != nullptr) + m_selectedEntry->IsSelected = false; + + m_selectedEntry = entry; + + if (entry != nullptr) { + entry->IsSelected = true; + m_useSystemAccent = entry->IsSystemAccent; + m_selectedColor = entry->DisplayColor; + } + + if (fireEvent && entry != nullptr) + ColorChanged(this, m_selectedColor, m_useSystemAccent); +} + +void SwatchPicker::SwatchGrid_ItemClick(Platform::Object^ sender, ItemClickEventArgs^ e) +{ + auto entry = dynamic_cast(e->ClickedItem); + if (entry == nullptr) return; + + if (entry->IsCustom) + OpenCustomColorPicker(entry); + else + SelectEntry(entry, true); +} + +static void RGBtoHSL(uint8_t r, uint8_t g, uint8_t b, double* outH, double* outS, double* outL) +{ + double rd = r / 255.0, gd = g / 255.0, bd = b / 255.0; + double cmax = rd > gd ? (rd > bd ? rd : bd) : (gd > bd ? gd : bd); + double cmin = rd < gd ? (rd < bd ? rd : bd) : (gd < bd ? gd : bd); + double delta = cmax - cmin; + double l = (cmax + cmin) * 0.5; + double h = 0.0, s = 0.0; + + if (delta > 0.0001) { + double denom = 1.0 - fabs(2.0 * l - 1.0); + s = (denom > 0.0001) ? delta / denom : 0.0; + + if (fabs(cmax - rd) < 0.0001) h = 60.0 * fmod((gd - bd) / delta, 6.0); + else if (fabs(cmax - gd) < 0.0001) h = 60.0 * ((bd - rd) / delta + 2.0); + else h = 60.0 * ((rd - gd) / delta + 4.0); + + if (h < 0.0) h += 360.0; + } + + *outH = h; + *outS = s * 100.0; + *outL = l * 100.0; +} + +static Windows::UI::Color HSLtoRGB(double h, double s, double l) +{ + h = fmod(h, 360.0); + if (h < 0.0) h += 360.0; + + double c = (1.0 - fabs(2.0 * l - 1.0)) * s; + double x = c * (1.0 - fabs(fmod(h / 60.0, 2.0) - 1.0)); + double m = l - c * 0.5; + double rd = 0, gd = 0, bd = 0; + + if (h < 60) { rd = c; gd = x; bd = 0; } + else if (h < 120) { rd = x; gd = c; bd = 0; } + else if (h < 180) { rd = 0; gd = c; bd = x; } + else if (h < 240) { rd = 0; gd = x; bd = c; } + else if (h < 300) { rd = x; gd = 0; bd = c; } + else { rd = c; gd = 0; bd = x; } + + Windows::UI::Color nc; + nc.A = 255; + nc.R = (uint8_t)((rd + m) * 255.0 + 0.5); + nc.G = (uint8_t)((gd + m) * 255.0 + 0.5); + nc.B = (uint8_t)((bd + m) * 255.0 + 0.5); + return nc; +} + +void SwatchPicker::OpenCustomColorPicker(SwatchEntry^ entry) +{ + auto c = entry->DisplayColor; + double hVal, sVal, lVal; + RGBtoHSL(c.R, c.G, c.B, &hVal, &sVal, &lVal); + + auto previewBrush = ref new Windows::UI::Xaml::Media::SolidColorBrush(c); + auto preview = ref new Windows::UI::Xaml::Shapes::Rectangle(); + preview->Height = 52; + preview->RadiusX = 8; + preview->RadiusY = 8; + preview->Margin = Thickness(0, 0, 0, 16); + preview->Fill = previewBrush; + + auto makeSlider = [](Platform::String^ label, double val, double maxVal) -> Slider^ { + auto sl = ref new Slider(); + sl->Header = label; + sl->Minimum = 0; + sl->Maximum = maxVal; + sl->Value = val; + sl->StepFrequency = 1; + sl->SnapsTo = Windows::UI::Xaml::Controls::Primitives::SliderSnapsTo::StepValues; + sl->Margin = Thickness(0, 0, 0, 8); + return sl; + }; + + auto hSlider = makeSlider("Hue", hVal, 360); + auto sSlider = makeSlider("Saturation", sVal, 100); + auto lSlider = makeSlider("Lightness", lVal, 100); + + auto onChanged = ref new Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventHandler( + [previewBrush, hSlider, sSlider, lSlider] + (Platform::Object^, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs^) { + previewBrush->Color = HSLtoRGB( + hSlider->Value, + sSlider->Value / 100.0, + lSlider->Value / 100.0); + }); + hSlider->ValueChanged += onChanged; + sSlider->ValueChanged += onChanged; + lSlider->ValueChanged += onChanged; + + auto content = ref new StackPanel(); + content->Width = 320; + content->Children->Append(preview); + content->Children->Append(hSlider); + content->Children->Append(sSlider); + content->Children->Append(lSlider); + + auto dialog = ref new ContentDialog(); + dialog->Title = "Custom Color"; + dialog->Content = content; + dialog->PrimaryButtonText = "Apply"; + dialog->CloseButtonText = "Cancel"; + dialog->XamlRoot = this->XamlRoot; + + auto that = this; + dialog->PrimaryButtonClick += ref new Windows::Foundation::TypedEventHandler( + [that, entry, hSlider, sSlider, lSlider](ContentDialog^, ContentDialogButtonClickEventArgs^) { + entry->UpdateColor(HSLtoRGB( + hSlider->Value, + sSlider->Value / 100.0, + lSlider->Value / 100.0)); + that->SelectEntry(entry, true); + }); + + dialog->ShowAsync(); +} + +void SwatchPicker::SetSwatches(IVector^ colors) +{ + m_entries->Clear(); + m_selectedEntry = nullptr; + m_customEntry = nullptr; + + auto sysColor = GetSystemAccentColor(); + auto sysEntry = ref new SwatchEntry(sysColor, true, false); + m_entries->Append(sysEntry); + + for (auto c : colors) + m_entries->Append(ref new SwatchEntry(c, false, false)); + + m_customEntry = ref new SwatchEntry(Windows::UI::Color{ 255, 108, 108, 108 }, false, true); + m_entries->Append(m_customEntry); + + SelectEntry(sysEntry, false); +} + +void SwatchPicker::SetSwatches(Windows::UI::Color* colors, int count) +{ + m_entries->Clear(); + m_selectedEntry = nullptr; + m_customEntry = nullptr; + + auto sysColor = GetSystemAccentColor(); + auto sysEntry = ref new SwatchEntry(sysColor, true, false); + m_entries->Append(sysEntry); + + for (int i = 0; i < count; ++i) + m_entries->Append(ref new SwatchEntry(colors[i], false, false)); + + m_customEntry = ref new SwatchEntry(Windows::UI::Color{ 255, 108, 108, 108 }, false, true); + m_entries->Append(m_customEntry); + + SelectEntry(sysEntry, false); +} + +void SwatchPicker::SelectColor(Windows::UI::Color color, bool useSystem) +{ + if (useSystem) { + if (m_entries->Size > 0) + SelectEntry(m_entries->GetAt(0), false); + return; + } + + for (unsigned int i = 0; i < m_entries->Size; ++i) { + auto entry = m_entries->GetAt(i); + if (entry->IsSystemAccent || entry->IsCustom) continue; + if (entry->DisplayColor.R == color.R + && entry->DisplayColor.G == color.G + && entry->DisplayColor.B == color.B) + { + SelectEntry(entry, false); + return; + } + } + + if (m_customEntry != nullptr) { + m_customEntry->UpdateColor(color); + SelectEntry(m_customEntry, false); + } +} diff --git a/UI/Controls/SwatchPicker.xaml.h b/UI/Controls/SwatchPicker.xaml.h new file mode 100644 index 00000000..f0f3fe39 --- /dev/null +++ b/UI/Controls/SwatchPicker.xaml.h @@ -0,0 +1,111 @@ +#pragma once +#include "UI\Controls\SwatchPicker.g.h" + +namespace moonlight_xbox_dx { + +[Windows::UI::Xaml::Data::Bindable] +public ref class SwatchEntry sealed : Windows::UI::Xaml::Data::INotifyPropertyChanged +{ +private: + bool m_isSelected = false; + bool m_isSystemAccent = false; + bool m_isCustom = false; + Windows::UI::Color m_color = {}; + +public: + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; + + SwatchEntry(Windows::UI::Color color, bool isSystem, bool isCustom) + : m_color(color), m_isSystemAccent(isSystem), m_isCustom(isCustom) {} + + void UpdateColor(Windows::UI::Color color) { + m_color = color; + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs("DisplayColor")); + } + + property Windows::UI::Color DisplayColor { + Windows::UI::Color get() { return m_color; } + } + + property bool IsSystemAccent { + bool get() { return m_isSystemAccent; } + } + + property bool IsCustom { + bool get() { return m_isCustom; } + } + + property bool IsSelected { + bool get() { return m_isSelected; } + void set(bool v) { + if (m_isSelected == v) return; + m_isSelected = v; + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs("IsSelected")); + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs("CheckVisibility")); + } + } + + property Windows::UI::Xaml::Visibility CheckVisibility { + Windows::UI::Xaml::Visibility get() { + return m_isSelected ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } + + property Windows::UI::Xaml::Visibility SystemIndicatorVisibility { + Windows::UI::Xaml::Visibility get() { + return m_isSystemAccent ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } + + property Windows::UI::Xaml::Visibility CustomIndicatorVisibility { + Windows::UI::Xaml::Visibility get() { + return m_isCustom ? Windows::UI::Xaml::Visibility::Visible + : Windows::UI::Xaml::Visibility::Collapsed; + } + } +}; + +public delegate void SwatchColorChangedHandler( + Platform::Object^ sender, + Windows::UI::Color color, + bool useSystemAccent); + +[Windows::UI::Xaml::Data::Bindable] +public ref class SwatchPicker sealed +{ +public: + SwatchPicker(); + + property Windows::UI::Color SelectedColor { + Windows::UI::Color get() { return m_selectedColor; } + } + + property bool UseSystemAccent { + bool get() { return m_useSystemAccent; } + } + + void SetSwatches(Windows::Foundation::Collections::IVector^ colors); + void SetSwatches(Windows::UI::Color* colors, int count); + + void SelectColor(Windows::UI::Color color, bool useSystem); + + event SwatchColorChangedHandler^ ColorChanged; + +private: + Windows::UI::Color m_selectedColor; + bool m_useSystemAccent = true; + Platform::Collections::Vector^ m_entries; + SwatchEntry^ m_selectedEntry = nullptr; + SwatchEntry^ m_customEntry = nullptr; + + void SwatchGrid_ItemClick(Platform::Object^ sender, + Windows::UI::Xaml::Controls::ItemClickEventArgs^ e); + void BuildDefaultSwatches(); + void SelectEntry(SwatchEntry^ entry, bool fireEvent); + void OpenCustomColorPicker(SwatchEntry^ entry); + Windows::UI::Color GetSystemAccentColor(); +}; + +} diff --git a/UI/Controls/TabsLayout.xaml b/UI/Controls/TabsLayout.xaml new file mode 100644 index 00000000..c49bfdd7 --- /dev/null +++ b/UI/Controls/TabsLayout.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/AddHostDialog.xaml.cpp b/UI/Modals/AddHostDialog.xaml.cpp new file mode 100644 index 00000000..2a7c80b4 --- /dev/null +++ b/UI/Modals/AddHostDialog.xaml.cpp @@ -0,0 +1,123 @@ +#include "pch.h" +#include "UI\Modals\AddHostDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::Foundation::Collections; + +namespace moonlight_xbox_dx +{ + +AddHostDialog::AddHostDialog() +{ + InitializeComponent(); +} + +void AddHostDialog::Configure( + RoutedEventHandler^ onAdd, + RoutedEventHandler^ onCancel) +{ + m_onAdd = onAdd; + m_onCancel = onCancel; + + try { + AddButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + if (ErrorText != nullptr) ErrorText->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + if (m_onAdd != nullptr) m_onAdd->Invoke(sender, args); + }); + + CancelButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + try { this->Hide(); } catch (...) {} + if (m_onCancel != nullptr) m_onCancel->Invoke(sender, args); + }); + + HostnameTextBox->KeyDown += ref new KeyEventHandler([this](Platform::Object^, KeyRoutedEventArgs^ e) { + if (e->Key == Windows::System::VirtualKey::Enter) { + try { Windows::UI::ViewManagement::InputPane::GetForCurrentView()->TryHide(); } catch (...) {} + if (ErrorText != nullptr) ErrorText->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + if (m_onAdd != nullptr) m_onAdd->Invoke(this, ref new RoutedEventArgs()); + } + }); + } catch (...) {} +} + +Platform::String^ AddHostDialog::GetHostname() +{ + try { if (HostnameTextBox != nullptr) return HostnameTextBox->Text; } catch (...) {} + return nullptr; +} + +void AddHostDialog::ShowError(Platform::String^ message) +{ + try { + if (ErrorText != nullptr) { + ErrorText->Text = message; + ErrorText->Visibility = Windows::UI::Xaml::Visibility::Visible; + } + } catch (...) {} +} + +void AddHostDialog::SetAddButtonEnabled(bool enabled) +{ + try { if (AddButton != nullptr) AddButton->IsEnabled = enabled; } catch (...) {} +} + +void AddHostDialog::SetRecentHostnames(IVector^ hostnames, IVector^ displayNames) +{ + try { + if (hostnames == nullptr || hostnames->Size == 0) return; + RecentHostsBorder->Visibility = Windows::UI::Xaml::Visibility::Visible; + for (unsigned int i = 0; i < hostnames->Size; i++) { + auto hostname = hostnames->GetAt(i); + auto computerName = (displayNames != nullptr && displayNames->Size > i) ? displayNames->GetAt(i) : nullptr; + auto btn = ref new Button(); + if (computerName != nullptr && computerName->Length() > 0) { + auto content = ref new StackPanel(); + content->Orientation = Windows::UI::Xaml::Controls::Orientation::Horizontal; + content->Spacing = 6; + auto nameText = ref new TextBlock(); + nameText->Text = computerName; + nameText->FontFamily = ref new Windows::UI::Xaml::Media::FontFamily("Bahnschrift"); + nameText->Foreground = ref new Windows::UI::Xaml::Media::SolidColorBrush(Windows::UI::Colors::White); + nameText->VerticalAlignment = Windows::UI::Xaml::VerticalAlignment::Center; + auto addressText = ref new TextBlock(); + addressText->Text = "(" + hostname + ")"; + addressText->FontFamily = ref new Windows::UI::Xaml::Media::FontFamily("Bahnschrift"); + addressText->FontSize = 10; + Windows::UI::Color dimWhite; + dimWhite.A = 0x99; dimWhite.R = 0xFF; dimWhite.G = 0xFF; dimWhite.B = 0xFF; + addressText->Foreground = ref new Windows::UI::Xaml::Media::SolidColorBrush(dimWhite); + addressText->VerticalAlignment = Windows::UI::Xaml::VerticalAlignment::Center; + content->Children->Append(nameText); + content->Children->Append(addressText); + btn->Content = content; + } else { + btn->Content = hostname; + } + btn->FontFamily = ref new Windows::UI::Xaml::Media::FontFamily("Bahnschrift"); + btn->FontSize = 12; + btn->Height = 36; + btn->HorizontalAlignment = Windows::UI::Xaml::HorizontalAlignment::Stretch; + btn->HorizontalContentAlignment = Windows::UI::Xaml::HorizontalAlignment::Left; + Windows::UI::Xaml::CornerRadius cr; + cr.TopLeft = cr.TopRight = cr.BottomRight = cr.BottomLeft = 8.0; + btn->CornerRadius = cr; + btn->Padding = Windows::UI::Xaml::Thickness{ 12, 0, 12, 0 }; + Windows::UI::Color bgColor; + bgColor.A = 0x88; bgColor.R = 0; bgColor.G = 0; bgColor.B = 0; + btn->Background = ref new Windows::UI::Xaml::Media::SolidColorBrush(bgColor); + btn->Foreground = ref new Windows::UI::Xaml::Media::SolidColorBrush(Windows::UI::Colors::White); + btn->Click += ref new RoutedEventHandler([this, hostname](Platform::Object^ sender, RoutedEventArgs^ args) { + try { + HostnameTextBox->Text = hostname; + if (ErrorText != nullptr) ErrorText->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + if (m_onAdd != nullptr) m_onAdd->Invoke(sender, args); + } catch (...) {} + }); + RecentHostsPanel->Children->Append(btn); + } + } catch (...) {} +} + +} diff --git a/UI/Modals/AddHostDialog.xaml.h b/UI/Modals/AddHostDialog.xaml.h new file mode 100644 index 00000000..2cea9ec9 --- /dev/null +++ b/UI/Modals/AddHostDialog.xaml.h @@ -0,0 +1,26 @@ +#pragma once + +#include "UI\Modals\AddHostDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class AddHostDialog sealed + { + public: + AddHostDialog(); + void Configure( + Windows::UI::Xaml::RoutedEventHandler^ onAdd, + Windows::UI::Xaml::RoutedEventHandler^ onCancel); + Platform::String^ GetHostname(); + void ShowError(Platform::String^ message); + void SetAddButtonEnabled(bool enabled); + void SetRecentHostnames( + Windows::Foundation::Collections::IVector^ hostnames, + Windows::Foundation::Collections::IVector^ displayNames); + + private: + Windows::UI::Xaml::RoutedEventHandler^ m_onAdd; + Windows::UI::Xaml::RoutedEventHandler^ m_onCancel; + }; +} diff --git a/UI/Modals/AlertDialog.xaml b/UI/Modals/AlertDialog.xaml new file mode 100644 index 00000000..37b0b9a9 --- /dev/null +++ b/UI/Modals/AlertDialog.xaml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/AlertDialog.xaml.cpp b/UI/Modals/AlertDialog.xaml.cpp new file mode 100644 index 00000000..558fc2e9 --- /dev/null +++ b/UI/Modals/AlertDialog.xaml.cpp @@ -0,0 +1,37 @@ +#include "pch.h" +#include "UI\Modals\AlertDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace moonlight_xbox_dx +{ + +AlertDialog::AlertDialog() +{ + InitializeComponent(); +} + +void AlertDialog::Configure(Platform::String^ title, Platform::String^ message) +{ + Configure(title, message, nullptr); +} + +void AlertDialog::Configure(Platform::String^ title, Platform::String^ message, Platform::String^ glyph) +{ + try { if (TitleHeader != nullptr) TitleHeader->Text = title; } catch (...) {} + try { if (MessageText != nullptr) MessageText->Text = message; } catch (...) {} + try { + if (TitleIcon != nullptr && glyph != nullptr && glyph->Length() > 0) + TitleIcon->Glyph = glyph; + } catch (...) {} + try { + if (OkButton != nullptr) { + OkButton->Click += ref new RoutedEventHandler([this](Platform::Object^, RoutedEventArgs^) { + try { this->Hide(); } catch (...) {} + }); + } + } catch (...) {} +} + +} diff --git a/UI/Modals/AlertDialog.xaml.h b/UI/Modals/AlertDialog.xaml.h new file mode 100644 index 00000000..d706af67 --- /dev/null +++ b/UI/Modals/AlertDialog.xaml.h @@ -0,0 +1,15 @@ +#pragma once + +#include "UI\Modals\AlertDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class AlertDialog sealed + { + public: + AlertDialog(); + void Configure(Platform::String^ title, Platform::String^ message); + void Configure(Platform::String^ title, Platform::String^ message, Platform::String^ glyph); + }; +} diff --git a/UI/Modals/AppActionsDialog.xaml b/UI/Modals/AppActionsDialog.xaml new file mode 100644 index 00000000..b63b2d66 --- /dev/null +++ b/UI/Modals/AppActionsDialog.xaml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/AppActionsDialog.xaml.cpp b/UI/Modals/AppActionsDialog.xaml.cpp new file mode 100644 index 00000000..6b14de97 --- /dev/null +++ b/UI/Modals/AppActionsDialog.xaml.cpp @@ -0,0 +1,94 @@ +#include "pch.h" +#include "UI\Modals\AppActionsDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace moonlight_xbox_dx +{ + +namespace { + void HideIfOpen(ContentDialog^ dialog) { + try { if (dialog != nullptr) dialog->Hide(); } catch (...) {} + } +} + +AppActionsDialog::AppActionsDialog() +{ + InitializeComponent(); + + try { + this->ResumeButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onResume != nullptr) m_onResume->Invoke(sender, args); + }); + this->CloseButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onClose != nullptr) m_onClose->Invoke(sender, args); + }); + this->CloseAndStartButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onCloseAndStart != nullptr) m_onCloseAndStart->Invoke(sender, args); + }); + this->StartButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onStart != nullptr) m_onStart->Invoke(sender, args); + }); + this->MoonlightSettingsButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onMoonlightSettings != nullptr) m_onMoonlightSettings->Invoke(sender, args); + }); + this->HostSettingsButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onHostSettings != nullptr) m_onHostSettings->Invoke(sender, args); + }); + this->FavoriteButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onToggleFavorite != nullptr) m_onToggleFavorite->Invoke(sender, args); + }); + } catch (...) {} +} + +void AppActionsDialog::Configure( + Platform::String^ appName, + bool showResumeClose, + bool showCloseAndStart, + bool showStart, + bool isFavorite, + RoutedEventHandler^ onResume, + RoutedEventHandler^ onClose, + RoutedEventHandler^ onCloseAndStart, + RoutedEventHandler^ onStart, + RoutedEventHandler^ onMoonlightSettings, + RoutedEventHandler^ onHostSettings, + RoutedEventHandler^ onToggleFavorite) +{ + m_onResume = onResume; + m_onClose = onClose; + m_onCloseAndStart = onCloseAndStart; + m_onStart = onStart; + m_onMoonlightSettings = onMoonlightSettings; + m_onHostSettings = onHostSettings; + m_onToggleFavorite = onToggleFavorite; + + try { + if (this->AppNameHeader != nullptr) this->AppNameHeader->Text = appName; + + if (this->FavoriteIcon != nullptr) + this->FavoriteIcon->Glyph = isFavorite ? L"\xE735" : L"\xE734"; + if (this->FavoriteLabel != nullptr) + this->FavoriteLabel->Text = isFavorite ? "Remove from Favorites" : "Add to Favorites"; + + if (this->ResumeButton != nullptr) + this->ResumeButton->Visibility = showResumeClose ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + if (this->CloseButton != nullptr) + this->CloseButton->Visibility = showResumeClose ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + + if (this->CloseAndStartButton != nullptr) + this->CloseAndStartButton->Visibility = showCloseAndStart ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + if (this->StartButton != nullptr) + this->StartButton->Visibility = showStart ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + } catch (...) {} +} + +} diff --git a/UI/Modals/AppActionsDialog.xaml.h b/UI/Modals/AppActionsDialog.xaml.h new file mode 100644 index 00000000..668aac28 --- /dev/null +++ b/UI/Modals/AppActionsDialog.xaml.h @@ -0,0 +1,36 @@ +#pragma once + +#include "UI\Modals\AppActionsDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class AppActionsDialog sealed + { + public: + AppActionsDialog(); + + void Configure( + Platform::String^ appName, + bool showResumeClose, + bool showCloseAndStart, + bool showStart, + bool isFavorite, + Windows::UI::Xaml::RoutedEventHandler^ onResume, + Windows::UI::Xaml::RoutedEventHandler^ onClose, + Windows::UI::Xaml::RoutedEventHandler^ onCloseAndStart, + Windows::UI::Xaml::RoutedEventHandler^ onStart, + Windows::UI::Xaml::RoutedEventHandler^ onMoonlightSettings, + Windows::UI::Xaml::RoutedEventHandler^ onHostSettings, + Windows::UI::Xaml::RoutedEventHandler^ onToggleFavorite); + + private: + Windows::UI::Xaml::RoutedEventHandler^ m_onResume; + Windows::UI::Xaml::RoutedEventHandler^ m_onClose; + Windows::UI::Xaml::RoutedEventHandler^ m_onCloseAndStart; + Windows::UI::Xaml::RoutedEventHandler^ m_onStart; + Windows::UI::Xaml::RoutedEventHandler^ m_onMoonlightSettings; + Windows::UI::Xaml::RoutedEventHandler^ m_onHostSettings; + Windows::UI::Xaml::RoutedEventHandler^ m_onToggleFavorite; + }; +} diff --git a/UI/Modals/ConfirmDialog.xaml b/UI/Modals/ConfirmDialog.xaml new file mode 100644 index 00000000..fd2166c4 --- /dev/null +++ b/UI/Modals/ConfirmDialog.xaml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/ConfirmDialog.xaml.cpp b/UI/Modals/ConfirmDialog.xaml.cpp new file mode 100644 index 00000000..0dc83f41 --- /dev/null +++ b/UI/Modals/ConfirmDialog.xaml.cpp @@ -0,0 +1,61 @@ +#include "pch.h" +#include "UI\Modals\ConfirmDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace moonlight_xbox_dx +{ + +ConfirmDialog::ConfirmDialog() +{ + InitializeComponent(); + try { + if (ConfirmButton != nullptr) { + ConfirmButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + try { this->Hide(); } catch (...) {} + if (m_onConfirm != nullptr) m_onConfirm->Invoke(sender, args); + }); + } + if (CancelButton != nullptr) { + CancelButton->Click += ref new RoutedEventHandler([this](Platform::Object^, RoutedEventArgs^) { + try { this->Hide(); } catch (...) {} + }); + } + } catch (...) {} +} + +void ConfirmDialog::Configure(Platform::String^ title, Platform::String^ message, + RoutedEventHandler^ onConfirm) +{ + Configure(title, message, nullptr, nullptr, nullptr, onConfirm); +} + +void ConfirmDialog::Configure(Platform::String^ title, Platform::String^ message, Platform::String^ glyph, + RoutedEventHandler^ onConfirm) +{ + Configure(title, message, glyph, nullptr, nullptr, onConfirm); +} + +void ConfirmDialog::Configure(Platform::String^ title, Platform::String^ message, Platform::String^ glyph, + Platform::String^ confirmText, Platform::String^ cancelText, + RoutedEventHandler^ onConfirm) +{ + m_onConfirm = onConfirm; + try { if (TitleHeader != nullptr) TitleHeader->Text = title; } catch (...) {} + try { if (MessageText != nullptr) MessageText->Text = message; } catch (...) {} + try { + if (TitleIcon != nullptr && glyph != nullptr && glyph->Length() > 0) + TitleIcon->Glyph = glyph; + } catch (...) {} + try { + if (ConfirmButtonText != nullptr && confirmText != nullptr && confirmText->Length() > 0) + ConfirmButtonText->Text = confirmText; + } catch (...) {} + try { + if (CancelButtonText != nullptr && cancelText != nullptr && cancelText->Length() > 0) + CancelButtonText->Text = cancelText; + } catch (...) {} +} + +} diff --git a/UI/Modals/ConfirmDialog.xaml.h b/UI/Modals/ConfirmDialog.xaml.h new file mode 100644 index 00000000..3ea05489 --- /dev/null +++ b/UI/Modals/ConfirmDialog.xaml.h @@ -0,0 +1,23 @@ +#pragma once + +#include "UI\Modals\ConfirmDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class ConfirmDialog sealed + { + public: + ConfirmDialog(); + void Configure(Platform::String^ title, Platform::String^ message, + Windows::UI::Xaml::RoutedEventHandler^ onConfirm); + void Configure(Platform::String^ title, Platform::String^ message, Platform::String^ glyph, + Windows::UI::Xaml::RoutedEventHandler^ onConfirm); + void Configure(Platform::String^ title, Platform::String^ message, Platform::String^ glyph, + Platform::String^ confirmText, Platform::String^ cancelText, + Windows::UI::Xaml::RoutedEventHandler^ onConfirm); + + private: + Windows::UI::Xaml::RoutedEventHandler^ m_onConfirm; + }; +} diff --git a/UI/Modals/HostActionsDialog.xaml b/UI/Modals/HostActionsDialog.xaml new file mode 100644 index 00000000..aa3896a7 --- /dev/null +++ b/UI/Modals/HostActionsDialog.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/HostActionsDialog.xaml.cpp b/UI/Modals/HostActionsDialog.xaml.cpp new file mode 100644 index 00000000..61265d52 --- /dev/null +++ b/UI/Modals/HostActionsDialog.xaml.cpp @@ -0,0 +1,70 @@ +#include "pch.h" +#include "UI\Modals\HostActionsDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace moonlight_xbox_dx +{ + +namespace { + void HideIfOpen(ContentDialog^ dialog) { + try { if (dialog != nullptr) dialog->Hide(); } catch (...) {} + } +} + +HostActionsDialog::HostActionsDialog() +{ + InitializeComponent(); + + try { + this->HostSettingsButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onHostSettings != nullptr) m_onHostSettings->Invoke(sender, args); + }); + this->WakeHostButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onWakeHost != nullptr) m_onWakeHost->Invoke(sender, args); + }); + this->TestConnectionButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onTestConnection != nullptr) m_onTestConnection->Invoke(sender, args); + }); + this->RemoveHostButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onRemoveHost != nullptr) m_onRemoveHost->Invoke(sender, args); + }); + this->MoonlightSettingsButton->Click += ref new RoutedEventHandler([this](Platform::Object^ sender, RoutedEventArgs^ args) { + HideIfOpen(this); + if (m_onMoonlightSettings != nullptr) m_onMoonlightSettings->Invoke(sender, args); + }); + } catch (...) {} +} + +void HostActionsDialog::Configure( + Platform::String^ hostName, + bool showWake, + bool showTestConnection, + RoutedEventHandler^ onHostSettings, + RoutedEventHandler^ onWakeHost, + RoutedEventHandler^ onTestConnection, + RoutedEventHandler^ onRemoveHost, + RoutedEventHandler^ onMoonlightSettings) +{ + m_onHostSettings = onHostSettings; + m_onWakeHost = onWakeHost; + m_onTestConnection = onTestConnection; + m_onRemoveHost = onRemoveHost; + m_onMoonlightSettings = onMoonlightSettings; + + try { + if (this->HostNameHeader != nullptr) this->HostNameHeader->Text = hostName; + + if (this->WakeHostButton != nullptr) + this->WakeHostButton->Visibility = showWake ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + if (this->TestConnectionButton != nullptr) + this->TestConnectionButton->Visibility = showTestConnection ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + } catch (...) {} +} + +} diff --git a/UI/Modals/HostActionsDialog.xaml.h b/UI/Modals/HostActionsDialog.xaml.h new file mode 100644 index 00000000..0bc177c6 --- /dev/null +++ b/UI/Modals/HostActionsDialog.xaml.h @@ -0,0 +1,30 @@ +#pragma once + +#include "UI\Modals\HostActionsDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class HostActionsDialog sealed + { + public: + HostActionsDialog(); + + void Configure( + Platform::String^ hostName, + bool showWake, + bool showTestConnection, + Windows::UI::Xaml::RoutedEventHandler^ onHostSettings, + Windows::UI::Xaml::RoutedEventHandler^ onWakeHost, + Windows::UI::Xaml::RoutedEventHandler^ onTestConnection, + Windows::UI::Xaml::RoutedEventHandler^ onRemoveHost, + Windows::UI::Xaml::RoutedEventHandler^ onMoonlightSettings); + + private: + Windows::UI::Xaml::RoutedEventHandler^ m_onHostSettings; + Windows::UI::Xaml::RoutedEventHandler^ m_onWakeHost; + Windows::UI::Xaml::RoutedEventHandler^ m_onTestConnection; + Windows::UI::Xaml::RoutedEventHandler^ m_onRemoveHost; + Windows::UI::Xaml::RoutedEventHandler^ m_onMoonlightSettings; + }; +} diff --git a/UI/Modals/PairDialog.xaml b/UI/Modals/PairDialog.xaml new file mode 100644 index 00000000..36b0be02 --- /dev/null +++ b/UI/Modals/PairDialog.xaml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/PairDialog.xaml.cpp b/UI/Modals/PairDialog.xaml.cpp new file mode 100644 index 00000000..86cd1c4b --- /dev/null +++ b/UI/Modals/PairDialog.xaml.cpp @@ -0,0 +1,27 @@ +#include "pch.h" +#include "UI\Modals\PairDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace moonlight_xbox_dx +{ + +PairDialog::PairDialog() +{ + InitializeComponent(); +} + +void PairDialog::Configure(Platform::String^ pin) +{ + try { if (PinText != nullptr) PinText->Text = pin; } catch (...) {} + try { + if (OkButton != nullptr) { + OkButton->Click += ref new RoutedEventHandler([this](Platform::Object^, RoutedEventArgs^) { + try { this->Hide(); } catch (...) {} + }); + } + } catch (...) {} +} + +} diff --git a/UI/Modals/PairDialog.xaml.h b/UI/Modals/PairDialog.xaml.h new file mode 100644 index 00000000..194afb84 --- /dev/null +++ b/UI/Modals/PairDialog.xaml.h @@ -0,0 +1,14 @@ +#pragma once + +#include "UI\Modals\PairDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class PairDialog sealed + { + public: + PairDialog(); + void Configure(Platform::String^ pin); + }; +} diff --git a/UI/Modals/StreamErrorDialog.xaml b/UI/Modals/StreamErrorDialog.xaml new file mode 100644 index 00000000..456876b6 --- /dev/null +++ b/UI/Modals/StreamErrorDialog.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/StreamErrorDialog.xaml.cpp b/UI/Modals/StreamErrorDialog.xaml.cpp new file mode 100644 index 00000000..a83cd67d --- /dev/null +++ b/UI/Modals/StreamErrorDialog.xaml.cpp @@ -0,0 +1,55 @@ +#include "pch.h" +#include "UI\Modals\StreamErrorDialog.xaml.h" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; + +namespace moonlight_xbox_dx +{ + +StreamErrorDialog::StreamErrorDialog() +{ + InitializeComponent(); +} + +void StreamErrorDialog::Configure( + Platform::String^ title, + Platform::String^ message, + Platform::String^ headerGlyph, + Platform::String^ primaryText, + Platform::String^ primaryGlyph, + Platform::String^ secondaryText, + Platform::String^ secondaryGlyph, + RoutedEventHandler^ onPrimary, + RoutedEventHandler^ onSecondary) +{ + try { if (TitleHeader) TitleHeader->Text = title; } catch (...) {} + try { if (MessageText) MessageText->Text = message; } catch (...) {} + try { if (TitleIcon && headerGlyph && headerGlyph->Length() > 0) TitleIcon->Glyph = headerGlyph; } catch (...) {} + try { if (PrimaryButtonLabel && primaryText) PrimaryButtonLabel->Text = primaryText; } catch (...) {} + try { if (PrimaryButtonIcon && primaryGlyph && primaryGlyph->Length() > 0) PrimaryButtonIcon->Glyph = primaryGlyph; } catch (...) {} + try { if (SecondaryButtonLabel && secondaryText) SecondaryButtonLabel->Text = secondaryText; } catch (...) {} + try { if (SecondaryButtonIcon && secondaryGlyph && secondaryGlyph->Length() > 0) SecondaryButtonIcon->Glyph = secondaryGlyph; } catch (...) {} + + try { + if (PrimaryButton) { + PrimaryButton->Click += ref new RoutedEventHandler( + [this, onPrimary](Platform::Object^ sender, RoutedEventArgs^ args) { + try { this->Hide(); } catch (...) {} + if (onPrimary) onPrimary->Invoke(sender, args); + }); + } + } catch (...) {} + + try { + if (SecondaryButton) { + SecondaryButton->Click += ref new RoutedEventHandler( + [this, onSecondary](Platform::Object^ sender, RoutedEventArgs^ args) { + try { this->Hide(); } catch (...) {} + if (onSecondary) onSecondary->Invoke(sender, args); + }); + } + } catch (...) {} +} + +} diff --git a/UI/Modals/StreamErrorDialog.xaml.h b/UI/Modals/StreamErrorDialog.xaml.h new file mode 100644 index 00000000..c155b8c1 --- /dev/null +++ b/UI/Modals/StreamErrorDialog.xaml.h @@ -0,0 +1,22 @@ +#pragma once + +#include "UI\Modals\StreamErrorDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class StreamErrorDialog sealed + { + public: + StreamErrorDialog(); + void Configure(Platform::String^ title, + Platform::String^ message, + Platform::String^ headerGlyph, + Platform::String^ primaryText, + Platform::String^ primaryGlyph, + Platform::String^ secondaryText, + Platform::String^ secondaryGlyph, + Windows::UI::Xaml::RoutedEventHandler^ onPrimary, + Windows::UI::Xaml::RoutedEventHandler^ onSecondary); + }; +} diff --git a/UI/Modals/TestConnectionResultDialog.xaml b/UI/Modals/TestConnectionResultDialog.xaml new file mode 100644 index 00000000..d469be05 --- /dev/null +++ b/UI/Modals/TestConnectionResultDialog.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Modals/TestConnectionResultDialog.xaml.cpp b/UI/Modals/TestConnectionResultDialog.xaml.cpp new file mode 100644 index 00000000..8438b2ae --- /dev/null +++ b/UI/Modals/TestConnectionResultDialog.xaml.cpp @@ -0,0 +1,68 @@ +#include "pch.h" +#include "UI\Modals\TestConnectionResultDialog.xaml.h" +#include "Utils.hpp" + +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; + +namespace moonlight_xbox_dx +{ + +TestConnectionResultDialog::TestConnectionResultDialog() +{ + InitializeComponent(); +} + +void TestConnectionResultDialog::Configure(Platform::String^ hostname, Platform::String^ resultMsg) +{ + try { if (HostNameHeader != nullptr) HostNameHeader->Text = hostname; } catch (...) {} + + std::string msg = Utils::PlatformStringToStdString(resultMsg); + bool isSuccess = (msg.rfind("Connection OK", 0) == 0); + + std::string statusText = msg; + auto parenPos = msg.find(" (RTT:"); + if (parenPos != std::string::npos) statusText = msg.substr(0, parenPos); + + double rttMs = -1.0; + auto rttPos = msg.find("RTT: "); + if (rttPos != std::string::npos) { + try { rttMs = std::stod(msg.substr(rttPos + 5)); } catch (...) {} + } + + try { if (StatusText != nullptr) StatusText->Text = Utils::StringFromStdString(statusText); } catch (...) {} + + try { + if (ResultIcon != nullptr) { + if (isSuccess) { + ResultIcon->Glyph = L"\xE73E"; + ResultIcon->Foreground = ref new SolidColorBrush( + Windows::UI::ColorHelper::FromArgb(255, 76, 175, 80)); + } else { + ResultIcon->Glyph = L"\xE711"; + ResultIcon->Foreground = ref new SolidColorBrush( + Windows::UI::ColorHelper::FromArgb(255, 244, 67, 54)); + } + } + } catch (...) {} + + try { + if (rttMs >= 0 && RttBadge != nullptr && RttText != nullptr) { + char buf[32]; + snprintf(buf, sizeof(buf), "RTT: %.1f ms", rttMs); + RttText->Text = Utils::StringFromStdString(std::string(buf)); + RttBadge->Visibility = Windows::UI::Xaml::Visibility::Visible; + } + } catch (...) {} + + try { + if (OkButton != nullptr) { + OkButton->Click += ref new RoutedEventHandler([this](Platform::Object^, RoutedEventArgs^) { + try { this->Hide(); } catch (...) {} + }); + } + } catch (...) {} +} + +} diff --git a/UI/Modals/TestConnectionResultDialog.xaml.h b/UI/Modals/TestConnectionResultDialog.xaml.h new file mode 100644 index 00000000..3ea364b9 --- /dev/null +++ b/UI/Modals/TestConnectionResultDialog.xaml.h @@ -0,0 +1,14 @@ +#pragma once + +#include "UI\Modals\TestConnectionResultDialog.g.h" + +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class TestConnectionResultDialog sealed + { + public: + TestConnectionResultDialog(); + void Configure(Platform::String^ hostname, Platform::String^ resultMsg); + }; +} diff --git a/UI/Models/MenuItem.cpp b/UI/Models/MenuItem.cpp new file mode 100644 index 00000000..56d9b5f6 --- /dev/null +++ b/UI/Models/MenuItem.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "MenuItem.h" + +using namespace moonlight_xbox_dx; + +MenuItem::MenuItem() +{ + Title = ref new Platform::String(L""); + IconGlyph = ref new Platform::String(L""); + Tag = nullptr; +} + +MenuItem::MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph) { + Title = title; + IconGlyph = iconGlyph; +} + +MenuItem::MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph, Platform::Object ^ tag) { + Title = title; + IconGlyph = iconGlyph; + Tag = tag; +} + +MenuItem::MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph, Windows::Foundation::EventHandler^ clickAction) { + Title = title; + IconGlyph = iconGlyph; + Tag = nullptr; + ClickAction = clickAction; +} + +MenuItem::MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph, Platform::Object ^ tag, Windows::Foundation::EventHandler^ clickAction) { + Title = title; + IconGlyph = iconGlyph; + Tag = tag; + ClickAction = clickAction; +} diff --git a/UI/Models/MenuItem.h b/UI/Models/MenuItem.h new file mode 100644 index 00000000..92ef4151 --- /dev/null +++ b/UI/Models/MenuItem.h @@ -0,0 +1,21 @@ +#pragma once + +namespace moonlight_xbox_dx +{ + public ref class MenuItem sealed + { + public: + MenuItem(); + MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph); + MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph, Platform::Object ^ tag); + + MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph, Windows::Foundation::EventHandler^ clickAction); + + MenuItem(Platform::String ^ title, Platform::String ^ iconGlyph, Platform::Object ^ tag, Windows::Foundation::EventHandler^ clickAction); + + property Platform::String^ Title; + property Platform::String^ IconGlyph; + property Platform::Object^ Tag; + property Windows::Foundation::EventHandler^ ClickAction; + }; +} diff --git a/UI/Models/UIPersonalization.h b/UI/Models/UIPersonalization.h new file mode 100644 index 00000000..5d2298a8 --- /dev/null +++ b/UI/Models/UIPersonalization.h @@ -0,0 +1,68 @@ +#pragma once +#include "pch.h" + +namespace moonlight_xbox_dx { + +public enum class AppHostView : int { + List = 0, + Grid = 1, +}; + +[Windows::UI::Xaml::Data::Bindable] +public ref class UIPersonalization sealed : Windows::UI::Xaml::Data::INotifyPropertyChanged +{ +private: + Platform::String^ background = ""; + AppHostView appView = AppHostView::List; + Windows::UI::Color accentColor = Windows::UI::Color{ 255, 0, 120, 215 }; + bool useSystemAccent = true; + +public: + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; + + void OnPropertyChanged(Platform::String^ propertyName) + { + PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName)); + } + + property Platform::String^ Background + { + Platform::String^ get() { return this->background; } + void set(Platform::String^ value) { + if (background == value) return; + this->background = value; + OnPropertyChanged("Background"); + } + } + + property AppHostView AppView + { + AppHostView get() { return this->appView; } + void set(AppHostView value) { + if (appView == value) return; + this->appView = value; + OnPropertyChanged("AppView"); + } + } + + property Windows::UI::Color AccentColor + { + Windows::UI::Color get() { return this->accentColor; } + void set(Windows::UI::Color value) { + this->accentColor = value; + OnPropertyChanged("AccentColor"); + } + } + + property bool UseSystemAccent + { + bool get() { return this->useSystemAccent; } + void set(bool value) { + if (useSystemAccent == value) return; + this->useSystemAccent = value; + OnPropertyChanged("UseSystemAccent"); + } + } +}; + +} diff --git a/UI/Models/ViewModels/AppPageViewModel.cpp b/UI/Models/ViewModels/AppPageViewModel.cpp new file mode 100644 index 00000000..306279a0 --- /dev/null +++ b/UI/Models/ViewModels/AppPageViewModel.cpp @@ -0,0 +1,100 @@ +#include "pch.h" +#include "AppPageViewModel.h" +#include + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; +using namespace Windows::UI::Xaml::Media::Imaging; +using namespace moonlight_xbox_dx; + +AppPageViewModel::AppPageViewModel() + : currentBackgroundImage(nullptr), + pageBackgroundBorder(nullptr), + backgroundTransitionDurationMs(250.0), + backgroundOverlayOpacity(0.05) +{ +} + +void AppPageViewModel::OnPropertyChanged(Platform::String^ propertyName) +{ + PropertyChanged(this, ref new PropertyChangedEventArgs(propertyName)); +} + +void AppPageViewModel::SetPageBackgroundBorder(Border^ border) +{ + this->pageBackgroundBorder = border; + this->currentBackgroundImage = nullptr; +} + +void AppPageViewModel::SetBackgroundTransitionSettings(double durationMs, double overlayOpacity) +{ + if (durationMs > 0.0) { + this->backgroundTransitionDurationMs = durationMs; + } + + if (overlayOpacity < 0.0) overlayOpacity = 0.0; + if (overlayOpacity > 1.0) overlayOpacity = 1.0; + this->backgroundOverlayOpacity = overlayOpacity; +} + +void AppPageViewModel::TransitionToBlurredImage(BitmapImage^ newImage) +{ + if (newImage == nullptr || this->pageBackgroundBorder == nullptr) return; + if (newImage == this->currentBackgroundImage) return; + this->currentBackgroundImage = newImage; + + try { + auto durationTicks = (long long)std::llround(this->backgroundTransitionDurationMs * 10000.0); + if (durationTicks <= 0) durationTicks = 2500000LL; + + auto fadeOutAnimation = ref new DoubleAnimation(); + fadeOutAnimation->To = 0.0; + fadeOutAnimation->Duration = Duration(Windows::Foundation::TimeSpan{ durationTicks }); + + Storyboard::SetTarget(fadeOutAnimation, this->pageBackgroundBorder); + Storyboard::SetTargetProperty(fadeOutAnimation, "Opacity"); + + auto fadeOutSB = ref new Storyboard(); + fadeOutSB->Children->Append(fadeOutAnimation); + + Platform::WeakReference weakThis(this); + fadeOutSB->Completed += ref new EventHandler( + [weakThis, newImage](Platform::Object^ sender, Platform::Object^ e) { + try { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + + auto localTicks = (long long)std::llround(that->backgroundTransitionDurationMs * 10000.0); + if (localTicks <= 0) localTicks = 2500000LL; + + try { + auto brush = dynamic_cast(that->pageBackgroundBorder->Background); + if (brush != nullptr) { + brush->ImageSource = newImage; + } + } catch(...) {} + + that->OnPropertyChanged("CurrentBackgroundImage"); + + auto fadeInAnimation = ref new DoubleAnimation(); + fadeInAnimation->From = 0.0; + fadeInAnimation->To = that->backgroundOverlayOpacity; + fadeInAnimation->Duration = Duration(Windows::Foundation::TimeSpan{ localTicks }); + + Storyboard::SetTarget(fadeInAnimation, that->pageBackgroundBorder); + Storyboard::SetTargetProperty(fadeInAnimation, "Opacity"); + + auto fadeInSB = ref new Storyboard(); + fadeInSB->Children->Append(fadeInAnimation); + fadeInSB->Begin(); + } catch(...) {} + }); + + fadeOutSB->Begin(); + } catch(...) {} +} diff --git a/UI/Models/ViewModels/AppPageViewModel.h b/UI/Models/ViewModels/AppPageViewModel.h new file mode 100644 index 00000000..bbc2725d --- /dev/null +++ b/UI/Models/ViewModels/AppPageViewModel.h @@ -0,0 +1,32 @@ +#pragma once + +#include "pch.h" + +namespace moonlight_xbox_dx { + + [Windows::UI::Xaml::Data::Bindable] + public ref class AppPageViewModel sealed : Windows::UI::Xaml::Data::INotifyPropertyChanged + { + private: + Windows::UI::Xaml::Media::Imaging::BitmapImage^ currentBackgroundImage; + Windows::UI::Xaml::Controls::Border^ pageBackgroundBorder; + double backgroundTransitionDurationMs; + double backgroundOverlayOpacity; + + public: + AppPageViewModel(); + + virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; + void OnPropertyChanged(Platform::String^ propertyName); + + property Windows::UI::Xaml::Media::Imaging::BitmapImage^ CurrentBackgroundImage + { + Windows::UI::Xaml::Media::Imaging::BitmapImage^ get() { return this->currentBackgroundImage; } + } + + void TransitionToBlurredImage(Windows::UI::Xaml::Media::Imaging::BitmapImage^ newImage); + + void SetPageBackgroundBorder(Windows::UI::Xaml::Controls::Border^ border); + void SetBackgroundTransitionSettings(double durationMs, double overlayOpacity); + }; +} diff --git a/UI/Pages/AppPage/AppPage.Resources.Grid.xaml b/UI/Pages/AppPage/AppPage.Resources.Grid.xaml new file mode 100644 index 00000000..5769e0ea --- /dev/null +++ b/UI/Pages/AppPage/AppPage.Resources.Grid.xaml @@ -0,0 +1,106 @@ + + + + + + + + + diff --git a/UI/Pages/AppPage/AppPage.Resources.List.xaml b/UI/Pages/AppPage/AppPage.Resources.List.xaml new file mode 100644 index 00000000..59d56e5b --- /dev/null +++ b/UI/Pages/AppPage/AppPage.Resources.List.xaml @@ -0,0 +1,164 @@ + + + + + + + + + diff --git a/UI/Pages/AppPage/AppPage.Resources.xaml b/UI/Pages/AppPage/AppPage.Resources.xaml new file mode 100644 index 00000000..68ed290a --- /dev/null +++ b/UI/Pages/AppPage/AppPage.Resources.xaml @@ -0,0 +1,102 @@ + + + + + + 0:0:0.125 + + + 1.5 + 0.2 + -150 + + + 0.66 + 16 + 0,0,0,0 + 32,0,32,0 + + 1.0 + 1.2 + 1.15 + 0.75 + 1.0 + + 0.4 + 0.0 + + 0.0 + 0.6 + + + + + + + + + + + diff --git a/UI/Pages/AppPage/AppPage.Selection.cpp b/UI/Pages/AppPage/AppPage.Selection.cpp new file mode 100644 index 00000000..ea5d6e33 --- /dev/null +++ b/UI/Pages/AppPage/AppPage.Selection.cpp @@ -0,0 +1,755 @@ +#include "pch.h" +#include +#include +#include +#include +#include "AppPage.xaml.h" +#include "UI\Utilities\ImageHelpers.h" +#define MLOG_TAG_OVERRIDE "AppPage" +#include "Utils.hpp" + +using namespace Platform; +using namespace Windows::ApplicationModel::Core; +using namespace Windows::Foundation; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Hosting; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::Graphics::Imaging; +using namespace Windows::Graphics::Display; +using namespace Windows::UI::Xaml::Media::Imaging; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace concurrency; + +namespace moonlight_xbox_dx { + +namespace { + +static Platform::String ^ BlurCachePath(Platform::String ^ imagePath, bool isGlow) { + if (imagePath == nullptr) return nullptr; + const wchar_t *s = imagePath->Data(); + int len = (int)wcslen(s); + int lastSlash = -1, lastDot = -1; + for (int i = len - 1; i >= 0; i--) { + if (s[i] == L'\\' && lastSlash < 0) lastSlash = i; + if (s[i] == L'.' && lastDot < 0) lastDot = i; + } + if (lastSlash < 0 || lastDot <= lastSlash) return nullptr; + auto dir = ref new Platform::String(s, lastSlash + 1); + auto base = ref new Platform::String(s + lastSlash + 1, lastDot - lastSlash - 1); + auto blurDir = Platform::String::Concat(dir, ref new Platform::String(L"blur\\")); + auto suffix = ref new Platform::String(isGlow ? L"_glow.png" : L"_bg.png"); + return Platform::String::Concat(blurDir, Platform::String::Concat(base, suffix)); +} + +static void SaveBlurStreamSync(IRandomAccessStream ^ stream, Platform::String ^ filePath) { + if (stream == nullptr || filePath == nullptr) return; + const wchar_t *s = filePath->Data(); + int len = (int)wcslen(s); + int lastSlash = -1; + for (int i = len - 1; i >= 0; i--) { + if (s[i] == L'\\') { + lastSlash = i; + break; + } + } + if (lastSlash < 0) return; + auto dirPath = ref new Platform::String(s, lastSlash); + auto fileName = ref new Platform::String(s + lastSlash + 1); + CreateDirectory(dirPath->Data(), nullptr); + auto folder = create_task(StorageFolder::GetFolderFromPathAsync(dirPath)).get(); + auto file = create_task(folder->CreateFileAsync(fileName, + CreationCollisionOption::ReplaceExisting)) + .get(); + auto fs = create_task(file->OpenAsync(FileAccessMode::ReadWrite)).get(); + create_task(RandomAccessStream::CopyAsync( + stream->GetInputStreamAt(0), fs->GetOutputStreamAt(0))) + .get(); + create_task(fs->FlushAsync()).get(); +} + +static concurrency::task OpenBlurCacheStreamAsync(Platform::String ^ filePath) { + if (filePath == nullptr || GetFileAttributes(filePath->Data()) == INVALID_FILE_ATTRIBUTES) + return task_from_result(nullptr); + return create_task(StorageFile::GetFileFromPathAsync(filePath)) + .then([](StorageFile ^ file) -> concurrency::task { + if (file == nullptr) return task_from_result(nullptr); + return create_task(file->OpenReadAsync()) + .then([](IRandomAccessStream ^ s) -> IRandomAccessStream ^ { return s; }); + }); +} + +} + +void AppPage::AppsGrid_SelectionChanged(Platform::Object ^ sender, SelectionChangedEventArgs ^ e) { + + if (m_suppressSelectionVisuals) return; + + auto lv = dynamic_cast(sender); + if (lv == nullptr || lv->SelectedIndex < 0) return; + + auto app = dynamic_cast(lv->SelectedItem); + if (app == nullptr) return; + if (app == nullptr) return; + + ApplySelectionVisuals(app, true); +} + +void AppPage::ApplySelectionVisuals(MoonlightApp ^ app, bool animate, bool centerImmediate) { + + if (app == nullptr) return; + + MoonlightApp ^ prev = m_selectedApp; + if (prev != nullptr && prev->Id != app->Id) { + try { + prev->IsSelected = false; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "ApplySelectionVisuals: prev->IsSelected=false failed for app id=%d, stale selected visual may persist\n", prev->Id); + } + UpdateContainerSelectionState(prev, false, animate); + } + + m_selectedApp = app; + try { + app->IsSelected = true; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "ApplySelectionVisuals: app->IsSelected=true failed for app id=%d, selection visual desynced\n", app->Id); + } + UpdateContainerSelectionState(app, true, animate); + + if (app->BlurredImage == nullptr) + BlurAppImage(app); + else + FadeInBlurIfSelected(app, app->BlurredImage); + + UpdateSelectedAppBox(app, prev, animate); + + if (centerImmediate) { + CenterSelectedItem(true); + return; + } + + CenterSelectedItem(false); + + try { + if (m_centerDebounceTimer == nullptr) { + m_centerDebounceTimer = ref new Windows::UI::Xaml::DispatcherTimer(); + m_centerDebounceTimer->Interval = Windows::Foundation::TimeSpan{800000LL}; + Platform::WeakReference weakThis(this); + m_centerDebounceTimer_token = m_centerDebounceTimer->Tick += + ref new Windows::Foundation::EventHandler([weakThis](Platform::Object ^, Platform::Object ^) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + that->m_centerDebounceTimer->Stop(); + } catch (...) { + } + try { + that->CenterSelectedItem(false); + } catch (...) { + } + }); + } + m_centerDebounceTimer->Stop(); + m_centerDebounceTimer->Start(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "ApplySelectionVisuals failed to set up center debounce timer, debounced re-centering permanently disabled"); + } +} + +void AppPage::UpdateContainerSelectionState(MoonlightApp ^ app, bool isSelected, bool animate) { + if (app == nullptr) return; + Windows::UI::Xaml::Controls::ListView ^ grids[] = {this->AppsListView, this->AppsGridView}; + for (auto grid : grids) { + if (grid == nullptr) continue; + try { + auto container = dynamic_cast(grid->ContainerFromItem(app)); + if (container == nullptr) continue; + VisualStateManager::GoToState(container, isSelected ? "Selected" : "Unselected", animate); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "UpdateContainerSelectionState: GoToState failed for app id=%d, isSelected=%d\n", app->Id, isSelected); + } + } +} + +void AppPage::UpdateSelectedAppBox(MoonlightApp ^ app, MoonlightApp ^ prev, bool animate) { + try { + if (this->SelectedAppText == nullptr || this->SelectedAppBox == nullptr) return; + Platform::String ^ newText = app->Name != nullptr ? app->Name : ref new Platform::String(L""); + this->SelectedAppBox->Visibility = Windows::UI::Xaml::Visibility::Visible; + this->SelectedAppText->Visibility = Windows::UI::Xaml::Visibility::Visible; + this->SelectedAppText->Foreground = ref new SolidColorBrush(Windows::UI::Colors::White); + + Windows::UI::Xaml::Media::Animation::Storyboard ^ showSb = nullptr; + Windows::UI::Xaml::Media::Animation::Storyboard ^ hideSb = nullptr; + try { + auto res = this->Resources; + if (res != nullptr) { + try { + showSb = dynamic_cast(res->Lookup(ref new Platform::String(L"ShowSelectedAppStoryboard"))); + } catch (...) { + } + try { + hideSb = dynamic_cast(res->Lookup(ref new Platform::String(L"HideSelectedAppStoryboard"))); + } catch (...) { + } + } + } catch (...) { + } + + const unsigned int animVer = ++m_appTextAnimVersion; + auto weakThis = WeakReference(this); + auto capturedText = newText; + AnimateCrossfadeText( + this->SelectedAppBox, showSb, hideSb, animate && prev != nullptr, + [weakThis, capturedText]() { + auto that = weakThis.Resolve(); + if (that != nullptr && that->SelectedAppText != nullptr) + that->SelectedAppText->Text = capturedText; + }, + [weakThis, animVer]() { + auto that = weakThis.Resolve(); + return that != nullptr && that->m_appTextAnimVersion == animVer; + }); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "ApplySelectionVisuals: text overlay update failed for app id=%d\n", app->Id); + } +} + +void AppPage::CenterSelectedItem(bool immediate) { + + auto lv = this->ActiveAppsGrid(); + ListViewItem ^ container = nullptr; + auto status = ResolveSelectedContainer(lv, m_scrollViewer, container); + if (status == SelectedContainerStatus::NoSelection) return; + if (status == SelectedContainerStatus::ScrollViewerMissing) { + WaitForScrollViewerThenCenter(immediate); + return; + } + if (status == SelectedContainerStatus::ContainerMissing) { + WaitForContainerThenCenter(immediate); + return; + } + + try { + lv->UpdateLayout(); + } catch (...) { + } + if (container->ActualWidth <= 0.0 || container->ActualHeight <= 0.0) return; + + double viewport = m_isGridLayout ? m_scrollViewer->ViewportHeight : m_scrollViewer->ViewportWidth; + if (viewport <= 0.0) { + try { + lv->UpdateLayout(); + } catch (...) { + } + viewport = m_isGridLayout ? m_scrollViewer->ViewportHeight : m_scrollViewer->ViewportWidth; + } + if (viewport <= 0.0) return; + + try { + double desiredEdgePadding = m_isGridLayout + ? 0.0 + : std::max(0.0, (viewport - container->ActualWidth) * 0.5); + if (ApplyEdgeCenteringPadding(lv, desiredEdgePadding) == EdgeCenteringPaddingResult::Applied) { + try { + lv->UpdateLayout(); + } catch (...) { + } + } + } catch (...) { + } + + if (!m_initialFocusApplied) { + m_initialFocusApplied = true; + try { + if (container != nullptr) + container->Focus(Windows::UI::Xaml::FocusState::Programmatic); + else + lv->Focus(Windows::UI::Xaml::FocusState::Programmatic); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "CenterSelectedItem: initial Focus() failed\n"); + } + } + + CenterContainerInScrollViewer(m_scrollViewer, container, m_isGridLayout, immediate); + + RevealActiveAppsGrid(); + + m_entranceAnimationsArmed = false; +} + +void AppPage::WaitForContainerThenCenter(bool immediate) { + auto lv = this->ActiveAppsGrid(); + if (lv == nullptr || m_centerWaitContainer_token.Value != 0) return; + m_centerWaitContainerTarget = lv; + Platform::WeakReference weakThis(this); + m_centerWaitContainer_token = ArmContainerRealizedWait(lv, [weakThis, immediate]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->m_centerWaitContainer_token.Value = 0; + that->m_centerWaitContainerTarget = nullptr; + that->CenterSelectedItem(immediate); + }); + if (m_centerWaitContainer_token.Value == 0) m_centerWaitContainerTarget = nullptr; +} + +void AppPage::WaitForScrollViewerThenCenter(bool immediate) { + auto lv = this->ActiveAppsGrid(); + if (lv == nullptr || m_centerWaitScrollViewer_token.Value != 0) return; + m_centerWaitScrollViewerTarget = lv; + Platform::WeakReference weakThis(this); + m_centerWaitScrollViewer_token = ArmLayoutUpdatedWait(lv, [weakThis, immediate]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->m_centerWaitScrollViewer_token.Value = 0; + that->m_centerWaitScrollViewerTarget = nullptr; + that->CenterSelectedItem(immediate); + }); +} + +void AppPage::UpdateItemHeights() { + try { + if (m_isGridLayout) return; + if (this->AppsListView == nullptr) return; + + for (unsigned int i = 0; i < this->AppsListView->Items->Size; ++i) { + auto container = dynamic_cast(this->AppsListView->ContainerFromIndex(i)); + if (container == nullptr) continue; + + std::function find = [&](DependencyObject ^ parent) -> DependencyObject ^ { + if (parent == nullptr) return nullptr; + int count = VisualTreeHelper::GetChildrenCount(parent); + for (int j = 0; j < count; ++j) { + auto child = VisualTreeHelper::GetChild(parent, j); + auto fe = dynamic_cast(child); + if (fe != nullptr && fe->GetType()->FullName == "moonlight_xbox_dx.AspectRatioBox") return child; + auto rec = find(child); + if (rec != nullptr) return rec; + } + return nullptr; + }; + + auto found = find(container); + if (found == nullptr) continue; + auto fe = dynamic_cast(found); + if (fe == nullptr) continue; + if (std::isnan(fe->Height)) continue; + + fe->Height = std::nan(""); + fe->InvalidateMeasure(); + fe->UpdateLayout(); + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "UpdateItemHeights: failed, stale Height may cause clipped artwork\n"); + } +} + +void AppPage::ArmEntranceAnimations() { + m_entranceAnimationsArmed = true; + m_gridEntranceAnimatedIndices.clear(); + m_listEntranceAnimatedIndices.clear(); +} + +void AppPage::AppsGrid_ContainerContentChanging( + Windows::UI::Xaml::Controls::ListViewBase ^ sender, + Windows::UI::Xaml::Controls::ContainerContentChangingEventArgs ^ args) { + auto container = dynamic_cast(args->ItemContainer); + if (container == nullptr) return; + + if (args->InRecycleQueue) { + try { + Windows::UI::Xaml::Thickness zero; + zero.Left = zero.Top = zero.Right = zero.Bottom = 0.0; + container->Margin = zero; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "AppsGrid_ContainerContentChanging: Margin reset failed on recycle, stale margin may persist into reuse\n"); + } + return; + } + + auto &entranceAnimated = (sender == this->AppsGridView) ? m_gridEntranceAnimatedIndices : m_listEntranceAnimatedIndices; + + if (args->Phase == 0) { + try { + if (entranceAnimated.find(args->ItemIndex) == entranceAnimated.end()) { + entranceAnimated.insert(args->ItemIndex); + bool isCurrentSelection = sender->SelectedItem != nullptr && args->Item == sender->SelectedItem; + if (m_entranceAnimationsArmed && !isCurrentSelection) + PlayEntranceAnimation(container, args->ItemIndex); + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "AppsGrid_ContainerContentChanging: entrance animation failed\n"); + } + + Platform::WeakReference weakThis(this); + args->RegisterUpdateCallback( + ref new TypedEventHandler( + [weakThis](ListViewBase ^ s, ContainerContentChangingEventArgs ^ a) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + that->AppsGrid_ContainerContentChanging(s, a); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "AppsGrid_ContainerContentChanging: Phase1 recursive dispatch failed\n"); + } + })); + return; + } + + try { + auto app = dynamic_cast(args->Item); + if (app != nullptr) { + VisualStateManager::GoToState(container, app->IsSelected ? "Selected" : "Unselected", false); + if (app->IsSelected && app->BlurredImage != nullptr) + FadeInBlurIfSelected(app, app->BlurredImage); + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "AppsGrid_ContainerContentChanging: Phase1 blur recovery failed\n"); + } +} + +void AppPage::PlayEntranceAnimation(Windows::UI::Xaml::Controls::ListViewItem ^ container, int index) { + using namespace Windows::UI::Xaml::Media; + using namespace Windows::UI::Xaml::Media::Animation; + + auto prevSb = dynamic_cast(container->Tag); + if (prevSb != nullptr) { + try { + prevSb->Stop(); + } catch (...) { + } + } + + auto scale = ref new ScaleTransform(); + scale->ScaleX = kEntranceStartScale; + scale->ScaleY = kEntranceStartScale; + container->RenderTransformOrigin = Windows::Foundation::Point(0.5f, 0.5f); + container->RenderTransform = scale; + container->Opacity = 0.0; + + int staggerIndex = index < kEntranceMaxStaggerItems ? index : kEntranceMaxStaggerItems; + Windows::UI::Xaml::Duration animDuration(TimeSpan{(long long)kEntranceDurationMs * 10000LL}); + TimeSpan beginTime{(long long)staggerIndex * kEntranceStaggerMs * 10000LL}; + + auto ease = ref new QuadraticEase(); + ease->EasingMode = EasingMode::EaseOut; + + auto sb = ref new Storyboard(); + + auto opacityAnim = ref new DoubleAnimation(); + opacityAnim->To = 1.0; + opacityAnim->Duration = animDuration; + opacityAnim->BeginTime = beginTime; + opacityAnim->EasingFunction = ease; + Storyboard::SetTarget(opacityAnim, container); + Storyboard::SetTargetProperty(opacityAnim, ref new Platform::String(L"Opacity")); + sb->Children->Append(opacityAnim); + + auto scaleXAnim = ref new DoubleAnimation(); + scaleXAnim->To = 1.0; + scaleXAnim->Duration = animDuration; + scaleXAnim->BeginTime = beginTime; + scaleXAnim->EasingFunction = ease; + scaleXAnim->EnableDependentAnimation = true; + Storyboard::SetTarget(scaleXAnim, container); + Storyboard::SetTargetProperty(scaleXAnim, ref new Platform::String(L"(UIElement.RenderTransform).(ScaleTransform.ScaleX)")); + sb->Children->Append(scaleXAnim); + + auto scaleYAnim = ref new DoubleAnimation(); + scaleYAnim->To = 1.0; + scaleYAnim->Duration = animDuration; + scaleYAnim->BeginTime = beginTime; + scaleYAnim->EasingFunction = ease; + scaleYAnim->EnableDependentAnimation = true; + Storyboard::SetTarget(scaleYAnim, container); + Storyboard::SetTargetProperty(scaleYAnim, ref new Platform::String(L"(UIElement.RenderTransform).(ScaleTransform.ScaleY)")); + sb->Children->Append(scaleYAnim); + + container->Tag = sb; + sb->Begin(); +} + +void AppPage::BlurAppImage(MoonlightApp ^ selApp) { + try { + if (selApp->BlurredImage != nullptr) return; + if (m_blurInProgressIds.count(selApp->Id)) return; + + static Platform::String ^ placeholderImagePath = ref new Platform::String(L"ms-appx:///Assets/gamepad.svg"); + if (selApp->ImagePath == nullptr || selApp->ImagePath->Equals(placeholderImagePath)) { + Platform::WeakReference weakThis(this); + Platform::WeakReference weakApp(selApp); + auto token = std::make_shared(); + *token = selApp->PropertyChanged += ref new Windows::UI::Xaml::Data::PropertyChangedEventHandler( + [weakThis, weakApp, token](Platform::Object ^, Windows::UI::Xaml::Data::PropertyChangedEventArgs ^ e) { + if (e->PropertyName != "ImagePath") return; + auto app = weakApp.Resolve(); + if (app == nullptr) return; + try { app->PropertyChanged -= *token; } catch (...) { } + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { that->BlurAppImage(app); } catch (...) { } + }); + return; + } + + m_blurInProgressIds.insert(selApp->Id); + bool isGrid = this->m_isGridLayout; + Platform::WeakReference weakThis(this); + + float glowBlurAmount = 16.0f; + try { + auto boxed = Resources->Lookup("BlurAmount"); + auto pv = dynamic_cast(boxed); + if (pv != nullptr) glowBlurAmount = (float)pv->GetDouble(); + } catch (...) { + } + + auto getOrComputeStream = [this, selApp](float blurDip, float padDip, + Platform::String ^ cachePath) + -> concurrency::task { + if (cachePath != nullptr && GetFileAttributes(cachePath->Data()) != INVALID_FILE_ATTRIBUTES) + return OpenBlurCacheStreamAsync(cachePath); + return ApplyBlur(selApp, blurDip, padDip) + .then([cachePath](IRandomAccessStream ^ stream) -> IRandomAccessStream ^ { + if (stream != nullptr && cachePath != nullptr) { + try { + SaveBlurStreamSync(stream, cachePath); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: cache write failed, blur will be recomputed every launch\n"); + } + stream->Seek(0); + } + return stream; + }, + concurrency::task_continuation_context::use_arbitrary()); + }; + + auto bgCachePath = BlurCachePath(selApp->ImagePath, false); + try { + getOrComputeStream(kBlurAmountBackground, 0.0f, bgCachePath) + .then([selApp, weakThis](IRandomAccessStream ^ stream) { + try { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->HandleBlurStreamReady(selApp, weakThis, stream, true); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: background RunAsync dispatch failed for app id=%d, m_blurInProgressIds entry stuck\n", selApp->Id); + } + }, + concurrency::task_continuation_context::use_current()); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: background getOrComputeStream setup failed for app id=%d, m_blurInProgressIds entry stuck\n", selApp->Id); + } + + if (!isGrid) { + auto glowCachePath = BlurCachePath(selApp->ImagePath, true); + try { + getOrComputeStream(glowBlurAmount, kBlurGlowPaddingDip, glowCachePath) + .then([selApp, weakThis](IRandomAccessStream ^ stream) { + try { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->HandleBlurStreamReady(selApp, weakThis, stream, false); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: glow RunAsync dispatch failed for app id=%d, GlowImage permanently null\n", selApp->Id); + } + }, + concurrency::task_continuation_context::use_current()); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: glow getOrComputeStream setup failed for app id=%d, GlowImage permanently null\n", selApp->Id); + } + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: unhandled failure for app id=%d, m_blurInProgressIds entry may be stuck\n", selApp->Id); + } +} + +concurrency::task AppPage::ApplyBlur(MoonlightApp ^ app, float blurDip, float padDip) { + if (app == nullptr) return concurrency::task_from_result(nullptr); + + Platform::String ^ path = app->ImagePath; + + return concurrency::create_task(ImageHelpers::LoadSoftwareBitmapFromUriOrPathAsync(path)) + .then([this, app, blurDip, padDip](SoftwareBitmap ^ softwareBitmap) -> concurrency::task { + if (softwareBitmap == nullptr) return concurrency::task_from_result(nullptr); + + unsigned int ui_targetW = 0, ui_targetH = 0; + double ui_dpi = 96.0; + try { + auto activeGrid = this->ActiveAppsGrid(); + FrameworkElement ^ fe_for_size = dynamic_cast(this->FindName("AppImageRect")); + if (fe_for_size == nullptr && activeGrid != nullptr) { + auto container = dynamic_cast(activeGrid->ContainerFromItem(app)); + if (container != nullptr) { + auto found = FindChildByName(container, ref new Platform::String(L"AppImageRect")); + if (found == nullptr) found = FindChildByName(container, ref new Platform::String(L"AppImageBlurRect")); + fe_for_size = dynamic_cast(found); + } + } + + if (fe_for_size != nullptr) { + double aw = 0.0, ah = 0.0; + try { + aw = fe_for_size->ActualWidth; + } catch (...) { + } + try { + ah = fe_for_size->ActualHeight; + } catch (...) { + } + + if ((aw <= 0 || ah <= 0) && activeGrid != nullptr) { + try { + auto container = dynamic_cast(activeGrid->ContainerFromItem(app)); + if (container != nullptr) { + auto tryFE = [&](const wchar_t *name) { + auto fe = dynamic_cast(FindChildByName(container, ref new Platform::String(name))); + if (fe != nullptr) { + try { + if (aw <= 0) aw = fe->ActualWidth; + } catch (...) { + } + try { + if (ah <= 0) ah = fe->ActualHeight; + } catch (...) { + } + } + }; + tryFE(L"AppAspectRatioBox"); + tryFE(L"ItemGrid"); + tryFE(L"AppImageBlurRect"); + if (aw <= 0) { + try { + aw = container->ActualWidth; + } catch (...) { + } + } + } + if (aw <= 0) { + try { + aw = activeGrid->ActualWidth; + } catch (...) { + } + } + } catch (...) { + } + } + + if (aw > 0 && ah > 0) { + try { + auto di = DisplayInformation::GetForCurrentView(); + double dpi = di != nullptr ? di->LogicalDpi : 96.0; + ui_dpi = dpi; + ui_targetW = (unsigned int)std::max(1u, (unsigned int)std::round(aw * dpi / 96.0)); + ui_targetH = (unsigned int)std::max(1u, (unsigned int)std::round(ah * dpi / 96.0)); + } catch (...) { + ui_targetW = ui_targetH = 0; + ui_dpi = 96.0; + } + } + } + } catch (...) { + ui_targetW = ui_targetH = 0; + } + + unsigned int targetW = ui_targetW != 0 ? ui_targetW : softwareBitmap->PixelWidth; + unsigned int targetH = ui_targetH != 0 ? ui_targetH : softwareBitmap->PixelHeight; + + unsigned int glowTargetW = targetW, glowTargetH = targetH; + if (padDip > 0.0f && targetW > 0 && targetH > 0) { + unsigned int padPx = (unsigned int)std::round((double)padDip * ui_dpi / 96.0); + if (padPx > 0) { + glowTargetW = targetW + 2 * padPx; + glowTargetH = targetH + 2 * padPx; + } + } + + try { + ImageHelpers::AdjustSaturation(softwareBitmap, kBackgroundSaturation); + } catch (...) { + } + return ImageHelpers::CreateMaskedBlurredPngStreamAsync( + softwareBitmap, glowTargetW, glowTargetH, ui_dpi, blurDip); + }); +} + +void AppPage::HandleBlurStreamReady(MoonlightApp ^ selApp, Platform::WeakReference weakThis, + IRandomAccessStream ^ stream, bool isBackground) { + if (stream == nullptr) { + if (!isBackground) return; + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: background stream null for app id=%d\n", selApp->Id); + m_blurInProgressIds.erase(selApp->Id); + return; + } + + try { + if (selApp == nullptr) return; + auto img = ref new BitmapImage(); + create_task(img->SetSourceAsync(stream)) + .then([weakThis, selApp, img, isBackground]() { + try { + auto thatCont = weakThis.Resolve(); + if (thatCont == nullptr) return; + if (isBackground) { + thatCont->m_blurInProgressIds.erase(selApp->Id); + selApp->BlurredImage = img; + if (thatCont->m_selectedApp != nullptr && + thatCont->m_selectedApp != selApp && + thatCont->m_selectedApp->Id == selApp->Id) + thatCont->m_selectedApp->BlurredImage = img; + try { + thatCont->FadeInBlurIfSelected(selApp, img); + } catch (...) { + } + } else { + selApp->GlowImage = img; + if (thatCont->m_selectedApp != nullptr && + thatCont->m_selectedApp != selApp && + thatCont->m_selectedApp->Id == selApp->Id) + thatCont->m_selectedApp->GlowImage = img; + } + } catch (...) { + if (isBackground) + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: assign-BlurredImage step failed for app id=%d, m_blurInProgressIds entry stuck\n", selApp->Id); + else + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: assign-GlowImage step failed for app id=%d, GlowImage permanently null (BlurredImage gate blocks retry)\n", selApp->Id); + } + }, + concurrency::task_continuation_context::use_current()); + } catch (...) { + if (isBackground) + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: SetSourceAsync setup failed for app id=%d, m_blurInProgressIds entry stuck\n", selApp->Id); + else + MLOGF(Utils::LogLevel::Warning, "BlurAppImage: glow SetSourceAsync setup failed for app id=%d, GlowImage permanently null\n", selApp->Id); + } +} + +void AppPage::FadeInBlurIfSelected(MoonlightApp ^ app, BitmapImage ^ img) { + if (app == nullptr || img == nullptr) return; + + bool isSelected = false; + try { + auto activeGrid = this->ActiveAppsGrid(); + if (activeGrid != nullptr) { + auto selItem = dynamic_cast(activeGrid->SelectedItem); + if (selItem != nullptr && selItem->Id == app->Id) isSelected = true; + } + if (!isSelected && m_selectedApp != nullptr && m_selectedApp->Id == app->Id) + isSelected = true; + } catch (...) { + } + + if (!isSelected) return; + + try { + auto vm = this->ViewModel; + if (vm != nullptr) vm->TransitionToBlurredImage(img); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "FadeInBlurIfSelected: TransitionToBlurredImage failed for app id=%d\n", app->Id); + } +} + +} diff --git a/UI/Pages/AppPage/AppPage.xaml b/UI/Pages/AppPage/AppPage.xaml new file mode 100644 index 00000000..d26877d0 Binary files /dev/null and b/UI/Pages/AppPage/AppPage.xaml differ diff --git a/UI/Pages/AppPage/AppPage.xaml.cpp b/UI/Pages/AppPage/AppPage.xaml.cpp new file mode 100644 index 00000000..8420f870 --- /dev/null +++ b/UI/Pages/AppPage/AppPage.xaml.cpp @@ -0,0 +1,1330 @@ +#include "pch.h" +#include "AppPage.xaml.h" +#include +#include +#include +#include +#include "State\MoonlightClient.h" +#include "UI\Controls\SlidingMenu.xaml.h" +#include "UI\Modals\AlertDialog.xaml.h" +#include "UI\Modals\AppActionsDialog.xaml.h" +#include "UI\Modals\ConfirmDialog.xaml.h" +#include "UI\Models\ViewModels\AppPageViewModel.h" +#include "UI\Pages\HostSelectorPage.xaml.h" +#include "UI\Pages\HostSettingsPage.xaml.h" +#include "UI\Pages\MoonlightSettings.xaml.h" +#include "UI\Pages\StreamPage.xaml.h" +#define MLOG_TAG_OVERRIDE "AppPage" +#include "Utils.hpp" + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::ApplicationModel::Core; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Hosting; +using namespace Windows::UI::Xaml::Navigation; +using namespace Windows::Foundation::Numerics; +using namespace concurrency; + +namespace moonlight_xbox_dx { + +namespace { + +static double ResolveBackgroundOverlayOpacityFromPageResources(Page ^ page) { + double overlayOpacity = 0.05; + if (page == nullptr || page->Resources == nullptr) return overlayOpacity; + + try { + auto value = page->Resources->Lookup(ref new Platform::String(L"BackgroundOverlayOpacity")); + auto pv = dynamic_cast(value); + if (pv != nullptr) { + overlayOpacity = pv->GetDouble(); + } + } catch (...) { + } + + if (overlayOpacity < 0.0) overlayOpacity = 0.0; + if (overlayOpacity > 1.0) overlayOpacity = 1.0; + return overlayOpacity; +} + +static double ResolveSharedAnimationDurationMsFromPageResources(Page ^ page) { + if (page == nullptr || page->Resources == nullptr) return 250.0; + + try { + auto value = page->Resources->Lookup(ref new Platform::String(L"SharedAnimationDuration")); + auto durationValue = dynamic_cast(value); + return Utils::DurationStringToMs(durationValue); + } catch (...) { + } + + return 250.0; +} + +static void FindElementChildren(DependencyObject ^ container, + UIElement ^ &outDesaturator, UIElement ^ &outImage, UIElement ^ &outName, + UIElement ^ &outBlur, UIElement ^ &outPlay) { + outDesaturator = outImage = outName = outBlur = outPlay = nullptr; + if (container == nullptr) return; + try { + outDesaturator = dynamic_cast(FindChildByName(container, ref new Platform::String(L"Desaturator"))); + outImage = dynamic_cast(FindChildByName(container, ref new Platform::String(L"AppImageRect"))); + outName = dynamic_cast(FindChildByName(container, ref new Platform::String(L"AppName"))); + outBlur = dynamic_cast(FindChildByName(container, ref new Platform::String(L"AppImageBlurRect"))); + outPlay = dynamic_cast(FindChildByName(container, ref new Platform::String(L"Play"))); + } catch (...) { + outDesaturator = outImage = outName = outBlur = outPlay = nullptr; + } +} + +} + +Controls::SlidingMenu ^ AppPage::GetLeftMenu() { + try { + if (m_leftMenu == nullptr) m_leftMenu = ref new Controls::SlidingMenu(); + return m_leftMenu; + } catch (...) { + return nullptr; + } +} + +AppPage::AppPage() { + InitializeComponent(); + Windows::UI::ViewManagement::ApplicationView::GetForCurrentView() + ->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseCoreWindow); + + m_apps_changed_token.Value = m_back_cookie.Value = m_keydown_cookie.Value = 0; + m_centerWaitContainer_token.Value = 0; + m_centerWaitScrollViewer_token.Value = 0; + m_appsListView_selection_token.Value = m_appsListView_itemclick_token.Value = 0; + m_appsListView_righttapped_token.Value = 0; + m_appsListView_loaded_token.Value = m_appsListView_ccc_token.Value = 0; + m_appsGridView_selection_token.Value = m_appsGridView_itemclick_token.Value = 0; + m_appsGridView_righttapped_token.Value = 0; + m_appsGridView_loaded_token.Value = m_appsGridView_ccc_token.Value = 0; + m_searchbox_gettingfocus_token.Value = 0; + m_centerDebounceTimer_token.Value = 0; + + { + auto weakThis = WeakReference(this); + this->Loaded += ref new RoutedEventHandler([weakThis](Platform::Object ^ s, RoutedEventArgs ^ e) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + that->OnLoaded(s, e); + } catch (...) { + MLOGF(Utils::LogLevel::Error, "OnLoaded failed, page lifecycle wiring may be incomplete\n"); + } + }); + } + { + auto weakThis = WeakReference(this); + this->Unloaded += ref new RoutedEventHandler([weakThis](Platform::Object ^ s, RoutedEventArgs ^ e) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + that->OnUnloaded(s, e); + } catch (...) { + MLOGF(Utils::LogLevel::Error, "OnUnloaded failed, cleanup may be incomplete\n"); + } + }); + } + try { + WireAppsGridEvents(this->AppsListView, + m_appsListView_selection_token, m_appsListView_itemclick_token, + m_appsListView_righttapped_token, m_appsListView_loaded_token, m_appsListView_ccc_token); + } catch (...) { + MLOGF(Utils::LogLevel::Error, "AppsListView event wiring failed, selection/click/layout events permanently unavailable\n"); + } + + try { + WireAppsGridEvents(this->AppsGridView, + m_appsGridView_selection_token, m_appsGridView_itemclick_token, + m_appsGridView_righttapped_token, m_appsGridView_loaded_token, m_appsGridView_ccc_token); + } catch (...) { + MLOGF(Utils::LogLevel::Error, "AppsGridView event wiring failed, selection/click/layout events permanently unavailable\n"); + } + + try { + if (this->SearchBox != nullptr) { + auto weakThis = WeakReference(this); + m_searchbox_gettingfocus_token = this->SearchBox->GettingFocus += + ref new Windows::Foundation::TypedEventHandler< + Windows::UI::Xaml::UIElement ^, + Windows::UI::Xaml::Input::GettingFocusEventArgs ^>( + [weakThis](Windows::UI::Xaml::UIElement ^, Windows::UI::Xaml::Input::GettingFocusEventArgs ^ args) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + if (that->m_searchIsOpen) return; + if (args->Direction == FocusNavigationDirection::Up) { + that->m_searchIsOpen = true; + return; + } + args->Cancel = true; + } catch (...) { + } + }); + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "SearchBox GettingFocus wiring failed, DPad-Up-to-search navigation permanently unavailable\n"); + } + + m_filteredApps = ref new Platform::Collections::Vector(); +} + +void AppPage::WireAppsGridEvents( + ListView ^ grid, + Windows::Foundation::EventRegistrationToken &selectionToken, + Windows::Foundation::EventRegistrationToken &itemClickToken, + Windows::Foundation::EventRegistrationToken &rightTappedToken, + Windows::Foundation::EventRegistrationToken &loadedToken, + Windows::Foundation::EventRegistrationToken &cccToken) { + if (grid == nullptr) return; + auto weakThis = WeakReference(this); + selectionToken = grid->SelectionChanged += + ref new SelectionChangedEventHandler([weakThis](Platform::Object ^ s, SelectionChangedEventArgs ^ e) { + auto that = weakThis.Resolve(); + if (that) try { + that->AppsGrid_SelectionChanged(s, e); + } catch (...) { + } + }); + itemClickToken = grid->ItemClick += + ref new ItemClickEventHandler([weakThis](Platform::Object ^ s, ItemClickEventArgs ^ e) { + auto that = weakThis.Resolve(); + if (that) try { + that->AppsGrid_ItemClick(s, e); + } catch (...) { + } + }); + rightTappedToken = grid->RightTapped += + ref new RightTappedEventHandler([weakThis](Platform::Object ^ s, RightTappedRoutedEventArgs ^ e) { + auto that = weakThis.Resolve(); + if (that) try { + that->AppsGrid_RightTapped(s, e); + } catch (...) { + } + }); + loadedToken = grid->Loaded += + ref new RoutedEventHandler([weakThis](Platform::Object ^ s, RoutedEventArgs ^ e) { + auto that = weakThis.Resolve(); + if (that) try { + that->AppsGrid_Loaded(s, e); + } catch (...) { + } + }); + cccToken = grid->ContainerContentChanging += + ref new TypedEventHandler( + [weakThis](ListViewBase ^ s, ContainerContentChangingEventArgs ^ args) { + auto that = weakThis.Resolve(); + if (that) try { + that->AppsGrid_ContainerContentChanging(s, args); + } catch (...) { + } + }); +} + +void AppPage::UnwireAppsGridEvents( + const char *gridName, + ListView ^ grid, + Windows::Foundation::EventRegistrationToken selectionToken, + Windows::Foundation::EventRegistrationToken itemClickToken, + Windows::Foundation::EventRegistrationToken rightTappedToken, + Windows::Foundation::EventRegistrationToken loadedToken, + Windows::Foundation::EventRegistrationToken cccToken) { + if (grid == nullptr) return; + try { + grid->SelectionChanged -= selectionToken; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: %s SelectionChanged -= failed, handler leaked\n", gridName); + } + try { + grid->ItemClick -= itemClickToken; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: %s ItemClick -= failed, handler leaked\n", gridName); + } + try { + grid->RightTapped -= rightTappedToken; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: %s RightTapped -= failed, handler leaked\n", gridName); + } + try { + grid->Loaded -= loadedToken; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: %s Loaded -= failed, handler leaked\n", gridName); + } + try { + grid->ContainerContentChanging -= cccToken; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: %s ContainerContentChanging -= failed, handler leaked\n", gridName); + } +} + +void AppPage::OnLoaded(Platform::Object ^, RoutedEventArgs ^) { + Platform::WeakReference weakThis(this); + m_back_cookie = Windows::UI::Core::SystemNavigationManager::GetForCurrentView()->BackRequested += + ref new EventHandler([weakThis](Platform::Object ^ s, BackRequestedEventArgs ^ args) { + auto that = weakThis.Resolve(); + if (that) that->OnBackRequested(s, args); + }); + + try { + auto window = CoreApplication::MainView->CoreWindow; + if (window != nullptr) { + m_keydown_cookie = window->KeyDown += + ref new TypedEventHandler([weakThis](CoreWindow ^ s, KeyEventArgs ^ args) { + auto that = weakThis.Resolve(); + if (that) that->OnGamepadKeyDown(s, args); + }); + } + } catch (...) { + MLOGF(Utils::LogLevel::Error, "OnLoaded: KeyDown subscribe failed, gamepad input permanently unavailable this page instance\n"); + } + + try { + if (this->ViewModel != nullptr && this->PageBackgroundImage != nullptr) { + this->ViewModel->SetPageBackgroundBorder(this->PageBackgroundImage); + this->ViewModel->SetBackgroundTransitionSettings( + ResolveSharedAnimationDurationMsFromPageResources(this), + ResolveBackgroundOverlayOpacityFromPageResources(this)); + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnLoaded: ViewModel background setup failed, background transitions may not apply\n"); + } + + StartBgPanAnimation(); + StartBannerSlideInAnimation(); +} + +void AppPage::OnUnloaded(Platform::Object ^, RoutedEventArgs ^) { + if (m_selectedApp != nullptr) { + try { + m_selectedApp->IsSelected = false; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: IsSelected=false failed for app id=%d\n", m_selectedApp->Id); + } + } + m_selectedApp = nullptr; + m_initialFocusApplied = false; + m_appsGridRevealed = false; + try { + Windows::UI::Core::SystemNavigationManager::GetForCurrentView()->BackRequested -= m_back_cookie; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: BackRequested -= failed, handler leaked\n"); + } + try { + if (this->SearchBox != nullptr) this->SearchBox->GettingFocus -= m_searchbox_gettingfocus_token; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: SearchBox GettingFocus -= failed, handler leaked\n"); + } + continueAppFetch.store(false); + try { + PollingIndicator->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: PollingIndicator visibility reset failed\n"); + } + + auto window = CoreApplication::MainView->CoreWindow; + if (window != nullptr) { + try { + window->KeyDown -= m_keydown_cookie; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: KeyDown -= failed, handler leaked\n"); + } + } + + if (this->host != nullptr && this->host->Apps != nullptr) { + auto obs = dynamic_cast ^>(this->host->Apps); + if (obs != nullptr) { + try { + obs->VectorChanged -= m_apps_changed_token; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnUnloaded: VectorChanged -= failed, handler leaked\n"); + } + } + } + + if (m_centerDebounceTimer != nullptr) { + try { + m_centerDebounceTimer->Stop(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnUnloaded: m_centerDebounceTimer->Stop() failed, timer may keep firing after teardown"); + } + try { + m_centerDebounceTimer->Tick -= m_centerDebounceTimer_token; + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnUnloaded: m_centerDebounceTimer Tick -= failed, handler leaked"); + } + m_centerDebounceTimer = nullptr; + } + + if (m_bgPanStoryboard != nullptr) { + m_bgPanStoryboard->Stop(); + m_bgPanStoryboard = nullptr; + } + UnwireAppsGridEvents("AppsListView", this->AppsListView, + m_appsListView_selection_token, m_appsListView_itemclick_token, + m_appsListView_righttapped_token, m_appsListView_loaded_token, m_appsListView_ccc_token); + UnwireAppsGridEvents("AppsGridView", this->AppsGridView, + m_appsGridView_selection_token, m_appsGridView_itemclick_token, + m_appsGridView_righttapped_token, m_appsGridView_loaded_token, m_appsGridView_ccc_token); + CancelContainerRealizedWait(m_centerWaitContainerTarget, m_centerWaitContainer_token); + m_centerWaitContainerTarget = nullptr; + CancelLayoutUpdatedWait(m_centerWaitScrollViewerTarget, m_centerWaitScrollViewer_token); + m_centerWaitScrollViewerTarget = nullptr; +} + +void AppPage::OnNavigatedTo(NavigationEventArgs ^ e) { + MoonlightHost ^ mhost = dynamic_cast(e->Parameter); + if (mhost == nullptr) return; + host = mhost; + + bool willAutoStartApp = (host->AutostartID >= 0 && GetApplicationState()->shouldAutoConnect); + + continueAppFetch.store(true); + wasConnected.store(true); + if (!willAutoStartApp) FadeInPollingIndicator(); + + try { + if (this->ClosingOverlayText != nullptr) + this->ClosingOverlayText->Text = ref new Platform::String(L"Loading apps..."); + if (this->ClosingOverlay != nullptr) { + this->ClosingOverlay->Opacity = 1.0; + this->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Visible; + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnNavigatedTo: failed to configure closing overlay text/visibility"); + } + + { + Platform::WeakReference weakThis(this); + create_task([weakThis]() { + auto that = weakThis.Resolve(); + if (that == nullptr || !that->continueAppFetch.load()) return; + that->host->UpdateApps(); + }); + } + + if (!willAutoStartApp) { + Platform::WeakReference weakThis(this); + create_task([weakThis]() { + while (true) { + auto that = weakThis.Resolve(); + if (that == nullptr) break; + if (!that->continueAppFetch.load()) break; + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::Normal, + ref new DispatchedHandler([weakThis]() { + auto ui = weakThis.Resolve(); + if (ui) try { + ui->FadeInPollingIndicator(); + } catch (...) { + } + })); + try { + that->PollAppRunningAndConnectivity(); + } catch (const std::exception &e) { + MLOGF(Utils::LogLevel::Error, "Failed to poll app and host running state. Exception: %s\n", e.what()); + } catch (...) { + MLOG(Utils::LogLevel::Error, "Failed to poll app and host running state. Unknown Exception.\n"); + } + Sleep(3000); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::Normal, + ref new DispatchedHandler([weakThis]() { + auto ui = weakThis.Resolve(); + if (ui) try { + ui->FadeOutPollingIndicator(); + } catch (...) { + } + })); + Sleep(7000); + } + }); + } + + try { + auto obs = dynamic_cast ^>(host->Apps); + if (obs != nullptr) { + Platform::WeakReference weakThis(this); + m_apps_changed_token = obs->VectorChanged += + ref new Windows::Foundation::Collections::VectorChangedEventHandler( + [weakThis](Windows::Foundation::Collections::IObservableVector ^ sender, + Windows::Foundation::Collections::IVectorChangedEventArgs ^ args) { + auto that = weakThis.Resolve(); + if (that) try { + that->OnHostAppsChanged(sender, args); + } catch (...) { + } + }); + } + } catch (...) { + MLOGF(Utils::LogLevel::Error, "OnNavigatedTo: VectorChanged subscribe failed, app-list updates permanently unavailable this session\n"); + } + + try { + bool savedGrid = (host->Personalization->AppView == AppHostView::Grid); + if (savedGrid != m_isGridLayout && LayoutToggleButton != nullptr) { + LayoutToggleButton->IsChecked = savedGrid; + LayoutToggleButton_Click(LayoutToggleButton, nullptr); + } + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "OnNavigatedTo: saved layout restore failed for host, defaulting to current layout\n"); + } + + ArmEntranceAnimations(); + + { + Windows::UI::Color accentColor = host->Personalization->UseSystemAccent + ? (ref new Windows::UI::ViewManagement::UISettings()) + ->GetColorValue(Windows::UI::ViewManagement::UIColorType::Accent) + : host->Personalization->AccentColor; + ApplyAccentColor(accentColor); + + auto cur = this->ActualTheme; + this->RequestedTheme = (cur != ElementTheme::Dark) ? ElementTheme::Dark : ElementTheme::Light; + this->RequestedTheme = ElementTheme::Default; + } + + if (willAutoStartApp) { + GetApplicationState()->shouldAutoConnect = false; + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::High, ref new DispatchedHandler([this]() { + this->Connect(host->AutostartID); + })); + } + GetApplicationState()->shouldAutoConnect = false; +} + +void AppPage::OnBackRequested(Platform::Object ^, BackRequestedEventArgs ^ args) { + auto lm = this->GetLeftMenu(); + if (lm != nullptr && lm->IsOpen) { + lm->Close(); + args->Handled = true; + return; + } + + if (this->Frame->CanGoBack) { + this->Frame->GoBack(); + args->Handled = true; + } +} + +void AppPage::backButton_Click(Platform::Object ^, RoutedEventArgs ^) { + this->Frame->GoBack(); +} + +void AppPage::PollAppRunningAndConnectivity() { + if (this->host == nullptr) return; + this->host->UpdateAppRunningStates(); + if (this->wasConnected.load() && !this->host->Connected) { + this->wasConnected.store(false); + auto weakThis = WeakReference(this); + CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + CoreDispatcherPriority::Normal, + ref new DispatchedHandler([weakThis]() { + auto that = weakThis.Resolve(); + if (that != nullptr) that->ShowDisconnectedDialogAndNavigateBack(); + })); + } else if (!this->wasConnected.load() && this->host->Connected) { + this->wasConnected.store(true); + } +} + +void AppPage::ShowDisconnectedDialogAndNavigateBack() { + try { + auto dialog = ref new ::moonlight_xbox_dx::AlertDialog(); + dialog->Configure(L"Disconnected", L"Connection to host was lost."); + try { + dialog->XamlRoot = this->XamlRoot; + } catch (...) { + MLOG(Utils::LogLevel::Warning, "ShowDisconnectedDialogAndNavigateBack: failed to set dialog XamlRoot, dialog may not display correctly"); + } + Platform::WeakReference weakThis(this); + create_task(dialog->ShowAsync()).then([weakThis](ContentDialogResult result) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([that]() { + try { + auto slideBack = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + slideBack->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromLeft; + that->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSelectorPage::typeid), nullptr, slideBack); + } catch (const std::exception &e) { + MLOGF(Utils::LogLevel::Error, "Failed to navigate to HostSelectorPage after disconnect. Exception: %s\n", e.what()); + } catch (...) { + MLOG(Utils::LogLevel::Error, "Failed to navigate to HostSelectorPage after disconnect. Unknown Exception.\n"); + } + })); + }); + } catch (const std::exception &e) { + MLOGF(Utils::LogLevel::Error, "Failed to show disconnect dialog. Exception: %s\n", e.what()); + } catch (...) { + MLOG(Utils::LogLevel::Error, "Failed to show disconnect dialog. Unknown Exception.\n"); + } +} + +bool AppPage::ApplyAppFilter(Platform::String ^ filter) { + auto host = this->Host; + auto vec = this->m_filteredApps; + if (vec == nullptr) return false; + + if (host == nullptr || host->Apps == nullptr) { + vec->Clear(); + return false; + } + + auto newResults = ref new Platform::Collections::Vector(); + bool empty = (filter == nullptr || filter->Length() == 0); + std::wstring fw; + if (!empty) { + std::string fstr = Utils::PlatformStringToStdString(filter); + fw = Utils::NarrowToWideString(fstr); + std::transform(fw.begin(), fw.end(), fw.begin(), ::towlower); + } + + std::vector emptyOrdered; + for (unsigned int i = 0; i < host->Apps->Size; ++i) { + auto app = host->Apps->GetAt(i); + if (app == nullptr) continue; + if (empty) { + emptyOrdered.push_back(app); + } else { + std::string nstr = Utils::PlatformStringToStdString(app->Name != nullptr ? app->Name : ref new Platform::String(L"")); + std::wstring namew = Utils::NarrowToWideString(nstr); + std::transform(namew.begin(), namew.end(), namew.begin(), ::towlower); + if (namew.find(fw) != std::wstring::npos) newResults->Append(app); + } + } + if (empty) { + + auto &favoriteIds = host->favoriteAppIds; + std::stable_sort(emptyOrdered.begin(), emptyOrdered.end(), [&favoriteIds](MoonlightApp ^ a, MoonlightApp ^ b) { + if (a->IsFavorite != b->IsFavorite) return a->IsFavorite; + if (!a->IsFavorite) return false; + auto itA = std::find(favoriteIds.begin(), favoriteIds.end(), a->Id); + auto itB = std::find(favoriteIds.begin(), favoriteIds.end(), b->Id); + return itA < itB; + }); + for (auto app : emptyOrdered) newResults->Append(app); + } + + bool identical = false; + try { + if (vec->Size == newResults->Size) { + identical = true; + for (unsigned int i = 0; i < vec->Size; ++i) { + auto a = vec->GetAt(i), b = newResults->GetAt(i); + if ((a != nullptr ? a->Id : -1) != (b != nullptr ? b->Id : -1)) { + identical = false; + break; + } + } + } + } catch (...) { + identical = false; + } + if (identical) { + try { + for (unsigned int i = 0; i < vec->Size; ++i) { + auto a = vec->GetAt(i), b = newResults->GetAt(i); + if (a != b) { + b->IsSelected = a->IsSelected; + b->BlurredImage = a->BlurredImage; + b->GlowImage = a->GlowImage; + if (a->Image != nullptr) b->Image = a->Image; + if (m_selectedApp != nullptr && m_selectedApp->Id == a->Id) + m_selectedApp = b; + + vec->SetAt(i, b); + } + } + } catch (...) { + } + } else { + vec->Clear(); + for (unsigned int i = 0; i < newResults->Size; ++i) vec->Append(newResults->GetAt(i)); + + try { + bool emptyResults = (vec->Size == 0); + auto weakThis = WeakReference(this); + this->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, + ref new DispatchedHandler([weakThis, emptyResults]() { + try { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + if (that->NoAppsMessage != nullptr) + that->NoAppsMessage->Visibility = emptyResults ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + + if (emptyResults) try { + that->RevealActiveAppsGrid(); + } catch (...) { + } + if (emptyResults && that->SelectedAppText != nullptr && that->SelectedAppBox != nullptr) { + auto sb = dynamic_cast( + that->Resources->Lookup(ref new Platform::String(L"HideSelectedAppStoryboard"))); + if (sb != nullptr) sb->Begin(); + } + } catch (...) { + } + })); + } catch (...) { + } + } + + auto activeGrid = this->ActiveAppsGrid(); + if (vec->Size > 0 && activeGrid != nullptr && activeGrid->SelectedIndex < 0) + activeGrid->SelectedIndex = 0; + return identical; +} + +void AppPage::OnHostAppsChanged( + Windows::Foundation::Collections::IObservableVector ^, + Windows::Foundation::Collections::IVectorChangedEventArgs ^) { + try { + auto weakThis = WeakReference(this); + this->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, + ref new DispatchedHandler([weakThis]() { + try { + auto that = weakThis.Resolve(); + if (that) that->ApplyAppFilter(that->SearchBox != nullptr ? that->SearchBox->Text : nullptr); + } catch (...) { + } + })); + } catch (...) { + } +} + +void AppPage::SearchBox_TextChanged(Platform::Object ^ sender, TextChangedEventArgs ^) { + try { + auto tb = dynamic_cast(sender); + if (tb == nullptr) return; + ArmEntranceAnimations(); + ApplyAppFilter(tb->Text); + } catch (...) { + } +} + +Windows::UI::Xaml::Controls::ListView ^ AppPage::ActiveAppsGrid() { + return m_isGridLayout ? this->AppsGridView : this->AppsListView; +} + +void AppPage::LayoutToggleButton_Click(Platform::Object ^ sender, RoutedEventArgs ^) { + try { + auto toggle = dynamic_cast(sender); + bool wantGrid = toggle != nullptr && toggle->IsChecked != nullptr && toggle->IsChecked->Value; + if (m_isGridLayout == wantGrid) return; + + CancelContainerRealizedWait(m_centerWaitContainerTarget, m_centerWaitContainer_token); + m_centerWaitContainerTarget = nullptr; + CancelLayoutUpdatedWait(m_centerWaitScrollViewerTarget, m_centerWaitScrollViewer_token); + m_centerWaitScrollViewerTarget = nullptr; + + auto oldGrid = this->ActiveAppsGrid(); + auto selectedItem = (oldGrid != nullptr) ? oldGrid->SelectedItem : nullptr; + + m_isGridLayout = wantGrid; + + if (host != nullptr) { + host->Personalization->AppView = wantGrid ? AppHostView::Grid : AppHostView::List; + GetApplicationState()->UpdateFile(); + } + + VisualStateManager::GoToState(this, wantGrid ? "GridLayout" : "ListLayout", false); + + if (this->AppsListView != nullptr) + this->AppsListView->Visibility = wantGrid ? Windows::UI::Xaml::Visibility::Collapsed : Windows::UI::Xaml::Visibility::Visible; + if (this->AppsGridView != nullptr) + this->AppsGridView->Visibility = wantGrid ? Windows::UI::Xaml::Visibility::Visible : Windows::UI::Xaml::Visibility::Collapsed; + + m_scrollViewer = nullptr; + + if (this->Host != nullptr && this->Host->Apps != nullptr) { + for (int i = 0; i < (int)this->Host->Apps->Size; ++i) { + auto app = this->Host->Apps->GetAt(i); + app->BlurredImage = nullptr; + app->GlowImage = nullptr; + this->Host->Apps->SetAt(i, app); + } + } + + auto newGrid = this->ActiveAppsGrid(); + if (newGrid == nullptr) return; + + m_suppressSelectionVisuals = true; + if (selectedItem != nullptr) { + try { + newGrid->SelectedItem = selectedItem; + } catch (...) { + } + try { + newGrid->ScrollIntoView(selectedItem); + } catch (...) { + } + } + m_suppressSelectionVisuals = false; + + try { + newGrid->UpdateLayout(); + } catch (...) { + } + try { + this->UpdateItemHeights(); + } catch (...) { + } + + try { + if (newGrid->SelectedItem != nullptr) { + auto c2 = dynamic_cast(newGrid->ContainerFromItem(newGrid->SelectedItem)); + if (c2 != nullptr) { + UIElement ^ des2 = nullptr, ^img2 = nullptr, ^nm2 = nullptr; + UIElement ^ bl2 = nullptr, ^pl2 = nullptr; + FindElementChildren(c2, des2, img2, nm2, bl2, pl2); + auto resetCP = [](UIElement ^ el) { + if (el == nullptr) return; + auto fe = dynamic_cast(el); + if (fe == nullptr || fe->ActualWidth <= 0 || fe->ActualHeight <= 0) return; + auto vis = ElementCompositionPreview::GetElementVisual(el); + if (vis == nullptr) return; + Windows::Foundation::Numerics::float3 cp; + cp.x = (float)fe->ActualWidth * 0.5f; + cp.y = (float)fe->ActualHeight * 0.5f; + cp.z = 0.0f; + vis->CenterPoint = cp; + }; + resetCP(img2); + resetCP(des2); + resetCP(pl2); + } + } + } catch (...) { + } + + try { + auto app = dynamic_cast(newGrid->SelectedItem); + if (app != nullptr) + this->ApplySelectionVisuals(app, false, true); + else + this->CenterSelectedItem(true); + } catch (...) { + } + + try { + if (newGrid->SelectedItem != nullptr) { + auto sel = dynamic_cast(newGrid->ContainerFromItem(newGrid->SelectedItem)); + if (sel != nullptr) + sel->Focus(Windows::UI::Xaml::FocusState::Programmatic); + else + newGrid->Focus(Windows::UI::Xaml::FocusState::Programmatic); + } else { + newGrid->Focus(Windows::UI::Xaml::FocusState::Programmatic); + } + } catch (...) { + } + } catch (...) { + MLOGF(Utils::LogLevel::Error, "LayoutToggleButton_Click failed, layout toggle may be left in an inconsistent visual state\n"); + } +} + +void AppPage::AppsGrid_Loaded(Platform::Object ^ sender, RoutedEventArgs ^) { + try { + auto grid = dynamic_cast(sender); + if (grid != nullptr && grid == this->ActiveAppsGrid() && m_scrollViewer == nullptr) + m_scrollViewer = FindScrollViewer(grid); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppsGrid_Loaded: FindScrollViewer failed, m_scrollViewer left unset"); + } +} + +void AppPage::OnGamepadKeyDown(CoreWindow ^, KeyEventArgs ^ args) { + try { + using namespace Windows::System; + auto key = args->VirtualKey; + + if (key == VirtualKey::GamepadY || key == VirtualKey::Y) { + try { + this->HandleYButtonPress(); + } catch (...) { + } + args->Handled = true; + } + + if (key == VirtualKey::B) { + try { + auto lm = this->GetLeftMenu(); + if (lm != nullptr && lm->IsOpen) { + lm->Close(); + args->Handled = true; + } else if (this->Frame->CanGoBack) { + this->Frame->GoBack(); + args->Handled = true; + } + } catch (...) { + } + } + + if (key == VirtualKey::GamepadX || key == VirtualKey::X) { + try { + this->HandleXButtonPress(); + } catch (...) { + } + args->Handled = true; + } + + bool searchHasFocus = this->SearchBox != nullptr && + this->SearchBox->FocusState != Windows::UI::Xaml::FocusState::Unfocused; + + if (key == VirtualKey::GamepadDPadUp && !searchHasFocus && !m_isGridLayout) { + m_searchIsOpen = true; + try { + if (this->SearchBox != nullptr && + this->SearchBox->FocusState == Windows::UI::Xaml::FocusState::Unfocused) + this->SearchBox->Focus(Windows::UI::Xaml::FocusState::Programmatic); + } catch (...) { + } + } + + if (key == VirtualKey::GamepadDPadDown && searchHasFocus) { + m_searchIsOpen = false; + try { + this->FocusSelectedContainerOrGrid(); + } catch (...) { + } + args->Handled = true; + } + + } catch (...) { + } +} + +void AppPage::HandleYButtonPress() { + bool searchHasFocus = this->SearchBox != nullptr && + this->SearchBox->FocusState != Windows::UI::Xaml::FocusState::Unfocused; + if (!searchHasFocus) { + this->m_searchIsOpen = true; + this->SearchBox->Focus(Windows::UI::Xaml::FocusState::Programmatic); + return; + } + this->m_searchIsOpen = false; + this->FocusSelectedContainerOrGrid(); +} + +void AppPage::HandleXButtonPress() { + bool newState = !this->m_isGridLayout; + if (this->LayoutToggleButton) this->LayoutToggleButton->IsChecked = newState; + this->LayoutToggleButton_Click(this->LayoutToggleButton, nullptr); +} + +void AppPage::FocusSelectedContainerOrGrid() { + auto lv = this->ActiveAppsGrid(); + if (lv == nullptr) return; + if (lv->SelectedItem != nullptr) { + auto c = dynamic_cast(lv->ContainerFromItem(lv->SelectedItem)); + if (c != nullptr) { + c->Focus(Windows::UI::Xaml::FocusState::Programmatic); + return; + } + } + lv->Focus(Windows::UI::Xaml::FocusState::Programmatic); +} + +void AppPage::AppsGrid_ItemClick(Platform::Object ^ sender, ItemClickEventArgs ^ e) { + MoonlightApp ^ app = (MoonlightApp ^) e->ClickedItem; + this->currentApp = app; + + if (this->host != nullptr) { + for (unsigned int i = 0; i < this->host->Apps->Size; ++i) { + auto candidate = this->host->Apps->GetAt(i); + if (candidate != nullptr && candidate->CurrentlyRunning && candidate->Id != app->Id) { + this->closeAndStartButton_Click(nullptr, nullptr); + return; + } + } + } + this->Connect(app->Id); +} + +void AppPage::Connect(int appId) { + StreamConfiguration ^ config = ref new StreamConfiguration(); + config->hostname = host->LastHostname; + config->appID = appId; + config->width = host->Resolution->Width; + config->height = host->Resolution->Height; + config->bitrate = host->Bitrate; + config->FPS = host->FPS; + config->audioConfig = host->AudioConfig; + config->videoCodec = host->VideoCodec; + config->playAudioOnPC = host->PlayAudioOnPC; + config->enableHDR = host->EnableHDR; + config->enableSOPS = host->EnableSOPS; + config->enableStats = host->EnableStats; + config->enableGraphs = host->EnableGraphs; + if (config->enableHDR) host->VideoCodec = "HEVC (H.265)"; + config->backgroundImage = (this->currentApp != nullptr) ? this->currentApp->BlurredImage : nullptr; + config->appName = (this->currentApp != nullptr) ? this->currentApp->Name : nullptr; + this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(StreamPage::typeid), config, ref new Windows::UI::Xaml::Media::Animation::DrillInNavigationTransitionInfo()); +} + +void AppPage::AppsGrid_RightTapped(Platform::Object ^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs ^ e) { + FrameworkElement ^ senderElement = dynamic_cast(e != nullptr ? e->OriginalSource : nullptr); + if (senderElement != nullptr) { + if (senderElement->GetType()->FullName->Equals(ListViewItem::typeid->FullName)) { + currentApp = (MoonlightApp ^)((ListViewItem ^) senderElement)->Content; + } else { + currentApp = dynamic_cast(senderElement->DataContext); + if (currentApp == nullptr) { + auto activeGrid = this->ActiveAppsGrid(); + if (activeGrid != nullptr && activeGrid->SelectedIndex >= 0) + currentApp = (MoonlightApp ^) activeGrid->SelectedItem; + } + } + } + + bool anyRunning = false; + if (this->host != nullptr) { + for (unsigned int i = 0; i < this->host->Apps->Size; ++i) { + auto c = this->host->Apps->GetAt(i); + if (c != nullptr && c->CurrentlyRunning) { + anyRunning = true; + break; + } + } + } + + if (this->currentApp == nullptr) { + if (e != nullptr) e->Handled = false; + return; + } + + try { + Platform::WeakReference weakThis(this); + auto dialog = ref new AppActionsDialog(); + dialog->Configure( + (this->currentApp->Name != nullptr) ? this->currentApp->Name : ref new Platform::String(L""), + this->currentApp->CurrentlyRunning, + !this->currentApp->CurrentlyRunning && anyRunning, + !this->currentApp->CurrentlyRunning && !anyRunning, + this->currentApp->IsFavorite, + ref new RoutedEventHandler([weakThis](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr) try { + that->Connect(that->currentApp->Id); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog Resume handler failed to Connect"); + } + }), + ref new RoutedEventHandler([weakThis, dialog](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr) try { + that->closeAppButton_Click(dialog, nullptr); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog Close handler failed to close running app"); + } + }), + ref new RoutedEventHandler([weakThis, dialog](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr) try { + that->closeAndStartButton_Click(dialog, nullptr); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog CloseAndStart handler failed to close and start app"); + } + }), + ref new RoutedEventHandler([weakThis](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr && that->currentApp != nullptr) try { + that->Connect(that->currentApp->Id); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog Start handler failed to Connect"); + } + }), + ref new RoutedEventHandler([weakThis](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr) try { + auto t = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + t->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromRight; + that->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightSettings::typeid), nullptr, t); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog MoonlightSettings handler failed to navigate"); + } + }), + ref new RoutedEventHandler([weakThis](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr) try { + auto t = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + t->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromRight; + that->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSettingsPage::typeid), that->Host, t); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog HostSettings handler failed to navigate"); + } + }), + ref new RoutedEventHandler([weakThis](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that != nullptr && that->currentApp != nullptr && that->host != nullptr) try { + that->host->ToggleFavorite(that->currentApp->Id); + GetApplicationState()->UpdateFile(); + that->ApplyAppFilter(that->SearchBox != nullptr ? that->SearchBox->Text : nullptr); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "AppActionsDialog ToggleFavorite handler failed"); + } + })); + + create_task(dialog->ShowAsync()); + if (e != nullptr) e->Handled = true; + } catch (...) { + if (e != nullptr) e->Handled = false; + } +} + +Platform::String ^ AppPage::FindRunningAppName() { + if (this->host == nullptr) return nullptr; + for (unsigned int i = 0; i < this->host->Apps->Size; ++i) { + auto candidate = this->host->Apps->GetAt(i); + if (candidate != nullptr && candidate->CurrentlyRunning && + (this->currentApp == nullptr || candidate->Id != this->currentApp->Id)) { + return candidate->Name; + } + } + return nullptr; +} + +void AppPage::closeAndStartButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { + if (this->currentApp == nullptr) return; + if (sender != nullptr) { + this->ExecuteCloseAndStart(); + return; + } + + Platform::String ^ runningName = this->FindRunningAppName(); + + auto startName = (this->currentApp->Name != nullptr && this->currentApp->Name->Length() > 0) + ? std::wstring(this->currentApp->Name->Data()) + : std::wstring(L"this app"); + auto closePart = (runningName != nullptr && runningName->Length() > 0) + ? std::wstring(L"Close '") + runningName->Data() + L"'" + : std::wstring(L"Close the currently running app"); + Platform::String ^ message = ref new Platform::String((closePart + L" and start '" + startName + L"'?").c_str()); + + Platform::WeakReference weakThis(this); + auto dialog = ref new ConfirmDialog(); + dialog->Configure( + ref new Platform::String(L"Close & Start"), + message, + ref new RoutedEventHandler([weakThis](Platform::Object ^, RoutedEventArgs ^) { + auto that = weakThis.Resolve(); + if (that) that->ExecuteCloseAndStart(); + })); + create_task(dialog->ShowAsync()); +} + +void AppPage::CloseRunningAppOnHostAsync( + const char *callerName, + CoreDispatcherPriority uiPriority, + std::function onClosedUIThread) { + Platform::WeakReference weakThis(this); + create_task(create_async([weakThis, callerName, uiPriority, onClosedUIThread]() { + try { + auto thatLocal = weakThis.Resolve(); + if (thatLocal == nullptr) return; + MoonlightClient client; + auto ipAddr = Utils::PlatformStringToStdString(thatLocal->host->LastHostname); + if (client.Connect(ipAddr.c_str()) == 0) { + client.StopApp(); + Sleep(1000); + } + } catch (...) { + MLOGF(Utils::LogLevel::Error, "%s: StopApp on host failed, running app may not actually be closed\n", callerName); + } + auto thatLocal2 = weakThis.Resolve(); + if (thatLocal2 == nullptr) return; + thatLocal2->Dispatcher->RunAsync(uiPriority, + ref new DispatchedHandler([weakThis, callerName, onClosedUIThread]() { + auto thatUI = weakThis.Resolve(); + try { + if (thatUI != nullptr) onClosedUIThread(thatUI); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "%s: post-close UI update failed, ClosingOverlay may stay visible\n", callerName); + } + })); + })).then([](concurrency::task t) { + try { + t.get(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "CloseRunningAppOnHostAsync background task faulted"); + } + }); +} + +void AppPage::ExecuteCloseAndStart() { + auto name = this->FindRunningAppName(); + this->ClosingOverlayText->Text = (name != nullptr && name->Length() > 0) + ? ref new Platform::String((std::wstring(L"Closing ") + name->Data() + L"...").c_str()) + : ref new Platform::String(L"Closing..."); + this->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Visible; + + CloseRunningAppOnHostAsync("ExecuteCloseAndStart", CoreDispatcherPriority::High, [](AppPage ^ thatUI) { + thatUI->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + if (thatUI->currentApp != nullptr) + thatUI->Connect(thatUI->currentApp->Id); + }); +} + +void AppPage::closeAppButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { + auto name = (this->currentApp != nullptr && this->currentApp->Name != nullptr) ? this->currentApp->Name : nullptr; + this->ClosingOverlayText->Text = (name != nullptr && name->Length() > 0) + ? ref new Platform::String((std::wstring(L"Closing ") + name->Data() + L"...").c_str()) + : ref new Platform::String(L"Closing..."); + this->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Visible; + + CloseRunningAppOnHostAsync("closeAppButton_Click", CoreDispatcherPriority::Normal, [](AppPage ^ thatUI) { + thatUI->host->UpdateHostInfo(true); + thatUI->host->UpdateAppRunningStates(); + thatUI->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + }); +} + +void AppPage::FadeInPollingIndicator() { + try { + using namespace Windows::UI::Xaml::Media::Animation; + PollingIndicator->Opacity = 0.0; + PollingIndicator->Visibility = Windows::UI::Xaml::Visibility::Visible; + auto anim = ref new DoubleAnimation(); + anim->To = ref new Platform::Box(0.3); + TimeSpan ts; + ts.Duration = 1500000LL; + anim->Duration = DurationHelper::FromTimeSpan(ts); + auto sb = ref new Storyboard(); + sb->Children->Append(anim); + Storyboard::SetTarget(anim, PollingIndicator); + Storyboard::SetTargetProperty(anim, ref new Platform::String(L"(UIElement.Opacity)")); + sb->Begin(); + } catch (...) { + } +} + +void AppPage::FadeOutPollingIndicator() { + try { + using namespace Windows::UI::Xaml::Media::Animation; + auto anim = ref new DoubleAnimation(); + anim->To = ref new Platform::Box(0.0); + TimeSpan ts; + ts.Duration = 1500000LL; + anim->Duration = DurationHelper::FromTimeSpan(ts); + auto sb = ref new Storyboard(); + sb->Children->Append(anim); + Storyboard::SetTarget(anim, PollingIndicator); + Storyboard::SetTargetProperty(anim, ref new Platform::String(L"(UIElement.Opacity)")); + Platform::WeakReference weakThis(this); + sb->Completed += ref new EventHandler([weakThis](Platform::Object ^, Platform::Object ^) { + auto that = weakThis.Resolve(); + if (that) try { + that->PollingIndicator->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + } catch (...) { + } + }); + sb->Begin(); + } catch (...) { + } +} + +void AppPage::RevealActiveAppsGrid() { + if (m_appsGridRevealed) return; + m_appsGridRevealed = true; + try { + using namespace Windows::UI::Xaml::Media::Animation; + if (this->ClosingOverlay == nullptr) return; + auto anim = ref new DoubleAnimation(); + anim->To = ref new Platform::Box(0.0); + TimeSpan ts; + ts.Duration = 1500000LL; + anim->Duration = DurationHelper::FromTimeSpan(ts); + auto sb = ref new Storyboard(); + sb->Children->Append(anim); + Storyboard::SetTarget(anim, this->ClosingOverlay); + Storyboard::SetTargetProperty(anim, ref new Platform::String(L"(UIElement.Opacity)")); + Platform::WeakReference weakThis(this); + sb->Completed += ref new EventHandler([weakThis](Platform::Object ^, Platform::Object ^) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + try { + that->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + that->ClosingOverlay->Opacity = 1.0; + } catch (...) { + MLOG(Utils::LogLevel::Warning, "RevealActiveAppsGrid: failed to reset ClosingOverlay after reveal, m_appsGridRevealed already set so this will not retry"); + } + }); + sb->Begin(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "RevealActiveAppsGrid: reveal animation failed to start, m_appsGridRevealed already set so this will not retry"); + } +} + +void AppPage::StartBgPanAnimation() { + using namespace Windows::UI::Xaml::Media; + using namespace Windows::UI::Xaml::Media::Animation; + + try { + if (PageBackgroundImage == nullptr) return; + + auto transform = ref new TranslateTransform(); + PageBackgroundImage->RenderTransform = transform; + + auto sb = ref new Storyboard(); + sb->RepeatBehavior = RepeatBehaviorHelper::Forever; + sb->AutoReverse = true; + + Windows::UI::Xaml::Duration dur(TimeSpan{kBgPanDurationSec * 10000000LL}); + + auto ease = ref new SineEase(); + ease->EasingMode = EasingMode::EaseInOut; + + auto animY = ref new DoubleAnimation(); + animY->To = ref new Platform::Box(120.0); + animY->Duration = dur; + animY->EasingFunction = ease; + Storyboard::SetTarget(animY, transform); + Storyboard::SetTargetProperty(animY, ref new Platform::String(L"Y")); + sb->Children->Append(animY); + + sb->Begin(); + m_bgPanStoryboard = sb; + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "StartBgPanAnimation failed, background pan animation will not run\n"); + } +} + +void AppPage::StartBannerSlideInAnimation() { + using namespace Windows::UI::Xaml::Media; + using namespace Windows::UI::Xaml::Media::Animation; + + try { + if (ComputerNameBannerContainer == nullptr) return; + + auto transform = ref new TranslateTransform(); + ComputerNameBannerContainer->RenderTransform = transform; + + auto ease = ref new CubicEase(); + ease->EasingMode = EasingMode::EaseInOut; + + auto anim = ref new DoubleAnimation(); + anim->From = ref new Platform::Box(-200.0); + anim->To = ref new Platform::Box(0.0); + TimeSpan ts; + ts.Duration = 15000000LL; + anim->Duration = DurationHelper::FromTimeSpan(ts); + anim->EasingFunction = ease; + + auto sb = ref new Storyboard(); + sb->Children->Append(anim); + Storyboard::SetTarget(anim, transform); + Storyboard::SetTargetProperty(anim, ref new Platform::String(L"X")); + sb->Begin(); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "StartBannerSlideInAnimation failed, banner slide-in animation will not run\n"); + } +} + +} diff --git a/UI/Pages/AppPage/AppPage.xaml.h b/UI/Pages/AppPage/AppPage.xaml.h new file mode 100644 index 00000000..3c981ec5 --- /dev/null +++ b/UI/Pages/AppPage/AppPage.xaml.h @@ -0,0 +1,165 @@ +#pragma once + +#include "UI\Pages\AppPage\AppPage.g.h" +#include "State\MoonlightApp.h" +#include "UI\Models\ViewModels\AppPageViewModel.h" +#include "UI\Utilities\XamlHelpers.h" +#include +#include +#include +#include + +namespace moonlight_xbox_dx +{ + +static constexpr float kBackgroundSaturation = 1.25f; +static constexpr int kBgPanDurationSec = 10; +static constexpr float kBlurAmountBackground = 2.0f; +static constexpr float kBlurGlowPaddingDip = 60.0f; + +static constexpr double kEntranceStartScale = 0.25; +static constexpr int kEntranceDurationMs = 500; +static constexpr int kEntranceStaggerMs = 100; +static constexpr int kEntranceMaxStaggerItems = 100; + + namespace Controls { ref class SlidingMenu; } + [Windows::Foundation::Metadata::WebHostHidden] + public ref class AppPage sealed + { + private: + moonlight_xbox_dx::Controls::SlidingMenu^ m_leftMenu = nullptr; + moonlight_xbox_dx::Controls::SlidingMenu^ GetLeftMenu(); + AppPageViewModel^ m_viewModel = nullptr; + MoonlightHost^ host; + MoonlightApp^ currentApp; + Platform::Collections::Vector^ m_filteredApps; + bool ApplyAppFilter(Platform::String^ filter); + Windows::Foundation::EventRegistrationToken m_apps_changed_token; + void OnHostAppsChanged(Windows::Foundation::Collections::IObservableVector^ sender, Windows::Foundation::Collections::IVectorChangedEventArgs^ args); + Windows::Foundation::EventRegistrationToken m_back_cookie; + std::atomic continueAppFetch{ false }; + std::atomic wasConnected{ false }; + MoonlightApp^ m_selectedApp = nullptr; + protected: + virtual void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; + void Connect(int app); + public: + AppPage(); + + property Windows::Foundation::Collections::IObservableVector^ FilteredApps { + Windows::Foundation::Collections::IObservableVector^ get() { + if (m_filteredApps == nullptr) m_filteredApps = ref new Platform::Collections::Vector(); + return m_filteredApps; + } + } + property MoonlightHost^ Host { + MoonlightHost^ get() { return this->host; } + } + property AppPageViewModel^ ViewModel { + AppPageViewModel^ get() { + if (m_viewModel == nullptr) m_viewModel = ref new AppPageViewModel(); + return m_viewModel; + } + } + void OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args); + + private: + void AppsGrid_ItemClick(Platform::Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e); + void BlurAppImage(MoonlightApp ^ selApp); + void HandleBlurStreamReady(MoonlightApp^ selApp, Platform::WeakReference weakThis, + Windows::Storage::Streams::IRandomAccessStream^ stream, bool isBackground); + void AppsGrid_Loaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void SearchBox_TextChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::TextChangedEventArgs^ e); + void AppsGrid_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void ApplySelectionVisuals(MoonlightApp^ app, bool animate, bool centerImmediate = false); + void UpdateContainerSelectionState(MoonlightApp^ app, bool isSelected, bool animate); + bool m_suppressSelectionVisuals = false; + Windows::UI::Xaml::DispatcherTimer^ m_centerDebounceTimer = nullptr; + Windows::Foundation::EventRegistrationToken m_centerDebounceTimer_token; + void AppsGrid_RightTapped(Platform::Object^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs^ e); + void AppsGrid_ContainerContentChanging(Windows::UI::Xaml::Controls::ListViewBase^ sender, Windows::UI::Xaml::Controls::ContainerContentChangingEventArgs^ args); + void PlayEntranceAnimation(Windows::UI::Xaml::Controls::ListViewItem^ container, int index); + void ArmEntranceAnimations(); + std::unordered_set m_listEntranceAnimatedIndices; + std::unordered_set m_gridEntranceAnimatedIndices; + bool m_entranceAnimationsArmed = false; + void closeAppButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void closeAndStartButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void ExecuteCloseAndStart(); + Platform::String^ FindRunningAppName(); + void CloseRunningAppOnHostAsync( + const char* callerName, + Windows::UI::Core::CoreDispatcherPriority uiPriority, + std::function onClosedUIThread); + void backButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateItemHeights(); + void WireAppsGridEvents( + Windows::UI::Xaml::Controls::ListView^ grid, + Windows::Foundation::EventRegistrationToken& selectionToken, + Windows::Foundation::EventRegistrationToken& itemClickToken, + Windows::Foundation::EventRegistrationToken& rightTappedToken, + Windows::Foundation::EventRegistrationToken& loadedToken, + Windows::Foundation::EventRegistrationToken& cccToken); + void UnwireAppsGridEvents( + const char* gridName, + Windows::UI::Xaml::Controls::ListView^ grid, + Windows::Foundation::EventRegistrationToken selectionToken, + Windows::Foundation::EventRegistrationToken itemClickToken, + Windows::Foundation::EventRegistrationToken rightTappedToken, + Windows::Foundation::EventRegistrationToken loadedToken, + Windows::Foundation::EventRegistrationToken cccToken); + + Windows::UI::Xaml::Controls::ScrollViewer^ m_scrollViewer; + Windows::UI::Xaml::Media::Animation::Storyboard^ m_bgPanStoryboard = nullptr; + void StartBgPanAnimation(); + void StartBannerSlideInAnimation(); + + bool m_isGridLayout = false; + + void LayoutToggleButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OnGamepadKeyDown(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ args); + void HandleYButtonPress(); + void HandleXButtonPress(); + void FocusSelectedContainerOrGrid(); + Windows::UI::Xaml::Controls::ListView^ ActiveAppsGrid(); + void RevealActiveAppsGrid(); + bool m_appsGridRevealed = false; + void CenterSelectedItem(bool immediate = false); + void WaitForContainerThenCenter(bool immediate); + void WaitForScrollViewerThenCenter(bool immediate); + void UpdateSelectedAppBox(MoonlightApp^ app, MoonlightApp^ prev, bool animate); + + Windows::Foundation::EventRegistrationToken m_keydown_cookie; + + Windows::Foundation::EventRegistrationToken m_appsListView_selection_token; + Windows::Foundation::EventRegistrationToken m_appsListView_itemclick_token; + Windows::Foundation::EventRegistrationToken m_appsListView_righttapped_token; + Windows::Foundation::EventRegistrationToken m_appsListView_loaded_token; + Windows::Foundation::EventRegistrationToken m_appsListView_ccc_token; + Windows::Foundation::EventRegistrationToken m_appsGridView_selection_token; + Windows::Foundation::EventRegistrationToken m_appsGridView_itemclick_token; + Windows::Foundation::EventRegistrationToken m_appsGridView_righttapped_token; + Windows::Foundation::EventRegistrationToken m_appsGridView_loaded_token; + Windows::Foundation::EventRegistrationToken m_appsGridView_ccc_token; + Windows::Foundation::EventRegistrationToken m_searchbox_gettingfocus_token; + Windows::Foundation::EventRegistrationToken m_centerWaitContainer_token; + Windows::UI::Xaml::Controls::ListViewBase^ m_centerWaitContainerTarget = nullptr; + Windows::Foundation::EventRegistrationToken m_centerWaitScrollViewer_token; + Windows::UI::Xaml::Controls::ListViewBase^ m_centerWaitScrollViewerTarget = nullptr; + bool m_searchIsOpen = false; + std::unordered_set m_blurInProgressIds; + + bool m_initialFocusApplied = false; + unsigned int m_appTextAnimVersion = 0; + + concurrency::task ApplyBlur(MoonlightApp^ app, float blurDip, float padDip = 0.0f); + + void FadeInBlurIfSelected(MoonlightApp^ app, Windows::UI::Xaml::Media::Imaging::BitmapImage^ img); + void FadeInPollingIndicator(); + void FadeOutPollingIndicator(); + void PollAppRunningAndConnectivity(); + void ShowDisconnectedDialogAndNavigateBack(); + }; +} diff --git a/UI/Pages/HostSelectorPage.xaml b/UI/Pages/HostSelectorPage.xaml new file mode 100644 index 00000000..c36d7879 --- /dev/null +++ b/UI/Pages/HostSelectorPage.xaml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MOONLIGHT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/Pages/HostSelectorPage.xaml.cpp b/UI/Pages/HostSelectorPage.xaml.cpp new file mode 100644 index 00000000..084d0a3c --- /dev/null +++ b/UI/Pages/HostSelectorPage.xaml.cpp @@ -0,0 +1,1040 @@ +#include "pch.h" +#include "HostSelectorPage.xaml.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Controls\LunarPhaseControl.xaml.h" +#include "UI\Pages\AppPage\AppPage.xaml.h" +#include +#include "UI\Pages\HostSettingsPage.xaml.h" +#define MLOG_TAG_OVERRIDE "HostSelectorPage" +#include "Utils.hpp" +#include "UI\Utilities\XamlHelpers.h" +#include "UI\Utilities\ToastService.h" +#include "UI\Pages\MoonlightSettings.xaml.h" +#include "State\MDNSHandler.h" +#include "UI\Pages\MoonlightWelcome.xaml.h" +#include "UI\Modals\AlertDialog.xaml.h" +#include "UI\Modals\PairDialog.xaml.h" +#include "UI\Modals\ConfirmDialog.xaml.h" +#include "UI\Modals\HostActionsDialog.xaml.h" +#include "UI\Modals\TestConnectionResultDialog.xaml.h" +#include +#include +#include +#include +#include +#include + +using namespace moonlight_xbox_dx; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Controls::Primitives; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Input; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Navigation; +using namespace Windows::UI::ViewManagement::Core; + +HostSelectorPage::HostSelectorPage() +{ + state = GetApplicationState(); + InitializeComponent(); + m_hostsScrollViewer = nullptr; +} + +void HostSelectorPage::OnStateLoaded() { + if (GetApplicationState()->FirstTime) { + this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightWelcome::typeid), nullptr, ref new Windows::UI::Xaml::Media::Animation::EntranceNavigationTransitionInfo()); + return; + } + + { + auto weakThis = WeakReference(this); + try { + this->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::Low, + ref new Windows::UI::Core::DispatchedHandler([weakThis]() { + auto that = weakThis.Resolve(); + if (that != nullptr) that->UpdateAllMoonPhases(true, 4); + })); + } catch (...) {} + } + + { + auto weakThis = WeakReference(this); + try { + this->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::Low, + ref new Windows::UI::Core::DispatchedHandler([weakThis]() { + auto that = weakThis.Resolve(); + if (that != nullptr) that->FocusFirstHostItem(4); + })); + } catch (...) {} + } + + Concurrency::create_task([this]() { + std::vector hostsSnapshot; + for (auto a : GetApplicationState()->SavedHosts) { + hostsSnapshot.push_back(a); + } + Concurrency::parallel_for_each(hostsSnapshot.begin(), hostsSnapshot.end(), [](MoonlightHost^ a) { + try { + a->UpdateHostInfo(true); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnStateLoaded UpdateHostInfo failed for a host"); + } + }); + }).then([this]() { + bool willAutoConnect = false; + if (GetApplicationState()->autostartInstance.size() > 0) { + auto pii = Utils::StringFromStdString(GetApplicationState()->autostartInstance); + for (unsigned int i = 0; i < GetApplicationState()->SavedHosts->Size; i++) { + auto host = GetApplicationState()->SavedHosts->GetAt(i); + if (host->InstanceId->Equals(pii)) { + willAutoConnect = true; + auto that = this; + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::High, + ref new Windows::UI::Core::DispatchedHandler([that, host]() { + that->Connect(host); + }) + ); + break; + } + } + } + if (!willAutoConnect) { + auto that = this; + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::Normal, + ref new Windows::UI::Core::DispatchedHandler([that]() { + that->StartPollTimerIfNeeded(); + }) + ); + } + }).then([this](concurrency::task t) { + try { + t.get(); + } + catch (const std::exception &e) { + MLOGF(Utils::LogLevel::Error, "OnStateLoaded task exception: %s", e.what()); + } + catch (...) { + MLOG(Utils::LogLevel::Error, "OnStateLoaded task unknown exception"); + } + }); +} + +void HostSelectorPage::OnKeyDown(Platform::Object ^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs ^ e) { + if (e->Key == Windows::System::VirtualKey::Enter) { + CoreInputView::GetForCurrentView()->TryHide(); + } +} + +void HostSelectorPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { + Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseCoreWindow); + continueFetch.store(true); + m_isNavigatedAway.store(false); + + try { + if (BackgroundHost != nullptr) { + BackgroundHost->SetHosts(State->SavedHosts); + BackgroundHost->Refresh(); + BackgroundHost->StartAnimations(); + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnNavigatedTo failed to refresh background/animations"); + } + + if (GetApplicationState()->IsStateLoaded) { + StartPollTimerIfNeeded(); + } +} + +void HostSelectorPage::StartPollTimerIfNeeded() +{ + if (m_isNavigatedAway.load()) return; + if (m_pollTimer != nullptr) return; + + using namespace Windows::System::Threading; + using namespace Windows::Foundation; + + try { + TimeSpan period; + period.Duration = 5000 * 10000LL; + Platform::WeakReference weakThis(this); + auto callback = ref new TimerElapsedHandler([weakThis](ThreadPoolTimer^ timer) { + auto that = weakThis.Resolve(); + if (that == nullptr) { + try { + if (timer != nullptr) timer->Cancel(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "failed to cancel poll timer for destroyed page"); + } + return; + } + that->PollTick(timer); + }); + + m_pollTimer = ThreadPoolTimer::CreatePeriodicTimer(callback, period); + } catch (...) { + MLOG(Utils::LogLevel::Error, "failed to start poll timer"); + } +} + +void HostSelectorPage::OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs ^ e) { + try { + if (BackgroundHost != nullptr) BackgroundHost->StopAnimations(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnNavigatedFrom failed to stop background animations"); + } + + try { + if (m_hostsGrid_ccc_token.Value != 0 && HostsGrid != nullptr) { + HostsGrid->LayoutUpdated -= m_hostsGrid_ccc_token; + m_hostsGrid_ccc_token.Value = 0; + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "failed to unsubscribe HostsGrid.LayoutUpdated on navigate-away"); + } + + CancelContainerRealizedWait(HostsGrid, m_hostsContainerWait_token); + CancelLayoutUpdatedWait(HostsGrid, m_hostsScrollViewerWait_token); + + m_isNavigatedAway.store(true); + continueFetch.store(false); + try { + if (m_pollTimer != nullptr) { + m_pollTimer->Cancel(); + m_pollTimer = nullptr; + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "failed to cancel poll timer on navigate-away"); + } + + for (int i = 0; i < 50 && m_pollActiveCount.load() > 0; ++i) { + Sleep(20); + } + + try { + __super::OnNavigatedFrom(e); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "OnNavigatedFrom base class call failed"); + } +} + +static const double kSelectedHostContainerWidth = 230; + +bool HostSelectorPage::EnsureCenteringPadding() +{ + try { + if (m_adjustingCenterPadding) return false; + + auto grid = this->HostsGrid; + ListViewItem^ container = nullptr; + auto status = ResolveSelectedContainer(grid, m_hostsScrollViewer, container); + if (status != SelectedContainerStatus::Ready) return false; + + double viewport = m_hostsScrollViewer->ViewportWidth; + double width = kSelectedHostContainerWidth; + if (!std::isfinite(viewport) || viewport <= 0.0) return false; + + double desired = std::max(0.0, (viewport - width) * 0.5); + if (!std::isfinite(desired)) return false; + if (m_lastCenterPadding >= 0.0 && std::fabs(m_lastCenterPadding - desired) < 0.5) return false; + + m_adjustingCenterPadding = true; + auto result = ApplyEdgeCenteringPadding(grid, desired); + m_lastCenterPadding = desired; + m_adjustingCenterPadding = false; + bool applied = result == EdgeCenteringPaddingResult::Applied; + if (applied) m_paddingDirty = true; + return applied; + } catch (...) { + m_adjustingCenterPadding = false; + return false; + } +} + +void HostSelectorPage::CenterSelectedHost(bool immediate) +{ + try { + auto grid = this->HostsGrid; + ListViewItem^ container = nullptr; + auto status = ResolveSelectedContainer(grid, m_hostsScrollViewer, container); + if (status == SelectedContainerStatus::NoSelection) return; + if (status == SelectedContainerStatus::ScrollViewerMissing) { + WaitForHostScrollViewerThenCenter(immediate); + return; + } + if (status == SelectedContainerStatus::ContainerMissing) { + WaitForHostContainerThenCenter(immediate); + return; + } + + EnsureCenteringPadding(); + if (m_paddingDirty) { + m_paddingDirty = false; + WaitForHostScrollViewerThenCenter(immediate); + return; + } + + try { grid->UpdateLayout(); } catch (...) {} + if (container->ActualWidth <= 0.0 || container->ActualHeight <= 0.0 || + std::fabs(container->ActualWidth - kSelectedHostContainerWidth) > 1.0) { + WaitForHostScrollViewerThenCenter(immediate); + return; + } + + double viewport = m_hostsScrollViewer->ViewportWidth; + if (!std::isfinite(viewport) || viewport <= 0.0) { + try { grid->UpdateLayout(); } catch (...) {} + viewport = m_hostsScrollViewer->ViewportWidth; + } + if (!std::isfinite(viewport) || viewport <= 0.0) return; + + CenterContainerInScrollViewer(m_hostsScrollViewer, container, false, immediate); + } catch (...) {} +} + +void HostSelectorPage::WaitForHostContainerThenCenter(bool immediate) +{ + auto grid = this->HostsGrid; + if (grid == nullptr || m_hostsContainerWait_token.Value != 0) return; + Platform::WeakReference weakThis(this); + m_hostsContainerWait_token = ArmContainerRealizedWait(grid, [weakThis, immediate]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->m_hostsContainerWait_token.Value = 0; + that->CenterSelectedHost(immediate); + }); +} + +void HostSelectorPage::WaitForHostScrollViewerThenCenter(bool immediate) +{ + auto grid = this->HostsGrid; + if (grid == nullptr || m_hostsScrollViewerWait_token.Value != 0) return; + Platform::WeakReference weakThis(this); + m_hostsScrollViewerWait_token = ArmLayoutUpdatedWait(grid, [weakThis, immediate]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + that->m_hostsScrollViewerWait_token.Value = 0; + that->CenterSelectedHost(immediate); + }); +} + +void HostSelectorPage::PreRealizeAllHostContainers() +{ + if (m_hostsPreRealized) return; + auto grid = this->HostsGrid; + if (grid == nullptr || grid->Items == nullptr || grid->Items->Size == 0) return; + + m_hostsPreRealized = true; + unsigned int count = grid->Items->Size; + int selectedIndex = grid->SelectedIndex; + try { + for (unsigned int i = 0; i < count; ++i) { + grid->ScrollIntoView(grid->Items->GetAt(i)); + grid->UpdateLayout(); + } + if (selectedIndex >= 0) { + grid->ScrollIntoView(grid->Items->GetAt(selectedIndex)); + grid->UpdateLayout(); + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "PreRealizeAllHostContainers failed"); + } +} + +void HostSelectorPage::HostsGrid_Loaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^) +{ + try { + auto grid = dynamic_cast(sender); + if (grid == nullptr) return; + if (m_hostsScrollViewer == nullptr) m_hostsScrollViewer = FindScrollViewer(grid); + + try { + if (grid->Items != nullptr && grid->Items->Size > 0 && grid->SelectedIndex < 0) { + grid->SelectedIndex = 0; + } + if (grid->Items != nullptr && grid->Items->Size > 0) { + Platform::WeakReference weakThis(this); + this->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::Low, + ref new Windows::UI::Core::DispatchedHandler([weakThis]() { + auto that = weakThis.Resolve(); + if (that != nullptr) that->FocusFirstHostItem(4); + })); + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "HostsGrid_Loaded initial-selection/focus dispatch failed"); + } + + PreRealizeAllHostContainers(); + SubscribeMoonPhaseRefreshOnNextLayout(true); + EnsureCenteringPadding(); + CenterSelectedHost(true); + UpdateAllMoonPhases(false, 4); + } catch (...) { + MLOG(Utils::LogLevel::Error, "HostsGrid_Loaded setup failed"); + } +} + +void HostSelectorPage::HostsGrid_SizeChanged(Platform::Object^, Windows::UI::Xaml::SizeChangedEventArgs^) +{ + if (m_adjustingCenterPadding) return; + EnsureCenteringPadding(); + CenterSelectedHost(true); +} + +void HostSelectorPage::FocusFirstHostItem(int attempts) +{ + try { + auto grid = HostsGrid; + if (grid == nullptr || grid->Items == nullptr || grid->Items->Size == 0) return; + if (grid->SelectedIndex < 0) grid->SelectedIndex = 0; + int idx = grid->SelectedIndex >= 0 ? grid->SelectedIndex : 0; + auto container = dynamic_cast(grid->ContainerFromIndex(idx)); + if (container != nullptr) { + container->Focus(Windows::UI::Xaml::FocusState::Programmatic); + return; + } + if (attempts <= 0 || Dispatcher == nullptr) return; + Platform::WeakReference weakThis(this); + try { + Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::Normal, + ref new Windows::UI::Core::DispatchedHandler([weakThis, attempts]() { + auto that = weakThis.Resolve(); + if (that != nullptr) that->FocusFirstHostItem(attempts - 1); + })); + } catch (...) {} + } catch (...) {} +} + +void HostSelectorPage::HostsGrid_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^) +{ + try { + auto grid = dynamic_cast(sender); + if (grid == nullptr || grid->SelectedIndex < 0) return; + CenterSelectedHost(false); + UpdateAllMoonPhases(true); + } catch (...) {} + + try { + auto grid = dynamic_cast(sender); + auto selectedHost = (grid != nullptr && grid->SelectedItem != nullptr) + ? dynamic_cast(grid->SelectedItem) + : nullptr; + + auto bgKey = (selectedHost != nullptr) ? selectedHost->Personalization->Background : nullptr; + auto ls = Windows::Storage::ApplicationData::Current->LocalSettings->Values; + if (bgKey != nullptr && !bgKey->IsEmpty()) { + ls->Insert("background", bgKey); + } else { + ls->Insert("background", DynamicBackgroundHost::DefaultKey); + } + + if (BackgroundHost != nullptr) { + auto singleHost = ref new Platform::Collections::Vector(); + if (selectedHost != nullptr) singleHost->Append(selectedHost); + BackgroundHost->SetHosts(singleHost->Size > 0 ? (IVector^)singleHost : State->SavedHosts); + BackgroundHost->Refresh(); + BackgroundHost->StartAnimations(); + } + + Windows::UI::Color accentColor = (selectedHost != nullptr && !selectedHost->Personalization->UseSystemAccent) + ? selectedHost->Personalization->AccentColor + : (ref new Windows::UI::ViewManagement::UISettings()) + ->GetColorValue(Windows::UI::ViewManagement::UIColorType::Accent); + ApplyAccentColor(accentColor); + } catch (...) {} + + try { + auto grid = dynamic_cast(sender); + auto selectedHost = (grid != nullptr && grid->SelectedItem != nullptr) + ? dynamic_cast(grid->SelectedItem) + : nullptr; + + if (this->SelectedHostBanner != nullptr && this->SelectedHostText != nullptr) { + Platform::String^ newName = (selectedHost != nullptr && selectedHost->ComputerName != nullptr) + ? selectedHost->ComputerName + : ref new Platform::String(L""); + + Windows::UI::Xaml::Media::Animation::Storyboard^ showSb = nullptr; + Windows::UI::Xaml::Media::Animation::Storyboard^ hideSb = nullptr; + auto res = this->Resources; + if (res != nullptr) { + try { showSb = dynamic_cast(res->Lookup(ref new Platform::String(L"ShowSelectedHostStoryboard"))); } catch (...) {} + try { hideSb = dynamic_cast(res->Lookup(ref new Platform::String(L"HideSelectedHostStoryboard"))); } catch (...) {} + } + + bool alreadyVisible = this->SelectedHostBanner->Opacity > 0.01; + if (selectedHost == nullptr) { + if (alreadyVisible && hideSb != nullptr) hideSb->Begin(); + this->SelectedHostText->Text = ref new Platform::String(L""); + } else { + const unsigned int animVer = ++m_hostTextAnimVersion; + Platform::WeakReference weakThis(this); + auto capturedName = newName; + AnimateCrossfadeText( + this->SelectedHostBanner, showSb, hideSb, true, + [weakThis, capturedName]() { + auto that = weakThis.Resolve(); + if (that != nullptr && that->SelectedHostText != nullptr) + that->SelectedHostText->Text = capturedName; + }, + [weakThis, animVer]() { + auto that = weakThis.Resolve(); + return that != nullptr && that->m_hostTextAnimVersion == animVer; + }); + } + } + } catch (...) {} +} + +void HostSelectorPage::SubscribeMoonPhaseRefreshOnNextLayout(bool alsoFocusFirst) +{ + if (HostsGrid == nullptr || m_hostsGrid_ccc_token.Value != 0) return; + Platform::WeakReference weakThis(this); + m_hostsGrid_ccc_token = HostsGrid->LayoutUpdated += + ref new EventHandler([weakThis, alsoFocusFirst](Platform::Object^, Platform::Object^) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + if (that->HostsGrid == nullptr || + that->HostsGrid->Items == nullptr || + that->HostsGrid->Items->Size == 0) return; + + try { that->HostsGrid->LayoutUpdated -= that->m_hostsGrid_ccc_token; } + catch (...) { MLOG(Utils::LogLevel::Warning, "failed to unsubscribe HostsGrid.LayoutUpdated"); } + that->m_hostsGrid_ccc_token.Value = 0; + that->UpdateAllMoonPhases(false, 0); + if (alsoFocusFirst) that->FocusFirstHostItem(4); + }); +} + +void HostSelectorPage::UpdateAllMoonPhases(bool animated, int attempts) +{ + try { + auto grid = HostsGrid; + if (grid == nullptr) return; + int selectedIdx = grid->SelectedIndex; + if (selectedIdx < 0) selectedIdx = 0; + unsigned int count = grid->Items->Size; + bool anyMissing = false; + for (unsigned int i = 0; i < count; ++i) { + try { + auto container = dynamic_cast( + grid->ContainerFromIndex(i)); + if (container == nullptr) { anyMissing = true; continue; } + auto ctrl = FindLunarControl(container); + if (ctrl == nullptr) { anyMissing = true; continue; } + int dist = (int)i - selectedIdx; + double fillAmount = dist < 0 ? -dist * 0.4 : dist * 0.4; + if (fillAmount > 1.0) fillAmount = 1.0; + int side = dist < 0 ? 1 : dist > 0 ? -1 : 0; + ctrl->UpdatePhase(fillAmount, side, animated); + ctrl->SetSelected(i == (unsigned int)selectedIdx, animated); + } catch (...) { + MLOGF(Utils::LogLevel::Warning, "UpdateAllMoonPhases failed for item %u", i); + } + } + if (anyMissing && attempts > 0) { + Platform::WeakReference weakThis(this); + bool capturedAnimated = animated; + int next = attempts - 1; + try { + this->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::Normal, + ref new Windows::UI::Core::DispatchedHandler([weakThis, capturedAnimated, next]() { + auto that = weakThis.Resolve(); + if (that != nullptr) that->UpdateAllMoonPhases(capturedAnimated, next); + })); + } catch (...) {} + } + } catch (...) {} +} + +LunarPhaseControl^ HostSelectorPage::FindLunarControl(Windows::UI::Xaml::DependencyObject^ root) +{ + if (root == nullptr) return nullptr; + if (auto ctrl = dynamic_cast(root)) return ctrl; + try { + int count = VisualTreeHelper::GetChildrenCount(root); + for (int i = 0; i < count; ++i) { + auto found = FindLunarControl(VisualTreeHelper::GetChild(root, i)); + if (found != nullptr) return found; + } + } catch (...) {} + return nullptr; +} + +void HostSelectorPage::NewHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) +{ + auto dialog = ref new AddHostDialog(); + Platform::WeakReference weakThis(this); + + dialog->Configure( + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis, dialog](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + Platform::String^ hostname = dialog->GetHostname(); + if (hostname == nullptr || hostname->Length() == 0) return; + dialog->SetAddButtonEnabled(false); + Concurrency::create_task([that, dialog, hostname]() { + bool status = that->state->AddHost(hostname); + Platform::WeakReference weakPage(that); + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::High, + ref new Windows::UI::Core::DispatchedHandler([weakPage, dialog, status, hostname]() { + if (!status) { + dialog->ShowError("Failed to connect to " + hostname); + dialog->SetAddButtonEnabled(true); + } else { + try { dialog->Hide(); } + catch (...) { MLOG(Utils::LogLevel::Warning, "failed to hide AddHostDialog after successful add"); } + auto page = weakPage.Resolve(); + if (page != nullptr) { + try { + if (page->HostsGrid != nullptr && page->HostsGrid->SelectedIndex < 0 && + page->HostsGrid->Items != nullptr && page->HostsGrid->Items->Size > 0) { + page->HostsGrid->SelectedIndex = 0; + } + } catch (...) { + MLOG(Utils::LogLevel::Warning, "failed to select newly added host"); + } + try { + page->SubscribeMoonPhaseRefreshOnNextLayout(false); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "failed to subscribe HostsGrid.LayoutUpdated for new host"); + } + } + } + })); + }); + }), + nullptr + ); + + dialog->SetRecentHostnames(state->RecentHostnames, state->RecentHostDisplayNames); + try { dialog->XamlRoot = this->XamlRoot; } catch (...) {} + concurrency::create_task(dialog->ShowAsync()); +} + +void HostSelectorPage::GridView_ItemClick(Platform::Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e) +{ + MoonlightHost^ host = (MoonlightHost^)e->ClickedItem; + + if (host->Connected && host->Paired) { + this->Connect(host); + return; + } + + if (host->Connected && !host->Paired) { + this->StartPairing(host); + return; + } + + this->ShowHostActions(host); +} + +void HostSelectorPage::ShowHostActions(MoonlightHost^ host) +{ + currentHost = host; + if (currentHost == nullptr) return; + + bool showWake = !(currentHost->Connected || currentHost->WolPolling); + bool showTest = !showWake; + + auto dialog = ref new HostActionsDialog(); + m_hostActionsDialog = dialog; + + Platform::WeakReference weakThis(this); + + dialog->Configure( + currentHost->ComputerName, + showWake, + showTest, + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr || that->currentHost == nullptr) return; + auto t = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + t->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromRight; + that->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSettingsPage::typeid), that->currentHost, t); + }), + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr || that->currentHost == nullptr) return; + that->wakeHostButton_Click(nullptr, nullptr); + }), + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr || that->currentHost == nullptr) return; + that->testConnectionButton_Click(nullptr, nullptr); + }), + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr || that->currentHost == nullptr) return; + auto confirmDialog = ref new ConfirmDialog(); + Platform::String^ hostName = that->currentHost->ComputerName; + Platform::String^ message = "Remove '" + hostName + "' from your saved hosts?"; + confirmDialog->Configure( + "Remove Host", + message, + L"\xE74D", + "Remove", + "Cancel", + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr || that->currentHost == nullptr) return; + int removedIdx = that->HostsGrid->SelectedIndex; + that->State->RemoveHost(that->currentHost); + that->currentHost = nullptr; + int newSize = (int)that->State->SavedHosts->Size; + if (newSize > 0) { + that->HostsGrid->SelectedIndex = removedIdx < newSize ? removedIdx : newSize - 1; + } else if (that->BackgroundHost != nullptr) { + that->BackgroundHost->ResetBackground(); + } + }) + ); + try { confirmDialog->XamlRoot = that->XamlRoot; } catch (...) {} + concurrency::create_task(confirmDialog->ShowAsync()); + }), + ref new Windows::UI::Xaml::RoutedEventHandler([weakThis](Platform::Object^, Windows::UI::Xaml::RoutedEventArgs^) { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + auto t = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + t->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromRight; + that->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightSettings::typeid), nullptr, t); + }) + ); + + try { + dialog->XamlRoot = this->XamlRoot; + } catch (...) {} + + concurrency::create_task(dialog->ShowAsync()); +} + +void HostSelectorPage::HostsGrid_RightTapped(Platform::Object^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs^ e) +{ + FrameworkElement^ senderElement = dynamic_cast(e->OriginalSource); + if (senderElement == nullptr) return; + + auto itemContainer = dynamic_cast(senderElement); + MoonlightHost^ host = nullptr; + if (itemContainer != nullptr) { + host = dynamic_cast(itemContainer->Content); + } + else { + host = dynamic_cast(senderElement->DataContext); + } + + this->ShowHostActions(host); +} + +void HostSelectorPage::SettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) +{ + auto slideForward = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + slideForward->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromRight; + this->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(MoonlightSettings::typeid), nullptr, slideForward); +} + +void HostSelectorPage::StartPairing(MoonlightHost^ host) { + MoonlightClient* client = new MoonlightClient(); + char ipAddressStr[2048]; + wcstombs_s(NULL, ipAddressStr, host->LastHostname->Data(), 2047); + int status = client->Connect(ipAddressStr); + if (status != 0)return; + char* pin = client->GeneratePIN(); + auto dialog = ref new ::moonlight_xbox_dx::PairDialog(); + wchar_t wpin[32]; + mbstowcs_s(NULL, wpin, pin, 31); + dialog->Configure(ref new Platform::String(wpin)); + try { dialog->XamlRoot = this->XamlRoot; } catch (...) {} + concurrency::create_task(dialog->ShowAsync()); + Concurrency::create_task([dialog, host, client, pin]() { + int a = client->Pair(); + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([a, dialog, host]() + { + if (a == 0) { + try { dialog->Hide(); } + catch (...) { MLOG(Utils::LogLevel::Warning, "failed to hide PairDialog after successful pairing"); } + } + host->UpdateHostInfo(true); + } + )); + }) .then([](concurrency::task t) { + try { + t.get(); + } + catch (const std::exception &e) { + MLOGF(Utils::LogLevel::Error, "StartPairing task exception: %s", e.what()); + } + catch (...) { + MLOG(Utils::LogLevel::Error, "StartPairing task unknown exception"); + } + }); +} + +static bool TryQuickTcpConnect(const std::string& hostOnly) +{ + bool reachable = false; + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) == 0) { + struct addrinfo hints = {}, *res = nullptr; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + if (getaddrinfo(hostOnly.c_str(), "47989", &hints, &res) == 0) { + SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (s != INVALID_SOCKET) { + u_long mode = 1; + ioctlsocket(s, FIONBIO, &mode); + connect(s, res->ai_addr, (int)res->ai_addrlen); + fd_set writeSet; FD_ZERO(&writeSet); FD_SET(s, &writeSet); + timeval tv; tv.tv_sec = 1; tv.tv_usec = 0; + reachable = (select(0, NULL, &writeSet, NULL, &tv) > 0 && FD_ISSET(s, &writeSet)); + closesocket(s); + } + freeaddrinfo(res); + } + WSACleanup(); + } + return reachable; +} + +void HostSelectorPage::Connect(MoonlightHost^ host) { + if (!host->Connected)return; + if (!host->Paired) { + StartPairing(host); + return; + } + state->shouldAutoConnect = true; + continueFetch.store(false); + + Platform::WeakReference weakThis(this); + std::string hostOnly = Utils::PlatformStringToStdString(host->LastHostname); + auto colonPos = hostOnly.find(':'); + if (colonPos != std::string::npos) hostOnly = hostOnly.substr(0, colonPos); + + Concurrency::create_task([weakThis, host, hostOnly]() { + bool reachable = TryQuickTcpConnect(hostOnly); + + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync( + Windows::UI::Core::CoreDispatcherPriority::High, + ref new Windows::UI::Core::DispatchedHandler([weakThis, host, reachable]() { + auto that = weakThis.Resolve(); + if (that == nullptr) return; + if (!reachable) { + that->continueFetch.store(true); + auto dialog = ref new ::moonlight_xbox_dx::AlertDialog(); + dialog->Configure(L"Host Offline", L"The host is not reachable. It may have gone offline."); + try { dialog->XamlRoot = that->XamlRoot; } catch (...) {} + concurrency::create_task(dialog->ShowAsync()); + return; + } + auto slideForward = ref new Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionInfo(); + slideForward->Effect = Windows::UI::Xaml::Media::Animation::SlideNavigationTransitionEffect::FromRight; + that->Frame->Navigate(Windows::UI::Xaml::Interop::TypeName(AppPage::typeid), host, slideForward); + })); + }); +} + +void HostSelectorPage::PollTick(Windows::System::Threading::ThreadPoolTimer^ timer) +{ + if (m_isNavigatedAway.load()) { + try { + if (timer != nullptr) timer->Cancel(); + } catch (...) { + MLOG(Utils::LogLevel::Warning, "PollTick failed to cancel timer for destroyed page"); + } + return; + } + + m_pollActiveCount.fetch_add(1); + if (continueFetch.load()) { + try { + try { + mdns_send_query(); + } catch (...) { + } + query_mdns(); + std::vector hostsSnapshot; + { + auto savedHosts = GetApplicationState()->SavedHosts; + for (unsigned int i = 0; i < savedHosts->Size; i++) { + try { + hostsSnapshot.push_back(savedHosts->GetAt(i)); + } catch (...) { + break; + } + } + } + Concurrency::parallel_for_each(hostsSnapshot.begin(), hostsSnapshot.end(), [](MoonlightHost^ a) { + try { + a->UpdateHostInfo(true); + } catch (...) { + } + }); + } catch (const std::exception &ex) { + MLOGF(Utils::LogLevel::Error, "poll exception: %s", ex.what()); + } catch (...) { + MLOG(Utils::LogLevel::Error, "poll unknown exception"); + } + } + m_pollActiveCount.fetch_sub(1); +} + +void moonlight_xbox_dx::HostSelectorPage::wakeHostButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { + if (currentHost == nullptr) { + return; + } + + try { + bool success = State->WakeHost(currentHost); + if (!success) { + auto fail = ref new ::moonlight_xbox_dx::AlertDialog(); + fail->Configure("Wake Host Failed", "Failed to send Wake-on-LAN packet.\n\nPlease check if Wake-on-LAN is enabled on the host."); + try { fail->XamlRoot = this->XamlRoot; } catch (...) {} + concurrency::create_task(fail->ShowAsync()); + } + + if (success) { + ShowToast(L"Wake on LAN sent"); + auto host = currentHost; + host->WolPolling = true; + PollHostAfterWake(host); + } + } catch (std::exception ex) { + auto errDlg = ref new ::moonlight_xbox_dx::AlertDialog(); + errDlg->Configure("Wake Host Error", "An error occurred while trying to wake the host:\n\n" + Utils::StringFromChars((char *)ex.what())); + try { errDlg->XamlRoot = this->XamlRoot; } catch (...) {} + concurrency::create_task(errDlg->ShowAsync()); + } +} + +void HostSelectorPage::PollHostAfterWake(MoonlightHost^ host) +{ + concurrency::create_task(concurrency::create_async([host]() { + int consecutiveSuccess = 0; + for (int i = 0; i < 240; ++i) { + try { + host->UpdateHostInfo(false); + if (host->Connected) { + consecutiveSuccess++; + if (consecutiveSuccess >= 2) { + host->WolPolling = false; + break; + } + } else { + consecutiveSuccess = 0; + } + } catch (...) { + consecutiveSuccess = 0; + } + Sleep(250); + } + })).then([host](concurrency::task t) { + try { + t.get(); + } catch (...) { + MLOG(Utils::LogLevel::Error, "PollHostAfterWake task unknown exception"); + } + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal, ref new Windows::UI::Core::DispatchedHandler([host]() { + host->WolPolling = false; + })); + }); +} + +static std::string ProbeHostConnection(const std::string& hostOnly) +{ + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) return "WSAStartup failed"; + + struct addrinfo hints; + struct addrinfo *res = nullptr; + ZeroMemory(&hints, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai = getaddrinfo(hostOnly.c_str(), "47989", &hints, &res); + if (gai != 0) { + WSACleanup(); + return std::string("DNS lookup failed: ") + std::to_string(gai); + } + + bool ok = false; + double bestRttMs = -1.0; + for (struct addrinfo *p = res; p != nullptr; p = p->ai_next) { + SOCKET s = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (s == INVALID_SOCKET) continue; + u_long mode = 1; + ioctlsocket(s, FIONBIO, &mode); + + using namespace std::chrono; + auto start = high_resolution_clock::now(); + int rc = connect(s, p->ai_addr, (int)p->ai_addrlen); + if (rc == 0) { + auto end = high_resolution_clock::now(); + double ms = duration_cast(end - start).count() / 1000.0; + if (bestRttMs < 0 || ms < bestRttMs) bestRttMs = ms; + ok = true; + closesocket(s); + break; + } + fd_set writeSet; + FD_ZERO(&writeSet); + FD_SET(s, &writeSet); + timeval tv; tv.tv_sec = 3; tv.tv_usec = 0; + int sel = select(0, NULL, &writeSet, NULL, &tv); + if (sel > 0 && FD_ISSET(s, &writeSet)) { + auto end = high_resolution_clock::now(); + double ms = duration_cast(end - start).count() / 1000.0; + if (bestRttMs < 0 || ms < bestRttMs) bestRttMs = ms; + ok = true; + closesocket(s); + break; + } + closesocket(s); + } + freeaddrinfo(res); + WSACleanup(); + + if (!ok) return "Connection failed"; + if (bestRttMs < 0) return "Connection OK"; + char buf[64]; + snprintf(buf, sizeof(buf), "Connection OK (RTT: %.1f ms)", bestRttMs); + return buf; +} + +void HostSelectorPage::testConnectionButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { + if (currentHost == nullptr) return; + + std::string hostname = Utils::PlatformStringToStdString(currentHost->LastHostname); + auto pos = hostname.find(':'); + std::string hostOnly = (pos == std::string::npos) ? hostname : hostname.substr(0, pos); + + Platform::WeakReference weakThis(this); + concurrency::create_task([hostOnly, weakThis]() { + std::string resultMsg = ProbeHostConnection(hostOnly); + + Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::High, ref new Windows::UI::Core::DispatchedHandler([resultMsg, hostOnly, weakThis]() { + auto dialog = ref new TestConnectionResultDialog(); + dialog->Configure( + Utils::StringFromStdString(hostOnly), + Utils::StringFromStdString(resultMsg) + ); + auto page = weakThis.Resolve(); + if (page != nullptr) { + try { dialog->XamlRoot = page->XamlRoot; } catch (...) {} + } + concurrency::create_task(dialog->ShowAsync()); + })); + }); +} diff --git a/UI/Pages/HostSelectorPage.xaml.h b/UI/Pages/HostSelectorPage.xaml.h new file mode 100644 index 00000000..cc7ee0a8 --- /dev/null +++ b/UI/Pages/HostSelectorPage.xaml.h @@ -0,0 +1,73 @@ + +#pragma once + +#include "UI\Pages\HostSelectorPage.g.h" +#include "UI\Backgrounds\DynamicBackgroundHost.xaml.h" +#include "UI\Modals\HostActionsDialog.xaml.h" +#include "UI\Modals\AddHostDialog.xaml.h" +#include "State\ApplicationState.h" +#include "UI\Controls\LunarPhaseControl.xaml.h" + +#include + +using namespace Windows::UI::Core; +namespace moonlight_xbox_dx +{ + [Windows::Foundation::Metadata::WebHostHidden] + public ref class HostSelectorPage sealed + { + public: + HostSelectorPage(); + property ApplicationState^ State { + ApplicationState^ get() { + return this->state; + } + } + void OnStateLoaded(); + void Connect(MoonlightHost^ host); + protected: + virtual void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; + virtual void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; + private: + ApplicationState ^state; + void NewHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void GridView_ItemClick(Platform::Object^ sender, Windows::UI::Xaml::Controls::ItemClickEventArgs^ e); + void HostsGrid_Loaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void HostsGrid_SizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void HostsGrid_SelectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::SelectionChangedEventArgs^ e); + void CenterSelectedHost(bool immediate = false); + bool EnsureCenteringPadding(); + void WaitForHostContainerThenCenter(bool immediate); + void WaitForHostScrollViewerThenCenter(bool immediate); + void PreRealizeAllHostContainers(); + void StartPairing(MoonlightHost^ host); + void HostsGrid_RightTapped(Platform::Object^ sender, Windows::UI::Xaml::Input::RightTappedRoutedEventArgs^ e); + MoonlightHost^ currentHost; + Windows::UI::Xaml::Controls::ScrollViewer^ m_hostsScrollViewer; + bool m_adjustingCenterPadding = false; + double m_lastCenterPadding = -1.0; + bool m_paddingDirty = false; + bool m_hostsPreRealized = false; + void SettingsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + std::atomic continueFetch; + std::atomic m_isNavigatedAway; + std::atomic m_pollActiveCount; + Windows::System::Threading::ThreadPoolTimer^ m_pollTimer; + void OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e); + void ShowHostActions(MoonlightHost^ host); + HostActionsDialog^ m_hostActionsDialog; + void wakeHostButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void testConnectionButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void UpdateAllMoonPhases(bool animated, int attempts = 0); + void FocusFirstHostItem(int attempts = 4); + LunarPhaseControl^ FindLunarControl(Windows::UI::Xaml::DependencyObject^ root); + void SubscribeMoonPhaseRefreshOnNextLayout(bool alsoFocusFirst); + void PollTick(Windows::System::Threading::ThreadPoolTimer^ timer); + void PollHostAfterWake(MoonlightHost^ host); + void StartPollTimerIfNeeded(); + unsigned int m_hostTextAnimVersion = 0; + Windows::Foundation::EventRegistrationToken m_hostsGrid_ccc_token; + Windows::Foundation::EventRegistrationToken m_hostsContainerWait_token; + Windows::Foundation::EventRegistrationToken m_hostsScrollViewerWait_token; + }; +} diff --git a/UI/Pages/HostSettingsPage.xaml b/UI/Pages/HostSettingsPage.xaml new file mode 100644 index 00000000..34ec25f9 --- /dev/null +++ b/UI/Pages/HostSettingsPage.xaml @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HDR requires Xbox system resolution set to 4K. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + In-stream menu + + + + + + + + + + + + + + + + + + + Left click + + Right click + + Show Keyboard + + Guide / Xbox button + + + + + + + + + + + + + + + + + + + + Close + + Left Click + + Right Click + + Move / Drag + + Press to Scroll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Servers + To make Moonlight work, you need a Server. You can use one of the following options: + + + + + + + + + + + Sunshine + + The recommended open source solution. Compatible with Windows/macOS/Linux and with any GPU. + + + https://github.com/LizardByte/Sunshine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hints + + + + After installing your server, wait or press '+' for adding your PC manually + + + + + During the stream, you can press ' + ' + to show more options and enable mouse mode + + + + + + + In mouse mode, you can move and scroll using your sticks, ' ' Left click + ' ' Right click ' ' Guide/Xbox button + + + + + + + In mouse mode, you can press ' ' to show Keyboard. You can also use a USB Keyboard + + + + + + + + Is your screen a quarter of its size? Change the global composition scale inside the 'Moonlight Settings' of the App + + + + + + + + + + + Support + Need some Help? + + + + + + + + + + GitHub + + Check the Wiki for Support Links, documentation and more! + + + https://github.com/TheElixZammuto/moonlight-xbox/wiki + + + + + + + + + + + diff --git a/MoonlightWelcome.xaml.cpp b/UI/Pages/MoonlightWelcome.xaml.cpp similarity index 88% rename from MoonlightWelcome.xaml.cpp rename to UI/Pages/MoonlightWelcome.xaml.cpp index f795a222..aeb44e9e 100644 --- a/MoonlightWelcome.xaml.cpp +++ b/UI/Pages/MoonlightWelcome.xaml.cpp @@ -1,7 +1,3 @@ -// -// MoonlightWelcome.xaml.cpp -// Implementazione della classe MoonlightWelcome -// #include "pch.h" #include "MoonlightWelcome.xaml.h" @@ -19,12 +15,10 @@ using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Media; using namespace Windows::UI::Xaml::Navigation; -// Il modello di elemento Pagina vuota è documentato all'indirizzo https://go.microsoft.com/fwlink/?LinkId=234238 - MoonlightWelcome::MoonlightWelcome() { InitializeComponent(); - Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseVisible); + Windows::UI::ViewManagement::ApplicationView::GetForCurrentView()->SetDesiredBoundsMode(Windows::UI::ViewManagement::ApplicationViewBoundsMode::UseCoreWindow); this->Loaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &MoonlightWelcome::OnLoaded); this->Unloaded += ref new Windows::UI::Xaml::RoutedEventHandler(this, &MoonlightWelcome::OnUnloaded); @@ -32,9 +26,7 @@ MoonlightWelcome::MoonlightWelcome() void MoonlightWelcome::OnBackRequested(Platform::Object^ e, Windows::UI::Core::BackRequestedEventArgs^ args) { - // UWP on Xbox One triggers a back request whenever the B - // button is pressed which can result in the app being - // suspended if unhandled + if (this->FlipView->SelectedIndex > 0) { args->Handled = true; this->FlipView->SelectedIndex = this->FlipView->SelectedIndex - 1; @@ -81,10 +73,12 @@ void MoonlightWelcome::OnLoaded(Platform::Object^ sender, Windows::UI::Xaml::Rou { auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); m_back_token = navigation->BackRequested += ref new EventHandler(this, &MoonlightWelcome::OnBackRequested); + BackgroundControl->StartAnimations(); } void MoonlightWelcome::OnUnloaded(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); navigation->BackRequested -= m_back_token; + BackgroundControl->StopAnimations(); } diff --git a/MoonlightWelcome.xaml.h b/UI/Pages/MoonlightWelcome.xaml.h similarity index 77% rename from MoonlightWelcome.xaml.h rename to UI/Pages/MoonlightWelcome.xaml.h index b09d53d4..e8db28af 100644 --- a/MoonlightWelcome.xaml.h +++ b/UI/Pages/MoonlightWelcome.xaml.h @@ -1,17 +1,12 @@ -// -// MoonlightWelcome.xaml.h -// Dichiarazione della classe MoonlightWelcome -// #pragma once -#include "MoonlightWelcome.g.h" +#include "UI\Pages\MoonlightWelcome.g.h" +#include "UI\Backgrounds\Streaks\StreaksBackground.xaml.h" namespace moonlight_xbox_dx { - /// - /// Pagina vuota che può essere usata autonomamente oppure per l'esplorazione all'interno di un frame. - /// + [Windows::Foundation::Metadata::WebHostHidden] public ref class MoonlightWelcome sealed { diff --git a/UI/Pages/StreamPage.xaml b/UI/Pages/StreamPage.xaml new file mode 100644 index 00000000..a1fe1a6a --- /dev/null +++ b/UI/Pages/StreamPage.xaml @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pages/StreamPage.xaml.cpp b/UI/Pages/StreamPage.xaml.cpp similarity index 58% rename from Pages/StreamPage.xaml.cpp rename to UI/Pages/StreamPage.xaml.cpp index b4a04b0d..0cd94837 100644 --- a/Pages/StreamPage.xaml.cpp +++ b/UI/Pages/StreamPage.xaml.cpp @@ -1,14 +1,10 @@ -// -// DirectXPage.xaml.cpp -// Implementation of the DirectXPage class. -// #include "pch.h" #include "StreamPage.xaml.h" #include "../Streaming/FFMpegDecoder.h" +#define MLOG_TAG_OVERRIDE "StreamPage" #include #include -#include "../Common/ModalDialog.xaml.h" using namespace moonlight_xbox_dx; @@ -29,6 +25,8 @@ using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Data; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; +using namespace Windows::UI::Xaml::Media::Imaging; using namespace Windows::UI::Xaml::Navigation; using namespace concurrency; @@ -43,22 +41,32 @@ StreamPage::StreamPage(): swapChainPanel->SizeChanged += ref new SizeChangedEventHandler(this, &StreamPage::OnSwapChainPanelSizeChanged); m_deviceResources = std::make_shared(); -} - + m_subMenuCloseTimer = ref new Windows::UI::Xaml::DispatcherTimer(); + m_subMenuCloseTimer->Interval = Windows::Foundation::TimeSpan{ 1000000LL }; + m_subMenuCloseTimer->Tick += ref new Windows::Foundation::EventHandler(this, &StreamPage::OnSubMenuCloseTimer_Tick); +} void StreamPage::OnBackRequested(Platform::Object^ e,Windows::UI::Core::BackRequestedEventArgs^ args) { - // UWP on Xbox One triggers a back request whenever the B - // button is pressed which can result in the app being - // suspended if unhandled + args->Handled = true; } void StreamPage::Page_Loaded(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { this->m_progressView->Visibility = Windows::UI::Xaml::Visibility::Visible; - this->m_progressRing->IsActive = true; + + m_streamMenuVisible = false; + this->MenuShowStoryboard->Stop(); + this->MenuHideStoryboard->Stop(); + this->StreamMenuGrid->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + InstantHideSubMenu(); + + if (m_backgroundImage != nullptr) { + StartBgPanAnimation(); + FadeInBackground(); + } auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); m_back_cookie = navigation->BackRequested += ref new EventHandler(this, &StreamPage::OnBackRequested); @@ -68,14 +76,13 @@ void StreamPage::Page_Loaded(Platform::Object ^ sender, Windows::UI::Xaml::Route keyDownHandler = (Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyDown += ref new Windows::Foundation::TypedEventHandler(this, &StreamPage::OnKeyDown)); keyUpHandler = (Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyUp += ref new Windows::Foundation::TypedEventHandler(this, &StreamPage::OnKeyUp)); - // Detect gamepad connection and disconnection events gamepadAddedHandler = Gamepad::GamepadAdded += ref new EventHandler(this, &StreamPage::OnGamepadAdded); gamepadRemovedHandler = Gamepad::GamepadRemoved += ref new EventHandler(this, &StreamPage::OnGamepadRemoved); try { m_deviceResources->SetSwapChainPanel(swapChainPanel); } catch (...) { - Utils::Log("StreamPage::Page_Loaded: SetSwapChainPanel failed\n"); + MLOG(Utils::LogLevel::Error, "Page_Loaded: SetSwapChainPanel failed\n"); } Platform::WeakReference weakThis(this); @@ -88,21 +95,23 @@ void StreamPage::Page_Loaded(Platform::Object ^ sender, Windows::UI::Xaml::Route that->m_main->CreateWindowSizeDependentResources(); that->m_main->StartRenderLoop(); } catch (const std::exception &ex) { - Utils::Logf("StreamPage::Page_Loaded: Exception when starting stream. Exception: %s", ex.what()); + MLOGF(Utils::LogLevel::Error, "Page_Loaded: Exception when starting stream. Exception: %s", ex.what()); } catch (const std::string &string) { - Utils::Logf("StreamPage::Page_Loaded: Exception when starting stream. Exception: %s", string); + MLOGF(Utils::LogLevel::Error, "Page_Loaded: Exception when starting stream. Exception: %s", string); } catch (Platform::Exception ^ e) { Platform::String ^ errorMsg = ref new Platform::String(); errorMsg = errorMsg->Concat(L"Exception: ", e->Message); errorMsg = errorMsg->Concat(errorMsg, Utils::StringPrintf("%x", e->HResult)); - Utils::Logf("StreamPage::Page_Loaded: Exception when starting stream. Exception: %s", Utils::PlatformStringToStdString(errorMsg)); + MLOGF(Utils::LogLevel::Error, "Page_Loaded: Exception when starting stream. Exception: %s", Utils::PlatformStringToStdString(errorMsg)); } catch (...) { - Utils::Log("StreamPage::Page_Loaded: Exception when starting stream. Exception: Generic Exception"); + MLOG(Utils::LogLevel::Error, "Page_Loaded: Exception when starting stream. Exception: Generic Exception"); } }); } void StreamPage::Page_Unloaded(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { + try { if (m_bgPanStoryboard != nullptr) { m_bgPanStoryboard->Stop(); m_bgPanStoryboard = nullptr; } } catch(...) {} + auto navigation = Windows::UI::Core::SystemNavigationManager::GetForCurrentView(); navigation->BackRequested -= m_back_cookie; @@ -111,18 +120,18 @@ void StreamPage::Page_Unloaded(Platform::Object ^ sender, Windows::UI::Xaml::Rou if (this->m_main) { - Utils::Log("StreamPage::Page_Unloaded stopping m_main render loop\n"); + MLOG(Utils::LogLevel::Info, "Page_Unloaded stopping m_main render loop\n"); try { this->m_main->StopRenderLoop(); this->m_main.reset(); } catch (std::exception &ex) { - Utils::Logf("StreamPage::Page_Unloaded m_main threw an exception: %s\n", ex.what()); + MLOGF(Utils::LogLevel::Error, "Page_Unloaded m_main threw an exception: %s\n", ex.what()); } catch (...) { - Utils::Log("StreamPage::Page_Unloaded m_main threw an exception\n"); + MLOG(Utils::LogLevel::Error, "Page_Unloaded m_main threw an exception\n"); } - Utils::Log("StreamPage::Page_Unloaded m_main reset\n"); + MLOG(Utils::LogLevel::Info, "Page_Unloaded m_main reset\n"); } Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyDown -= keyDownHandler; @@ -136,24 +145,80 @@ StreamPage::~StreamPage() void StreamPage::OnSwapChainPanelSizeChanged(Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e) { if (m_main == nullptr || m_deviceResources == nullptr)return; - Utils::Logf("StreamPage::OnSwapChainPanelSizeChanged( NewSize: %f x %f )\n", e->NewSize.Width, e->NewSize.Height); + MLOGF(Utils::LogLevel::Debug, "OnSwapChainPanelSizeChanged( NewSize: %f x %f )\n", e->NewSize.Width, e->NewSize.Height); critical_section::scoped_lock lock(m_main->GetCriticalSection()); m_deviceResources->SetLogicalSize(e->NewSize); m_main->CreateDeviceDependentResources(); m_main->CreateWindowSizeDependentResources(); } +void StreamPage::SetStreamMenuVisible(bool visible) { + m_streamMenuVisible = visible; + if (visible) { + this->MenuHideStoryboard->Stop(); + this->StreamMenuGrid->Visibility = Windows::UI::Xaml::Visibility::Visible; + this->MenuShowStoryboard->Begin(); + this->FirstMenuButton->Focus(Windows::UI::Xaml::FocusState::Programmatic); + } else { + InstantHideSubMenu(); + this->MenuShowStoryboard->Stop(); + this->MenuHideStoryboard->Begin(); -void StreamPage::flyoutButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) -{ - Windows::UI::Xaml::Controls::Flyout::ShowAttachedFlyout((FrameworkElement^)sender); - m_main->SetFlyoutOpened(true); + } + if (m_main) m_main->SetMenuVisible(visible); +} + +void StreamPage::MenuHideStoryboard_Completed(Platform::Object^ sender, Platform::Object^ e) { + this->StreamMenuGrid->Visibility = Windows::UI::Xaml::Visibility::Collapsed; } +void StreamPage::ShowSubMenu() { + if (m_subMenuVisible) return; + m_subMenuVisible = true; + this->SubMenuHideStoryboard->Stop(); + this->OtherSubMenuPanel->Visibility = Windows::UI::Xaml::Visibility::Visible; + this->SubMenuShowStoryboard->Begin(); +} -void StreamPage::ActionsFlyout_Closed(Platform::Object^ sender, Platform::Object^ e) -{ - if(m_main != nullptr) m_main->SetFlyoutOpened(false); +void StreamPage::HideSubMenu() { + if (!m_subMenuVisible) return; + m_subMenuVisible = false; + this->SubMenuShowStoryboard->Stop(); + this->SubMenuHideStoryboard->Begin(); +} + +void StreamPage::InstantHideSubMenu() { + if (m_subMenuCloseTimer) m_subMenuCloseTimer->Stop(); + m_subMenuVisible = false; + this->SubMenuShowStoryboard->Stop(); + this->SubMenuHideStoryboard->Stop(); + this->OtherSubMenuPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; +} + +void StreamPage::SubMenuHideStoryboard_Completed(Platform::Object^ sender, Platform::Object^ e) { + this->OtherSubMenuPanel->Visibility = Windows::UI::Xaml::Visibility::Collapsed; +} + +void StreamPage::OtherButton_GotFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { + m_subMenuCloseTimer->Stop(); + ShowSubMenu(); +} + +void StreamPage::OtherButton_LostFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { + m_subMenuCloseTimer->Start(); +} + +void StreamPage::SubMenuButton_GotFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { + m_subMenuCloseTimer->Stop(); +} + +void StreamPage::SubMenuButton_LostFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { + m_subMenuCloseTimer->Start(); +} + +void StreamPage::OnSubMenuCloseTimer_Tick(Platform::Object^ sender, Platform::Object^ e) { + m_subMenuCloseTimer->Stop(); + HideSubMenu(); } void StreamPage::toggleMouseButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) @@ -163,13 +228,24 @@ void StreamPage::toggleMouseButton_Click(Platform::Object^ sender, Windows::UI:: void StreamPage::SetMouseMode(bool enabled) { - this->MouseMode = enabled; - if (m_main) m_main->mouseMode = this->MouseMode; + this->MouseMode = enabled; + if (m_main) m_main->mouseMode = this->MouseMode; + + if (this->MouseModeText != nullptr) { + this->MouseModeText->Text = this->MouseMode ? ref new Platform::String(L"Mouse On") : ref new Platform::String(L"Mouse Off"); + } + + if (enabled) { + this->StreamMenuGrid->Visibility = Windows::UI::Xaml::Visibility::Visible; + this->FirstMenuButton->Focus(Windows::UI::Xaml::FocusState::Programmatic); + this->StreamMenuGrid->Visibility = Windows::UI::Xaml::Visibility::Collapsed; + } } void StreamPage::showKeyboardButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { if (!m_main) return; + SetStreamMenuVisible(false); if (GetApplicationState()->EnableKeyboard) { m_main->keyboardMode = true; @@ -190,7 +266,10 @@ void StreamPage::toggleLogsButton_Click(Platform::Object^ sender, Windows::UI::X } void StreamPage::SetShowLogs(bool enabled) { - this->ShowLogs = enabled; + this->ShowLogs = enabled; + if (this->ShowLogsText != nullptr) { + this->ShowLogsText->Text = this->ShowLogs ? ref new Platform::String(L"Logs On") : ref new Platform::String(L"Logs Off"); + } } void StreamPage::toggleStatsButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { @@ -199,7 +278,60 @@ void StreamPage::toggleStatsButton_Click(Platform::Object ^ sender, Windows::UI: } void StreamPage::SetShowStats(bool enabled) { - this->ShowStats = enabled; + this->ShowStats = enabled; + if (this->ShowStatsText != nullptr) { + this->ShowStatsText->Text = this->ShowStats ? ref new Platform::String(L"Stats On") : ref new Platform::String(L"Stats Off"); + } +} + +void StreamPage::StartBgPanAnimation() { + try { + if (PageBackgroundImage == nullptr) return; + + auto transform = ref new TranslateTransform(); + PageBackgroundImage->RenderTransform = transform; + + auto sb = ref new Storyboard(); + sb->RepeatBehavior = RepeatBehaviorHelper::Forever; + sb->AutoReverse = true; + + Windows::UI::Xaml::Duration dur(TimeSpan{ 10LL * 10000000LL }); + + auto ease = ref new SineEase(); + ease->EasingMode = EasingMode::EaseInOut; + + auto animY = ref new DoubleAnimation(); + animY->To = ref new Platform::Box(120.0); + animY->Duration = dur; + animY->EasingFunction = ease; + Storyboard::SetTarget(animY, transform); + Storyboard::SetTargetProperty(animY, ref new Platform::String(L"Y")); + sb->Children->Append(animY); + + sb->Begin(); + m_bgPanStoryboard = sb; + } catch(...) {} +} + +void StreamPage::FadeInBackground() { + try { + if (PageBackgroundImage == nullptr || m_backgroundImage == nullptr) return; + + auto brush = dynamic_cast(PageBackgroundImage->Background); + if (brush == nullptr) return; + brush->ImageSource = m_backgroundImage; + + auto anim = ref new DoubleAnimation(); + anim->From = 0.0; + anim->To = 0.2; + anim->Duration = Windows::UI::Xaml::Duration(TimeSpan{ 2500000LL }); + + auto sb = ref new Storyboard(); + sb->Children->Append(anim); + Storyboard::SetTarget(anim, PageBackgroundImage); + Storyboard::SetTargetProperty(anim, ref new Platform::String(L"Opacity")); + sb->Begin(); + } catch(...) {} } void StreamPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) { @@ -209,6 +341,8 @@ void StreamPage::OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArg if (configuration == nullptr)return; + m_backgroundImage = configuration->backgroundImage; + SetMouseMode(false); SetShowLogs(false); SetShowStats(configuration->enableStats); @@ -220,16 +354,16 @@ void StreamPage::disonnectButton_Click(Platform::Object^ sender, Windows::UI::Xa Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyDown -= keyDownHandler; Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyUp -= keyUpHandler; - // trigger the server disconnected flow this->m_main->moonlightClient->SetConnectionTerminated(); } void StreamPage::OnKeyDown(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ e) { - //Ignore Gamepad input + if (e->VirtualKey >= Windows::System::VirtualKey::GamepadA && e->VirtualKey <= Windows::System::VirtualKey::GamepadRightThumbstickLeft) { return; } + if (!m_main) return; char modifiers = 0; modifiers |= CoreWindow::GetForCurrentThread()->GetKeyState(Windows::System::VirtualKey::Control) == (CoreVirtualKeyStates::Down) ? MODIFIER_CTRL : 0; modifiers |= CoreWindow::GetForCurrentThread()->GetKeyState(Windows::System::VirtualKey::Menu) == (CoreVirtualKeyStates::Down) ? MODIFIER_ALT : 0; @@ -237,10 +371,9 @@ void StreamPage::OnKeyDown(Windows::UI::Core::CoreWindow^ sender, Windows::UI::C this->m_main->OnKeyDown((unsigned short)e->VirtualKey,modifiers); } - void StreamPage::OnKeyUp(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Core::KeyEventArgs^ e) { - //Ignore Gamepad input + if (e->VirtualKey >= Windows::System::VirtualKey::GamepadA && e->VirtualKey <= Windows::System::VirtualKey::GamepadRightThumbstickLeft) { return; } @@ -249,37 +382,37 @@ void StreamPage::OnKeyUp(Windows::UI::Core::CoreWindow^ sender, Windows::UI::Cor modifiers |= CoreWindow::GetForCurrentThread()->GetKeyState(Windows::System::VirtualKey::Menu) == (CoreVirtualKeyStates::Down) ? MODIFIER_ALT : 0; modifiers |= CoreWindow::GetForCurrentThread()->GetKeyState(Windows::System::VirtualKey::Shift) == (CoreVirtualKeyStates::Down) ? MODIFIER_SHIFT : 0; this->m_main->OnKeyUp((unsigned short) e->VirtualKey, modifiers); - } void StreamPage::disconnectAndCloseButton_Click(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e) { Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyDown -= keyDownHandler; Windows::UI::Core::CoreWindow::GetForCurrentThread()->KeyUp -= keyUpHandler; if (this->m_main) { - // trigger the server disconnected flow which will cleanly exit the loop and call StopRenderLoop() + this->m_main->moonlightClient->SetConnectionTerminated(); } auto that = this; - auto progressToken = ::moonlight_xbox_dx::ModalDialog::ShowProgressDialogToken(nullptr, Utils::StringFromStdString("Closing...")); + auto name = (configuration != nullptr && configuration->appName != nullptr) ? configuration->appName : nullptr; + this->ClosingOverlayText->Text = (name != nullptr && name->Length() > 0) + ? ref new Platform::String((std::wstring(L"Closing ") + name->Data() + L"...").c_str()) + : ref new Platform::String(L"Closing..."); + this->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Visible; - concurrency::create_task(concurrency::create_async([that, progressToken]() { + concurrency::create_task(concurrency::create_async([that]() { try { if (that->m_main) { that->m_main->CloseApp(); } } catch (...) { } - })).then([that, progressToken](concurrency::task t) { + })).then([that](concurrency::task t) { try { t.get(); - // UI is sent back to HostSelectorPage in StartRenderLoop(), after the loop exits - // All we need to do is close the progress dialog - - DISPATCH_UI([progressToken] { - ::moonlight_xbox_dx::ModalDialog::HideDialogByToken(progressToken); + DISPATCH_UI([that] { + that->ClosingOverlay->Visibility = Windows::UI::Xaml::Visibility::Collapsed; }); } catch (...) { } @@ -291,25 +424,23 @@ void StreamPage::Keyboard_OnKeyDown(KeyboardControl^ sender, KeyEvent^ e) this->m_main->OnKeyDown(e->VirtualKey, e->Modifiers); } - void StreamPage::Keyboard_OnKeyUp(KeyboardControl^ sender, KeyEvent^ e) { this->m_main->OnKeyUp(e->VirtualKey, e->Modifiers); } - void StreamPage::guideButtonShort_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { + SetStreamMenuVisible(false); this->m_main->SendGuideButton(500); } - void StreamPage::guideButtonLong_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { + SetStreamMenuVisible(false); this->m_main->SendGuideButton(3000); } - void StreamPage::toggleHDR_WinAltB_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { this->m_main->SendWinAltB(); @@ -322,14 +453,14 @@ void StreamPage::resetDecoder_Click(Platform::Object^ sender, Windows::UI::Xaml: void StreamPage::toggleFramePacing_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e) { - // thread safe atomic bool + bool isImmediate = Pacer::instance().getPacingImmediate(); Pacer::instance().setPacingImmediate(isImmediate ? false : true); } void StreamPage::OnPropertyChanged(Platform::String^ propertyName) { - PropertyChanged(this, ref new Windows::UI::Xaml::Data::PropertyChangedEventArgs(propertyName)); + } void StreamPage::OnGamepadAdded(Platform::Object^ sender, Gamepad^ gamepad) @@ -349,4 +480,3 @@ bool StreamPage::ShouldRefreshGamepads() { void StreamPage::RequestRefreshGamepads() { m_refreshGamepads.store(true, std::memory_order_release); } - diff --git a/Pages/StreamPage.xaml.h b/UI/Pages/StreamPage.xaml.h similarity index 75% rename from Pages/StreamPage.xaml.h rename to UI/Pages/StreamPage.xaml.h index dc1862a0..36193956 100644 --- a/Pages/StreamPage.xaml.h +++ b/UI/Pages/StreamPage.xaml.h @@ -1,36 +1,28 @@ -// -// DirectXPage.xaml.h -// Declaration of the DirectXPage class. -// #pragma once -#include "Pages\StreamPage.g.h" +#include "UI\Pages\StreamPage.g.h" #include "Common\DeviceResources.h" #include "Streaming\moonlight_xbox_dxMain.h" #include "KeyboardControl.xaml.h" -#include "Converters\BoolToTextConverter.h" +#include "UI\Converters\BoolToTextConverter.h" using namespace Microsoft::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls; namespace moonlight_xbox_dx { - /// - /// A page that hosts a DirectX SwapChainPanel. - /// - class moonlight_xbox_dxMain; - public ref class StreamPage sealed : Windows::UI::Xaml::Data::INotifyPropertyChanged + class moonlight_xbox_dxMain; + public ref class StreamPage sealed { public: StreamPage(); virtual ~StreamPage(); - virtual event Windows::UI::Xaml::Data::PropertyChangedEventHandler^ PropertyChanged; - void OnPropertyChanged(Platform::String^ propertyName); + void OnPropertyChanged(Platform::String^ propertyName); bool ShouldRefreshGamepads(); void RequestRefreshGamepads(); - + void SetMouseMode(bool enabled); property bool MouseMode { bool get() { return m_mouseMode; } void set(bool value) { @@ -71,17 +63,13 @@ namespace moonlight_xbox_dx return GetApplicationState(); } } - property MenuFlyout^ m_flyout { - MenuFlyout^ get() { - return this->ActionsFlyout; + property Grid^ m_streamMenuGrid { + Grid^ get() { + return this->StreamMenuGrid; } } - property Button^ m_flyoutButton { - Button^ get() { - return this->flyoutButton; - } - } + void SetStreamMenuVisible(bool visible); property Grid^ m_progressView { Grid^ get() { @@ -89,12 +77,6 @@ namespace moonlight_xbox_dx } } - property Microsoft::UI::Xaml::Controls::ProgressRing^ m_progressRing { - Microsoft::UI::Xaml::Controls::ProgressRing ^ get() { - return this->MainProgressRing; - } - } - property TextBlock^ m_statusText { TextBlock^ get() { return this->StatusText; @@ -122,12 +104,11 @@ namespace moonlight_xbox_dx protected: virtual void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs^ e) override; private: - // Track our independent input on a background worker thread. + Windows::Foundation::IAsyncAction^ m_inputLoopWorker; Windows::UI::Core::CoreIndependentInputSource^ m_coreInput; Windows::Foundation::EventRegistrationToken m_back_cookie; - // Resources used to render the DirectX content in the XAML page background. std::shared_ptr m_deviceResources; std::unique_ptr m_main; bool m_windowVisible; @@ -135,14 +116,24 @@ namespace moonlight_xbox_dx void Page_Unloaded(Platform::Object ^ sender, Windows::UI::Xaml::RoutedEventArgs ^ e); void OnSwapChainPanelSizeChanged(Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); - void flyoutButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void ActionsFlyout_Closed(Platform::Object^ sender, Platform::Object^ e); void toggleMouseButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); - void SetMouseMode(bool enabled); + void OtherButton_GotFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void OtherButton_LostFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void SubMenuButton_GotFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void SubMenuButton_LostFocus(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void SubMenuHideStoryboard_Completed(Platform::Object^ sender, Platform::Object^ e); + void OnSubMenuCloseTimer_Tick(Platform::Object^ sender, Platform::Object^ e); + void ShowSubMenu(); + void HideSubMenu(); + void InstantHideSubMenu(); void showKeyboardButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void toggleLogsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void SetShowLogs(bool show); StreamConfiguration^ configuration; + Windows::UI::Xaml::Media::Animation::Storyboard^ m_bgPanStoryboard = nullptr; + Windows::UI::Xaml::Media::Imaging::BitmapImage^ m_backgroundImage = nullptr; + void StartBgPanAnimation(); + void FadeInBackground(); void toggleStatsButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void SetShowStats(bool show); void disonnectButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); @@ -166,6 +157,9 @@ namespace moonlight_xbox_dx bool m_mouseMode = false; bool m_showLogs = false; bool m_showStats = false; + bool m_streamMenuVisible = false; + bool m_subMenuVisible = false; + Windows::UI::Xaml::DispatcherTimer^ m_subMenuCloseTimer = nullptr; + void MenuHideStoryboard_Completed(Platform::Object^ sender, Platform::Object^ e); }; } - diff --git a/UI/Styles/Colors.xaml b/UI/Styles/Colors.xaml new file mode 100644 index 00000000..d4a59392 --- /dev/null +++ b/UI/Styles/Colors.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UI/Styles/Controls.xaml b/UI/Styles/Controls.xaml new file mode 100644 index 00000000..e7e23590 --- /dev/null +++ b/UI/Styles/Controls.xaml @@ -0,0 +1,914 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UI/Utilities/EffectsLibrary.cpp b/UI/Utilities/EffectsLibrary.cpp new file mode 100644 index 00000000..80186492 --- /dev/null +++ b/UI/Utilities/EffectsLibrary.cpp @@ -0,0 +1,623 @@ +#include "pch.h" +#include "UI\Utilities\EffectsLibrary.h" +#include +#include +#include +#define MLOG_TAG_OVERRIDE "EffectsLibrary" +#include "../Utils.hpp" +#include "Common\DirectXHelper.h" +#include +#include +#include "UI\Utilities\ImageHelpers.h" +using namespace Windows::Graphics::Imaging; + +struct DECLSPEC_UUID("5B0D3235-4DBA-4D44-865E-8F1D0ED9F3E4") IMemoryBufferByteAccess : IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetBuffer(BYTE** value, UINT32* capacity) = 0; +}; + +ID3D11Device* EffectsLibrary::m_device = nullptr; +ID3D11DeviceContext* EffectsLibrary::m_context = nullptr; +ID3D11Multithread* EffectsLibrary::m_multithread = nullptr; +ID3D11VertexShader* EffectsLibrary::m_vs = nullptr; +ID3D11PixelShader* EffectsLibrary::m_blurPS = nullptr; +ID3D11Buffer* EffectsLibrary::m_cb = nullptr; +ID3D11SamplerState* EffectsLibrary::m_sampler = nullptr; +std::mutex EffectsLibrary::m_mutex; +bool EffectsLibrary::m_ownsDevice = false; + +void EffectsLibrary::Initialize(ID3D11Device* device, ID3D11DeviceContext* context) +{ + std::lock_guard lock(m_mutex); + + if (m_device == device && m_context == context) + return; + + if (m_vs) { m_vs->Release(); m_vs = nullptr; } + if (m_blurPS) { m_blurPS->Release(); m_blurPS = nullptr; } + if (m_cb) { m_cb->Release(); m_cb = nullptr; } + if (m_sampler) { m_sampler->Release(); m_sampler = nullptr; } + + if (m_ownsDevice) { + if (m_context) { m_context->Release(); m_context = nullptr; } + if (m_device) { m_device->Release(); m_device = nullptr; } + if (m_multithread) { m_multithread->SetMultithreadProtected(FALSE); m_multithread->Release(); m_multithread = nullptr; } + m_ownsDevice = false; + } + + m_device = device; + m_context = context; + + m_ownsDevice = false; + if (m_multithread) { m_multithread->SetMultithreadProtected(FALSE); m_multithread->Release(); m_multithread = nullptr; } +} + +bool EffectsLibrary::EnsureDeviceInitialized() +{ + + if (m_device != nullptr && m_context != nullptr) return true; + + std::lock_guard lock(m_mutex); + if (m_device != nullptr && m_context != nullptr) return true; + + D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0 }; + ID3D11Device* dev = nullptr; + ID3D11DeviceContext* ctx = nullptr; + D3D_FEATURE_LEVEL createdFL = D3D_FEATURE_LEVEL_11_0; + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; +#if defined(_DEBUG) + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, featureLevels, _countof(featureLevels), + D3D11_SDK_VERSION, &dev, &createdFL, &ctx); + if (FAILED(hr) || dev == nullptr || ctx == nullptr) { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "lazy D3D11CreateDevice failed hr=0x%08x\n", hr); + + return false; + } + + m_device = dev; + m_context = ctx; + + Microsoft::WRL::ComPtr mt; + if (SUCCEEDED(m_device->QueryInterface(__uuidof(ID3D11Multithread), reinterpret_cast(mt.GetAddressOf())))) { + mt->SetMultithreadProtected(TRUE); + + mt.Get()->AddRef(); + m_multithread = mt.Get(); + } + m_ownsDevice = true; + return true; +} + +bool EffectsLibrary::EnsureBlurShadersCompiled() +{ + std::lock_guard lock(m_mutex); + if (m_vs != nullptr && m_blurPS != nullptr) return true; + + const char* vsSrc = "struct VSOut { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };\n" + "VSOut VS(uint vid : SV_VertexID) { VSOut o; float2 pos[3] = { float2(-1,-1), float2(-1,3), float2(3,-1) }; o.pos = float4(pos[vid], 0.0f, 1.0f); o.uv = pos[vid] * 0.5f + 0.5f; return o; }\n"; + const char* psSrc = + "Texture2D srcTex : register(t0); SamplerState samp : register(s0); cbuffer BlurCB : register(b0) { float2 texSize; float sigma; int direction; };\n" + "struct VSOut { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };\n" + "float4 PS(VSOut i) : SV_TARGET {\n" + " float s = max(sigma, 0.0001f);\n" + " int radius = (int)ceil(3.0f * s);\n" + " float2 texel = float2(1.0/texSize.x, 1.0/texSize.y);\n" + " float2 step = (direction==0) ? float2(texel.x,0) : float2(0,texel.y);\n" + " float4 sum = float4(0,0,0,0);\n" + " float wsum = 0.0f;\n" + " float twoSigmaSq = 2.0f * s * s;\n" + " for (int k = -radius; k <= radius; ++k) {\n" + " float wk = exp(-((float)(k*k)) / twoSigmaSq);\n" + " sum += srcTex.SampleLevel(samp, i.uv + step * k, 0) * wk;\n" + " wsum += wk;\n" + " }\n" + " return sum / wsum;\n" + "}\n"; + + Microsoft::WRL::ComPtr vsBlob, psBlob, errBlob; + UINT flags = D3DCOMPILE_OPTIMIZATION_LEVEL3; + HRESULT hr = D3DCompile(vsSrc, strlen(vsSrc), nullptr, nullptr, nullptr, "VS", "vs_4_0", flags, 0, vsBlob.GetAddressOf(), errBlob.GetAddressOf()); + if (FAILED(hr)) { + if (errBlob) MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "EnsureBlurShadersCompiled: VS compile error: %s\n", (const char*)errBlob->GetBufferPointer()); + return false; + } + hr = m_device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &m_vs); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "EnsureBlurShadersCompiled: CreateVertexShader failed hr=0x%08x\n", hr); return false; } + + hr = D3DCompile(psSrc, strlen(psSrc), nullptr, nullptr, nullptr, "PS", "ps_4_0", flags, 0, psBlob.GetAddressOf(), errBlob.GetAddressOf()); + if (FAILED(hr)) { + if (errBlob) MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "EnsureBlurShadersCompiled: PS compile error: %s\n", (const char*)errBlob->GetBufferPointer()); + return false; + } + hr = m_device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &m_blurPS); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "EnsureBlurShadersCompiled: CreatePixelShader failed hr=0x%08x\n", hr); return false; } + + if (m_sampler == nullptr) { + D3D11_SAMPLER_DESC sd = {}; + + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + + sd.AddressU = D3D11_TEXTURE_ADDRESS_BORDER; + sd.AddressV = D3D11_TEXTURE_ADDRESS_BORDER; + sd.AddressW = D3D11_TEXTURE_ADDRESS_BORDER; + sd.ComparisonFunc = D3D11_COMPARISON_NEVER; + sd.MinLOD = 0; + sd.MaxLOD = D3D11_FLOAT32_MAX; + sd.BorderColor[0] = 0.0f; + sd.BorderColor[1] = 0.0f; + sd.BorderColor[2] = 0.0f; + sd.BorderColor[3] = 0.0f; + m_device->CreateSamplerState(&sd, &m_sampler); + } + + if (m_cb == nullptr) { + D3D11_BUFFER_DESC cbd = {}; + cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbd.Usage = D3D11_USAGE_DYNAMIC; + cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + cbd.ByteWidth = sizeof(BlurCB); + m_device->CreateBuffer(&cbd, nullptr, &m_cb); + } + return true; +} + +static SoftwareBitmap^ EnsureBgra8Premultiplied(Windows::Graphics::Imaging::SoftwareBitmap^ bmp) +{ + if (bmp == nullptr) return nullptr; + if (bmp->BitmapPixelFormat == BitmapPixelFormat::Bgra8 && bmp->BitmapAlphaMode == BitmapAlphaMode::Premultiplied) + return bmp; + return ImageHelpers::EnsureBgra8Premultiplied(bmp); +} + +bool EffectsLibrary::BoxBlurSoftwareBitmap(Windows::Graphics::Imaging::SoftwareBitmap^ bitmap, int radius) { + using namespace Windows::Graphics::Imaging; + if (bitmap == nullptr) return false; + try { + auto buffer = bitmap->LockBuffer(BitmapBufferAccessMode::ReadWrite); + auto reference = buffer->CreateReference(); + Microsoft::WRL::ComPtr bufferByteAccess; + HRESULT hr = S_OK; + IUnknown* unk = reinterpret_cast(reference); + if (unk == nullptr) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BoxBlurSoftwareBitmap: CreateReference returned null IUnknown\n"); + return false; + } + hr = unk->QueryInterface(IID_PPV_ARGS(&bufferByteAccess)); + BYTE* data = nullptr; UINT32 capacity = 0; + bool usedFastPath = false; + if (!FAILED(hr) && bufferByteAccess != nullptr) { + hr = bufferByteAccess->GetBuffer(&data, &capacity); + if (!FAILED(hr) && data != nullptr) { + usedFastPath = true; + } else { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Warning, "BoxBlurSoftwareBitmap: GetBuffer failed hr=0x%08x\n", hr); + } + } else { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Warning, "BoxBlurSoftwareBitmap: QueryInterface failed hr=0x%08x\n", hr); + } + auto desc = buffer->GetPlaneDescription(0); + int width = desc.Width; + int height = desc.Height; + int stride = desc.Stride; + int start = desc.StartIndex; + if (width <= 0 || height <= 0 || stride <= 0) return false; + size_t planeSize = (size_t)height * (size_t)stride; + std::vector src(planeSize); + if (usedFastPath) { + memcpy(src.data(), data + start, planeSize); + } else { + + reference = nullptr; + buffer = nullptr; + try { + auto ibuf = ref new Windows::Storage::Streams::Buffer((unsigned int)planeSize); + bitmap->CopyToBuffer(ibuf); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ibuf); + reader->ReadBytes(Platform::ArrayReference(src.data(), (unsigned int)planeSize)); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BoxBlurSoftwareBitmap: fallback CopyToBuffer/DataReader failed\n"); + return false; + } + } + + std::vector tmp(src.size()); + std::vector dst(src.size()); + + for (int y = 0; y < height; ++y) { + const uint8_t* row = src.data() + y * stride; + std::vector prefB(width + 1, 0), prefG(width + 1, 0), prefR(width + 1, 0), prefA(width + 1, 0); + for (int x = 0; x < width; ++x) { + uint8_t b = row[x * 4 + 0]; + uint8_t g = row[x * 4 + 1]; + uint8_t r = row[x * 4 + 2]; + uint8_t a = row[x * 4 + 3]; + prefB[x + 1] = prefB[x] + b; + prefG[x + 1] = prefG[x] + g; + prefR[x + 1] = prefR[x] + r; + prefA[x + 1] = prefA[x] + a; + } + for (int x = 0; x < width; ++x) { + int x0 = std::max(0, x - radius); + int x1 = std::min(width - 1, x + radius); + int count = x1 - x0 + 1; + uint32_t sumB = prefB[x1 + 1] - prefB[x0]; + uint32_t sumG = prefG[x1 + 1] - prefG[x0]; + uint32_t sumR = prefR[x1 + 1] - prefR[x0]; + uint32_t sumA = prefA[x1 + 1] - prefA[x0]; + uint8_t* outPx = tmp.data() + y * stride + x * 4; + outPx[0] = (uint8_t)(sumB / count); + outPx[1] = (uint8_t)(sumG / count); + outPx[2] = (uint8_t)(sumR / count); + outPx[3] = (uint8_t)(sumA / count); + } + } + + for (int x = 0; x < width; ++x) { + std::vector prefB(height + 1, 0), prefG(height + 1, 0), prefR(height + 1, 0), prefA(height + 1, 0); + for (int y = 0; y < height; ++y) { + uint8_t* px = tmp.data() + y * stride + x * 4; + prefB[y + 1] = prefB[y] + px[0]; + prefG[y + 1] = prefG[y] + px[1]; + prefR[y + 1] = prefR[y] + px[2]; + prefA[y + 1] = prefA[y] + px[3]; + } + for (int y = 0; y < height; ++y) { + int y0 = std::max(0, y - radius); + int y1 = std::min(height - 1, y + radius); + int count = y1 - y0 + 1; + uint32_t sumB = prefB[y1 + 1] - prefB[y0]; + uint32_t sumG = prefG[y1 + 1] - prefG[y0]; + uint32_t sumR = prefR[y1 + 1] - prefR[y0]; + uint32_t sumA = prefA[y1 + 1] - prefA[y0]; + uint8_t* outPx = dst.data() + y * stride + x * 4; + outPx[0] = (uint8_t)(sumB / count); + outPx[1] = (uint8_t)(sumG / count); + outPx[2] = (uint8_t)(sumR / count); + outPx[3] = (uint8_t)(sumA / count); + } + } + if (usedFastPath) { + memcpy(data + start, dst.data(), planeSize); + } else { + try { + auto writer = ref new Windows::Storage::Streams::DataWriter(); + writer->WriteBytes(Platform::ArrayReference(dst.data(), (unsigned int)planeSize)); + auto outBuf = writer->DetachBuffer(); + bitmap->CopyFromBuffer(outBuf); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BoxBlurSoftwareBitmap: fallback CopyFromBuffer failed\n"); + return false; + } + } + + return true; + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BoxBlurSoftwareBitmap: exception while blurring\n"); + return false; + } +} + +static void CopyBgraRegion(uint8_t* dst, size_t dstRowPitch, const uint8_t* src, size_t srcRowPitch, int pad, bool returnPadded, int copyWidth, int copyHeight) +{ + size_t srcOffsetRow = returnPadded ? 0 : (size_t)pad * srcRowPitch; + size_t srcOffsetCol = returnPadded ? 0 : (size_t)pad * 4; + for (int y = 0; y < copyHeight; ++y) { + const uint8_t* srcRow = src + srcOffsetRow + (size_t)y * srcRowPitch + srcOffsetCol; + size_t copyBytes = std::min((size_t)copyWidth * 4, dstRowPitch); + memcpy(dst + (size_t)y * dstRowPitch, srcRow, copyBytes); + if (dstRowPitch > copyBytes) memset(dst + (size_t)y * dstRowPitch + copyBytes, 0, dstRowPitch - copyBytes); + } +} + +SoftwareBitmap^ EffectsLibrary::GpuBoxBlurSoftwareBitmap(SoftwareBitmap^ bitmap, int radius, bool returnPadded) +{ + using namespace Windows::Graphics::Imaging; + if (bitmap == nullptr) return nullptr; + + if ((m_device == nullptr || m_context == nullptr) && !EnsureDeviceInitialized()) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: D3D device/context not initialized\n"); + return nullptr; + } + + bitmap = EnsureBgra8Premultiplied(bitmap); + + try { + auto buffer = bitmap->LockBuffer(BitmapBufferAccessMode::Read); + auto reference = buffer->CreateReference(); + Microsoft::WRL::ComPtr bufferByteAccess; + HRESULT hr = S_OK; + IUnknown* unk = reinterpret_cast(reference); + BYTE* data = nullptr; UINT32 capacity = 0; + bool usedFastPath = false; + if (unk != nullptr) hr = unk->QueryInterface(IID_PPV_ARGS(&bufferByteAccess)); + if (!FAILED(hr) && bufferByteAccess != nullptr) { + hr = bufferByteAccess->GetBuffer(&data, &capacity); + if (!FAILED(hr) && data != nullptr) usedFastPath = true; + } + + auto desc = buffer->GetPlaneDescription(0); + int width = desc.Width; + int height = desc.Height; + int stride = desc.Stride; + int start = desc.StartIndex; + if (width <= 0 || height <= 0 || stride <= 0) return nullptr; + size_t planeSize = (size_t)height * (size_t)stride; + std::vector src(planeSize); + if (usedFastPath) { + memcpy(src.data(), data + start, planeSize); + } else { + + reference = nullptr; + buffer = nullptr; + try { + auto ibuf = ref new Windows::Storage::Streams::Buffer((unsigned int)planeSize); + bitmap->CopyToBuffer(ibuf); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ibuf); + reader->ReadBytes(Platform::ArrayReference(src.data(), (unsigned int)planeSize)); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: fallback CopyToBuffer failed\n"); + return nullptr; + } + } + + Microsoft::WRL::ComPtr srcTex; + D3D11_TEXTURE2D_DESC texDesc = {}; + + int pad = 54; + int paddedW = width + pad * 2; + int paddedH = height + pad * 2; + int paddedStride = paddedW * 4; + texDesc.Width = paddedW; + texDesc.Height = paddedH; + texDesc.MipLevels = 1; + texDesc.ArraySize = 1; + texDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + texDesc.CPUAccessFlags = 0; + + std::vector paddedSrc((size_t)paddedH * (size_t)paddedStride); + + memset(paddedSrc.data(), 0, paddedSrc.size()); + + for (int y = 0; y < height; ++y) { + uint8_t* dstRow = paddedSrc.data() + ((size_t)(y + pad) * (size_t)paddedStride); + uint8_t* srcRow = src.data() + ((size_t)y * (size_t)stride); + + memcpy(dstRow + pad * 4, srcRow, width * 4); + } + + D3D11_SUBRESOURCE_DATA initData = {}; + initData.pSysMem = paddedSrc.data(); + initData.SysMemPitch = paddedStride; + + hr = m_device->CreateTexture2D(&texDesc, &initData, srcTex.GetAddressOf()); + if (FAILED(hr) || srcTex == nullptr) { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateTexture2D(src padded) failed hr=0x%08x\n", hr); + return nullptr; + } + + Microsoft::WRL::ComPtr srcSRV; + hr = m_device->CreateShaderResourceView(srcTex.Get(), nullptr, srcSRV.GetAddressOf()); + if (FAILED(hr) || srcSRV == nullptr) { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateShaderResourceView(src) failed hr=0x%08x\n", hr); + return nullptr; + } + + Microsoft::WRL::ComPtr rtA, rtB; + Microsoft::WRL::ComPtr rtvA, rtvB; + Microsoft::WRL::ComPtr srvB; + + D3D11_TEXTURE2D_DESC rtDesc = texDesc; + rtDesc.Width = paddedW; + rtDesc.Height = paddedH; + rtDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + rtDesc.Usage = D3D11_USAGE_DEFAULT; + rtDesc.CPUAccessFlags = 0; + + hr = m_device->CreateTexture2D(&rtDesc, nullptr, rtA.GetAddressOf()); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateTexture2D(rtA) failed hr=0x%08x\n", hr); return nullptr; } + hr = m_device->CreateTexture2D(&rtDesc, nullptr, rtB.GetAddressOf()); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateTexture2D(rtB) failed hr=0x%08x\n", hr); return nullptr; } + hr = m_device->CreateRenderTargetView(rtA.Get(), nullptr, rtvA.GetAddressOf()); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateRenderTargetView(rtA) failed hr=0x%08x\n", hr); return nullptr; } + hr = m_device->CreateRenderTargetView(rtB.Get(), nullptr, rtvB.GetAddressOf()); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateRenderTargetView(rtB) failed hr=0x%08x\n", hr); return nullptr; } + hr = m_device->CreateShaderResourceView(rtB.Get(), nullptr, srvB.GetAddressOf()); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateSRV(rtB) failed hr=0x%08x\n", hr); return nullptr; } + + if (!EnsureBlurShadersCompiled()) return nullptr; + + D3D11_VIEWPORT vp = {}; + vp.TopLeftX = 0.f; + vp.TopLeftY = 0.f; + vp.Width = (float)paddedW; + vp.Height = (float)paddedH; + vp.MinDepth = 0.f; + vp.MaxDepth = 1.f; + + { + std::lock_guard lock(m_mutex); + m_context->IASetInputLayout(nullptr); + m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + m_context->VSSetShader(m_vs, nullptr, 0); + m_context->PSSetShader(m_blurPS, nullptr, 0); + ID3D11SamplerState* samps[1] = { m_sampler }; + m_context->PSSetSamplers(0, 1, samps); + + BlurCB cbdata; + cbdata.texSize = DirectX::XMFLOAT2((float)paddedW, (float)paddedH); + cbdata.sigma = (float)radius; + + cbdata.direction = 0; + D3D11_MAPPED_SUBRESOURCE mapped = {}; + if (m_cb) { + if (SUCCEEDED(m_context->Map(m_cb, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + memcpy(mapped.pData, &cbdata, sizeof(cbdata)); + m_context->Unmap(m_cb, 0); + } + ID3D11Buffer* cbs[1] = { m_cb }; + m_context->PSSetConstantBuffers(0, 1, cbs); + } + + ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; + ID3D11ShaderResourceView* srvSrcPad = srcSRV.Get(); + m_context->PSSetShaderResources(0, 1, &srvSrcPad); + ID3D11RenderTargetView* rtvBptr = rtvB.Get(); + m_context->OMSetRenderTargets(1, &rtvBptr, nullptr); + m_context->RSSetViewports(1, &vp); + m_context->Draw(3, 0); + + ID3D11RenderTargetView* nullRTVArr[1] = { nullptr }; + m_context->OMSetRenderTargets(1, nullRTVArr, nullptr); + + m_context->PSSetShaderResources(0, 1, nullSRV); + cbdata.direction = 1; + if (m_cb) { + if (SUCCEEDED(m_context->Map(m_cb, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + memcpy(mapped.pData, &cbdata, sizeof(cbdata)); + m_context->Unmap(m_cb, 0); + } + ID3D11Buffer* cbs2[1] = { m_cb }; + m_context->PSSetConstantBuffers(0, 1, cbs2); + } + ID3D11ShaderResourceView* srvBptr = srvB.Get(); + m_context->PSSetShaderResources(0, 1, &srvBptr); + ID3D11RenderTargetView* rtvAptr = rtvA.Get(); + m_context->OMSetRenderTargets(1, &rtvAptr, nullptr); + m_context->RSSetViewports(1, &vp); + m_context->Draw(3, 0); + + m_context->PSSetShaderResources(0, 1, nullSRV); + + ID3D11RenderTargetView* nullRTV[1] = { nullptr }; + m_context->OMSetRenderTargets(1, nullRTV, nullptr); + } + + Microsoft::WRL::ComPtr staging; + D3D11_TEXTURE2D_DESC stagingDesc = rtDesc; + stagingDesc.Usage = D3D11_USAGE_STAGING; + stagingDesc.BindFlags = 0; + stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + stagingDesc.MiscFlags = 0; + hr = m_device->CreateTexture2D(&stagingDesc, nullptr, staging.GetAddressOf()); + if (FAILED(hr)) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: CreateTexture2D(staging) failed hr=0x%08x\n", hr); return nullptr; } + { + std::lock_guard lock(m_mutex); + + m_context->CopyResource(staging.Get(), rtA.Get()); + + D3D11_MAPPED_SUBRESOURCE mapSR = {}; + hr = m_context->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &mapSR); + if (FAILED(hr)) { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: Map staging failed hr=0x%08x\n", hr); + return nullptr; + } + bool stagingMapped = true; + + int copyWidth = returnPadded ? paddedW : width; + int copyHeight = returnPadded ? paddedH : height; + uint8_t* srcPtr = (uint8_t*)mapSR.pData; + size_t srcRowPitch = mapSR.RowPitch; + + try { + auto outBmp = ref new SoftwareBitmap(BitmapPixelFormat::Bgra8, copyWidth, copyHeight, BitmapAlphaMode::Premultiplied); + auto outBuf = outBmp->LockBuffer(BitmapBufferAccessMode::Write); + auto outRef = outBuf->CreateReference(); + Microsoft::WRL::ComPtr outAccess; + IUnknown* outUnk = reinterpret_cast(outRef); + + static std::atomic s_outAccessAvailable(-1); + HRESULT outQiHr = E_FAIL; + BYTE* outData = nullptr; UINT32 outCap = 0; + + if (s_outAccessAvailable.load() == 1) { + if (outUnk != nullptr) outQiHr = outUnk->QueryInterface(IID_PPV_ARGS(&outAccess)); + if (SUCCEEDED(outQiHr) && outAccess) outAccess->GetBuffer(&outData, &outCap); + } else if (s_outAccessAvailable.load() == 0) { + outQiHr = E_NOINTERFACE; + } else { + try { + if (outUnk != nullptr) outQiHr = outUnk->QueryInterface(IID_PPV_ARGS(&outAccess)); + } catch (...) { + outQiHr = RPC_E_DISCONNECTED; + } + if (SUCCEEDED(outQiHr) && outAccess) { + outAccess->GetBuffer(&outData, &outCap); + s_outAccessAvailable.store(1); + } else { + s_outAccessAvailable.store(0); + } + } + + if (SUCCEEDED(outQiHr) && outData != nullptr) { + auto outDesc = outBuf->GetPlaneDescription(0); + uint8_t* dstPtr = outData + outDesc.StartIndex; + size_t dstRowPitch = outDesc.Stride; + if (srcPtr == nullptr) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: invalid mapped source pointer (null)\n"); + return nullptr; + } + if (srcRowPitch < (size_t)width * 4) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: unexpected srcRowPitch=%u (width*4=%u)\n", (unsigned)srcRowPitch, (unsigned)(width*4)); + return nullptr; + } + if (outCap < outDesc.StartIndex + dstRowPitch * (size_t)height) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: output buffer capacity too small (cap=%u required=%zu)\n", outCap, outDesc.StartIndex + dstRowPitch * (size_t)height); + return nullptr; + } + CopyBgraRegion(dstPtr, dstRowPitch, srcPtr, srcRowPitch, pad, returnPadded, copyWidth, copyHeight); + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + return outBmp; + } + + try { + if (srcPtr == nullptr) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: invalid mapped source pointer (null) in fallback\n"); + return nullptr; + } + if (srcRowPitch < (size_t)4) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: unexpected small srcRowPitch=%u in fallback\n", (unsigned)srcRowPitch); + return nullptr; + } + auto outDesc = outBuf->GetPlaneDescription(0); + size_t dstRowPitch = outDesc.Stride; + size_t allocSize = (size_t)copyHeight * dstRowPitch; + if (allocSize == 0 || allocSize / (size_t)copyHeight != dstRowPitch) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: integer overflow or zero detected allocating tmpBuf\n"); + return nullptr; + } + std::vector tmpBuf(allocSize); + CopyBgraRegion(tmpBuf.data(), dstRowPitch, srcPtr, srcRowPitch, pad, returnPadded, copyWidth, copyHeight); + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + + auto writer = ref new Windows::Storage::Streams::DataWriter(); + writer->WriteBytes(Platform::ArrayReference(tmpBuf.data(), (unsigned int)tmpBuf.size())); + auto outIBuf = writer->DetachBuffer(); + return SoftwareBitmap::CreateCopyFromBuffer(outIBuf, BitmapPixelFormat::Bgra8, copyWidth, copyHeight, BitmapAlphaMode::Premultiplied); + } catch(...) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: fallback CopyFromBuffer failed\n"); + return nullptr; + } + } catch(...) { + if (stagingMapped) { m_context->Unmap(staging.Get(), 0); stagingMapped = false; } + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: exception while creating output bitmap\n"); + return nullptr; + } + } + + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "GpuBoxBlurSoftwareBitmap: unexpected exception\n"); + return nullptr; + } +} + diff --git a/UI/Utilities/EffectsLibrary.h b/UI/Utilities/EffectsLibrary.h new file mode 100644 index 00000000..9e3ad09f --- /dev/null +++ b/UI/Utilities/EffectsLibrary.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include +#include + +class EffectsLibrary { + +public: + + static void Initialize(ID3D11Device* device, ID3D11DeviceContext* context); + + static bool BoxBlurSoftwareBitmap(Windows::Graphics::Imaging::SoftwareBitmap^ bitmap, int radius); + + static Windows::Graphics::Imaging::SoftwareBitmap^ GpuBoxBlurSoftwareBitmap(Windows::Graphics::Imaging::SoftwareBitmap^ bitmap, int radius, bool returnPadded = false); + + static ID3D11Device* GetDevice() { return m_device; } + + static ID3D11DeviceContext* GetContext() { return m_context; } + +private: + + static ID3D11Device* m_device; + + static ID3D11DeviceContext* m_context; + static ID3D11Multithread* m_multithread; + + static bool m_ownsDevice; + + static ID3D11VertexShader* m_vs; + + static ID3D11PixelShader* m_blurPS; + + static ID3D11Buffer* m_cb; + + static ID3D11SamplerState* m_sampler; + + static std::mutex m_mutex; + + struct BlurCB { + + DirectX::XMFLOAT2 texSize; + + float sigma; + + int direction; + + }; + + static bool EnsureDeviceInitialized(); + + static bool EnsureBlurShadersCompiled(); + +}; \ No newline at end of file diff --git a/UI/Utilities/ImageHelpers.cpp b/UI/Utilities/ImageHelpers.cpp new file mode 100644 index 00000000..3313a59d --- /dev/null +++ b/UI/Utilities/ImageHelpers.cpp @@ -0,0 +1,646 @@ +#include "pch.h" +#include "ImageHelpers.h" +#define MLOG_TAG_OVERRIDE "ImageHelpers" +#include "../Utils.hpp" +#include "UI\Utilities\EffectsLibrary.h" + +struct DECLSPEC_UUID("5B0D3235-4DBA-4D44-865E-8F1D0ED9F3E4") IMemoryBufferByteAccess : IUnknown { + virtual HRESULT STDMETHODCALLTYPE GetBuffer(BYTE** value, UINT32* capacity) = 0; +}; + +using namespace Platform; +using namespace Windows::Storage; +using namespace Windows::Storage::Streams; +using namespace Windows::Graphics::Imaging; +using namespace concurrency; + +static concurrency::task DecodeStreamToSoftwareBitmapAsync(concurrency::task streamTask, const char* context) { + return streamTask.then([context](task st) -> task { + try { + auto stream = st.get(); + if (stream == nullptr || stream->Size == 0) return task_from_result(nullptr); + return create_task(BitmapDecoder::CreateAsync(stream)).then([](BitmapDecoder^ decoder) -> task { + if (decoder == nullptr) return task_from_result(nullptr); + return create_task(decoder->GetSoftwareBitmapAsync()).then([](SoftwareBitmap^ sb) -> SoftwareBitmap^ { + try { return ImageHelpers::EnsureBgra8Premultiplied(sb); } catch(...) { MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "EnsureBgra8Premultiplied failed\n"); return nullptr; } + }); + }).then([](task sbTask) -> SoftwareBitmap^ { + try { return sbTask.get(); } + catch (Platform::COMException^ ex) { MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "BitmapDecoder/GetSoftwareBitmap failed hr=0x%08x\n", ex->HResult); return nullptr; } + catch(...) { MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BitmapDecoder/GetSoftwareBitmap unknown error\n"); return nullptr; } + }); + } catch(...) { + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Error, "LoadSoftwareBitmapFromUriOrPathAsync: stream task failed (%s)\n", context); + return task_from_result(nullptr); + } + }); +} + +concurrency::task ImageHelpers::LoadSoftwareBitmapFromUriOrPathAsync(String^ path) { + if (path == nullptr) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "LoadSoftwareBitmapFromUriOrPathAsync: path is null\n"); + return task_from_result(nullptr); + } + const wchar_t* raw = path->Data(); + try { + + size_t rawLen = wcslen(raw); + if (rawLen >= 4 && _wcsicmp(raw + rawLen - 4, L".svg") == 0) { + return task_from_result(nullptr); + } + + if (wcsncmp(raw, L"ms-appx://", 10) == 0 || wcsncmp(raw, L"ms-appdata://", 12) == 0) { + auto uri = ref new Windows::Foundation::Uri(path); + + concurrency::task streamTask = create_task(StorageFile::GetFileFromApplicationUriAsync(uri)) + .then([](task fileTask) -> task { + try { + StorageFile^ file = fileTask.get(); + return create_task(file->OpenReadAsync()).then([](task sTask) -> IRandomAccessStream^ { + try { auto s = sTask.get(); return safe_cast(s); } catch(...) { return nullptr; } + }); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "LoadSoftwareBitmapFromUriOrPathAsync: GetFileFromApplicationUriAsync failed\n"); + return task_from_result(nullptr); + } + }); + return DecodeStreamToSoftwareBitmapAsync(streamTask, "ms-appx/ms-appdata"); + } + + if ((wcslen(raw) >= 2 && raw[1] == L':') || (wcslen(raw) >= 2 && raw[0] == L'\\' && raw[1] == L'\\')) { + + try { + auto localPath = Windows::Storage::ApplicationData::Current->LocalFolder->Path->Data(); + size_t lpLen = wcslen(localPath); + if (_wcsnicmp(raw, localPath, lpLen) == 0) { + + const wchar_t* rel = raw + lpLen; + if (*rel == L'\\' || *rel == L'/') ++rel; + Platform::String^ relStr = ref new Platform::String(rel); + concurrency::task streamTask = create_task(Windows::Storage::ApplicationData::Current->LocalFolder->GetFileAsync(relStr)).then([](StorageFile^ file) -> task { + if (file == nullptr) return task_from_result(nullptr); + return create_task(file->OpenReadAsync()).then([](IRandomAccessStreamWithContentType^ s) -> IRandomAccessStream^ { return safe_cast(s); }); + }); + return DecodeStreamToSoftwareBitmapAsync(streamTask, "LocalFolder-relative path"); + } + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "LoadSoftwareBitmapFromUriOrPathAsync: LocalFolder-relative path attempt failed, falling back\n"); + } + + concurrency::task streamTask = create_task(StorageFile::GetFileFromPathAsync(path)).then([](task fileTask) -> task { + try { + StorageFile^ file = fileTask.get(); + return create_task(file->OpenReadAsync()).then([](task sTask) -> IRandomAccessStream^ { + try { auto s = sTask.get(); return safe_cast(s); } catch(...) { return nullptr; } + }); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "LoadSoftwareBitmapFromUriOrPathAsync: GetFileFromPathAsync failed\n"); + return task_from_result(nullptr); + } + }); + return DecodeStreamToSoftwareBitmapAsync(streamTask, "file path"); + } + + MLOGF(moonlight_xbox_dx::Utils::LogLevel::Warning, "LoadSoftwareBitmapFromUriOrPathAsync: unsupported path='%S'\n", raw); + return task_from_result(nullptr); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "LoadSoftwareBitmapFromUriOrPathAsync: exception during load\n"); + return task_from_result(nullptr); + } +} + +static void UnpremultiplyBgraPixel(uint8_t pb, uint8_t pg, uint8_t pr, uint8_t pa, + uint8_t& outB, uint8_t& outG, uint8_t& outR, uint8_t& outA) { + if (pa == 0) { outB = outG = outR = outA = 0; return; } + uint32_t r = (uint32_t)pr * 255 + (pa / 2); + uint32_t g = (uint32_t)pg * 255 + (pa / 2); + uint32_t b = (uint32_t)pb * 255 + (pa / 2); + outR = (uint8_t)std::min(255, r / pa); + outG = (uint8_t)std::min(255, g / pa); + outB = (uint8_t)std::min(255, b / pa); + outA = pa; +} + +static SoftwareBitmap^ BuildStraightAlphaBitmap(SoftwareBitmap^ bitmap) { + try { + unsigned int w = bitmap->PixelWidth; + unsigned int h = bitmap->PixelHeight; + + SoftwareBitmap^ src = ImageHelpers::EnsureBgra8Premultiplied(bitmap); + if (src == nullptr) src = bitmap; + + auto out = ref new SoftwareBitmap(BitmapPixelFormat::Bgra8, (int)w, (int)h, BitmapAlphaMode::Straight); + + bool fastPath = false; + try { + auto srcBuf = src->LockBuffer(BitmapBufferAccessMode::Read); + auto outBuf = out->LockBuffer(BitmapBufferAccessMode::Write); + auto srcRef = srcBuf->CreateReference(); + auto outRef = outBuf->CreateReference(); + Microsoft::WRL::ComPtr srcAccess; + Microsoft::WRL::ComPtr outAccess; + IUnknown* srcUnk = reinterpret_cast(srcRef); + IUnknown* outUnk = reinterpret_cast(outRef); + BYTE* srcData = nullptr; UINT32 srcCap = 0; + BYTE* outData = nullptr; UINT32 outCap = 0; + if (srcUnk != nullptr && outUnk != nullptr && SUCCEEDED(srcUnk->QueryInterface(IID_PPV_ARGS(&srcAccess))) && SUCCEEDED(outUnk->QueryInterface(IID_PPV_ARGS(&outAccess))) + && SUCCEEDED(srcAccess->GetBuffer(&srcData, &srcCap)) && SUCCEEDED(outAccess->GetBuffer(&outData, &outCap))) { + auto srcDesc = srcBuf->GetPlaneDescription(0); + auto outDesc = outBuf->GetPlaneDescription(0); + for (unsigned int y = 0; y < h; ++y) { + uint8_t* srow = srcData + srcDesc.StartIndex + (size_t)y * srcDesc.Stride; + uint8_t* orow = outData + outDesc.StartIndex + (size_t)y * outDesc.Stride; + for (unsigned int x = 0; x < w; ++x) { + UnpremultiplyBgraPixel(srow[x*4 + 0], srow[x*4 + 1], srow[x*4 + 2], srow[x*4 + 3], + orow[x*4 + 0], orow[x*4 + 1], orow[x*4 + 2], orow[x*4 + 3]); + } + } + fastPath = true; + } + } catch(...) { fastPath = false; } + + if (!fastPath) { + + try { + unsigned int sh = src->PixelHeight; + auto srcBuf2 = src->LockBuffer(BitmapBufferAccessMode::Read); + auto srcDesc2 = srcBuf2->GetPlaneDescription(0); + std::vector srcTmp((size_t)sh * (size_t)srcDesc2.Stride); + try { + auto ib = ref new Windows::Storage::Streams::Buffer((unsigned int)srcTmp.size()); + src->CopyToBuffer(ib); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ib); + reader->ReadBytes(Platform::ArrayReference(srcTmp.data(), (unsigned int)srcTmp.size())); + } catch(...) { + + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "BuildStraightAlphaBitmap: failed to read src buffer (fallback path)\n"); + } + + auto outDesc = out->LockBuffer(BitmapBufferAccessMode::Write)->GetPlaneDescription(0); + std::vector outTmp((size_t)h * (size_t)outDesc.Stride); + for (unsigned int y = 0; y < h; ++y) { + uint8_t* srow = srcTmp.data() + (size_t)y * srcDesc2.Stride; + uint8_t* orow = outTmp.data() + (size_t)y * outDesc.Stride; + for (unsigned int x = 0; x < w; ++x) { + UnpremultiplyBgraPixel(srow[x*4 + 0], srow[x*4 + 1], srow[x*4 + 2], srow[x*4 + 3], + orow[x*4 + 0], orow[x*4 + 1], orow[x*4 + 2], orow[x*4 + 3]); + } + } + + try { + auto writer = ref new Windows::Storage::Streams::DataWriter(); + writer->WriteBytes(Platform::ArrayReference(outTmp.data(), (unsigned int)outTmp.size())); + auto buf = writer->DetachBuffer(); + out->CopyFromBuffer(buf); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BuildStraightAlphaBitmap: failed to write unpremultiplied buffer into out bitmap\n"); + } + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "BuildStraightAlphaBitmap: fallback unpremultiply path failed\n"); + } + } + + return out; + } catch(...) { + + return bitmap; + } +} + +static Platform::Array^ ExtractStraightAlphaPixels(SoftwareBitmap^ bitmap, unsigned int outW, unsigned int outH) { + try { + SoftwareBitmap^ src = ImageHelpers::EnsureBgra8Premultiplied(bitmap); + auto buf = src->LockBuffer(BitmapBufferAccessMode::Read); + auto desc = buf->GetPlaneDescription(0); + std::vector srcBytes((size_t)outH * (size_t)desc.Stride); + try { + auto ib = ref new Windows::Storage::Streams::Buffer((unsigned int)srcBytes.size()); + src->CopyToBuffer(ib); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ib); + reader->ReadBytes(Platform::ArrayReference(srcBytes.data(), (unsigned int)srcBytes.size())); + } catch(...) { srcBytes.clear(); } + + if (srcBytes.empty()) return nullptr; + + size_t pixCount = (size_t)outW * (size_t)outH; + std::vector outPixels(pixCount * 4); + for (unsigned int y = 0; y < outH; ++y) { + uint8_t* srow = srcBytes.data() + (size_t)y * desc.Stride; + for (unsigned int x = 0; x < outW; ++x) { + size_t idx = ((size_t)y * outW + x) * 4; + UnpremultiplyBgraPixel(srow[x*4 + 0], srow[x*4 + 1], srow[x*4 + 2], srow[x*4 + 3], + outPixels[idx + 0], outPixels[idx + 1], outPixels[idx + 2], outPixels[idx + 3]); + } + } + auto pixelArr = ref new Platform::Array((unsigned int)outPixels.size()); + memcpy(pixelArr->Data, outPixels.data(), outPixels.size()); + return pixelArr; + } catch(...) { + return nullptr; + } +} + +concurrency::task ImageHelpers::EncodeSoftwareBitmapToPngStreamAsync(SoftwareBitmap^ bitmap) { + if (bitmap == nullptr) return task_from_result(nullptr); + try { + auto stream = ref new InMemoryRandomAccessStream(); + + SoftwareBitmap^ encodeBitmap = BuildStraightAlphaBitmap(bitmap); + + unsigned int outW = bitmap->PixelWidth; + unsigned int outH = bitmap->PixelHeight; + Platform::Array^ pixelArr = ExtractStraightAlphaPixels(encodeBitmap != nullptr ? encodeBitmap : bitmap, outW, outH); + + return concurrency::create_task(BitmapEncoder::CreateAsync(BitmapEncoder::PngEncoderId, stream)).then([pixelArr, outW, outH, stream, encodeBitmap](BitmapEncoder^ encoder) -> concurrency::task { + bool usedSetPixelData = false; + try { + if (pixelArr != nullptr) { + encoder->SetPixelData(BitmapPixelFormat::Bgra8, BitmapAlphaMode::Straight, outW, outH, 96.0, 96.0, pixelArr); + usedSetPixelData = true; + } + } catch(...) { usedSetPixelData = false; } + + if (!usedSetPixelData) { + + try { + + if (encodeBitmap != nullptr) encoder->SetSoftwareBitmap(encodeBitmap); + else encoder->SetSoftwareBitmap(EnsureBgra8Premultiplied(ref new SoftwareBitmap(BitmapPixelFormat::Bgra8, outW, outH, BitmapAlphaMode::Premultiplied))); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "EncodeSoftwareBitmapToPngStreamAsync: SetSoftwareBitmap fallback failed, encoder has no pixel data\n"); + } + } + return concurrency::create_task(encoder->FlushAsync()); + }).then([stream]() -> IRandomAccessStream^ { + try { + stream->Seek(0); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "EncodeSoftwareBitmapToPngStreamAsync: exception seeking stream\n"); + } + return stream; + }); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "EncodeSoftwareBitmapToPngStreamAsync: exception encoding\n"); + return task_from_result(nullptr); + } +} + +SoftwareBitmap^ ImageHelpers::EnsureBgra8Premultiplied(SoftwareBitmap^ bitmap) { + if (bitmap == nullptr) return nullptr; + if (bitmap->BitmapPixelFormat != BitmapPixelFormat::Bgra8 || bitmap->BitmapAlphaMode != BitmapAlphaMode::Premultiplied) { + try { + auto conv = SoftwareBitmap::Convert(bitmap, BitmapPixelFormat::Bgra8, BitmapAlphaMode::Premultiplied); + return conv; + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "EnsureBgra8Premultiplied: conversion failed\n"); + return bitmap; + } + } + return bitmap; +} + +SoftwareBitmap^ ImageHelpers::ResizeSoftwareBitmap(SoftwareBitmap^ src, unsigned int width, unsigned int height) { + if (src == nullptr) return nullptr; + try { + src = EnsureBgra8Premultiplied(src); + unsigned int srcW = src->PixelWidth; + unsigned int srcH = src->PixelHeight; + if (srcW == width && srcH == height) return src; + + auto outBmp = ref new SoftwareBitmap(BitmapPixelFormat::Bgra8, width, height, BitmapAlphaMode::Premultiplied); + + auto srcBuf = src->LockBuffer(BitmapBufferAccessMode::Read); + auto srcDesc = srcBuf->GetPlaneDescription(0); + auto dstBuf = outBmp->LockBuffer(BitmapBufferAccessMode::Write); + auto dstDesc = dstBuf->GetPlaneDescription(0); + + auto srcRef = srcBuf->CreateReference(); + auto dstRef = dstBuf->CreateReference(); + Microsoft::WRL::ComPtr srcAccess; + Microsoft::WRL::ComPtr dstAccess; + IUnknown* srcUnk = reinterpret_cast(srcRef); + IUnknown* dstUnk = reinterpret_cast(dstRef); + BYTE* srcData = nullptr; UINT32 srcCap = 0; + BYTE* dstData = nullptr; UINT32 dstCap = 0; + bool haveFast = srcUnk != nullptr && dstUnk != nullptr + && SUCCEEDED(srcUnk->QueryInterface(IID_PPV_ARGS(&srcAccess))) && SUCCEEDED(dstUnk->QueryInterface(IID_PPV_ARGS(&dstAccess))) + && SUCCEEDED(srcAccess->GetBuffer(&srcData, &srcCap)) && SUCCEEDED(dstAccess->GetBuffer(&dstData, &dstCap)); + + if (haveFast) { + for (unsigned int y = 0; y < height; ++y) { + unsigned int sy = (unsigned int)((uint64_t)y * srcH / height); + uint8_t* srcRow = srcData + srcDesc.StartIndex + sy * srcDesc.Stride; + uint8_t* dstRow = dstData + dstDesc.StartIndex + y * dstDesc.Stride; + for (unsigned int x = 0; x < width; ++x) { + unsigned int sx = (unsigned int)((uint64_t)x * srcW / width); + uint8_t* pSrc = srcRow + sx * 4; + uint8_t* pDst = dstRow + x * 4; + pDst[0] = pSrc[0]; pDst[1] = pSrc[1]; pDst[2] = pSrc[2]; pDst[3] = pSrc[3]; + } + } + return outBmp; + } + + std::vector srcBufData((size_t)srcH * (size_t)srcDesc.Stride); + try { + auto ib = ref new Windows::Storage::Streams::Buffer((unsigned int)srcBufData.size()); + src->CopyToBuffer(ib); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ib); + reader->ReadBytes(Platform::ArrayReference(srcBufData.data(), (unsigned int)srcBufData.size())); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmap: failed to read src buffer\n"); + return nullptr; + } + + std::vector dstTmp((size_t)height * (size_t)dstDesc.Stride); + for (unsigned int y = 0; y < height; ++y) { + unsigned int sy = (unsigned int)((uint64_t)y * srcH / height); + uint8_t* srcRow = srcBufData.data() + sy * srcDesc.Stride; + uint8_t* dstRow = dstTmp.data() + y * dstDesc.Stride; + for (unsigned int x = 0; x < width; ++x) { + unsigned int sx = (unsigned int)((uint64_t)x * srcW / width); + uint8_t* pSrc = srcRow + sx * 4; + uint8_t* pDst = dstRow + x * 4; + pDst[0] = pSrc[0]; pDst[1] = pSrc[1]; pDst[2] = pSrc[2]; pDst[3] = pSrc[3]; + } + } + + try { + auto writer = ref new Windows::Storage::Streams::DataWriter(); + writer->WriteBytes(Platform::ArrayReference(dstTmp.data(), (unsigned int)dstTmp.size())); + auto buf = writer->DetachBuffer(); + + dstRef = nullptr; dstBuf = nullptr; + outBmp->CopyFromBuffer(buf); + return outBmp; + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmap: failed to write dst buffer\n"); + return nullptr; + } + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmap: exception during resize\n"); + return nullptr; + } +} + +SoftwareBitmap^ ImageHelpers::ResizeSoftwareBitmapUniformToFill(SoftwareBitmap^ src, unsigned int width, unsigned int height) { + if (src == nullptr) return nullptr; + try { + unsigned int srcW = src->PixelWidth; + unsigned int srcH = src->PixelHeight; + if (srcW == 0 || srcH == 0) return nullptr; + double srcAspect = (double)srcW / (double)srcH; + double dstAspect = (double)width / (double)height; + + unsigned int cropX = 0, cropY = 0, cropW = srcW, cropH = srcH; + if (srcAspect > dstAspect) { + + cropH = srcH; + cropW = (unsigned int)std::ceil(dstAspect * (double)cropH); + if (cropW > srcW) cropW = srcW; + cropX = (srcW - cropW) / 2; + cropY = 0; + } else if (srcAspect < dstAspect) { + + cropW = srcW; + cropH = (unsigned int)std::ceil((double)cropW / dstAspect); + if (cropH > srcH) cropH = srcH; + cropY = (srcH - cropH) / 2; + cropX = 0; + } else { + + } + + if (cropX == 0 && cropY == 0 && cropW == srcW && cropH == srcH) { + return ResizeSoftwareBitmap(src, width, height); + } + + auto cropped = ref new SoftwareBitmap(BitmapPixelFormat::Bgra8, cropW, cropH, BitmapAlphaMode::Premultiplied); + + auto srcBuf = src->LockBuffer(BitmapBufferAccessMode::Read); + auto srcDesc = srcBuf->GetPlaneDescription(0); + auto dstBuf = cropped->LockBuffer(BitmapBufferAccessMode::Write); + auto dstDesc = dstBuf->GetPlaneDescription(0); + + try { + + std::vector srcData((size_t)srcH * (size_t)srcDesc.Stride); + try { + auto ib = ref new Windows::Storage::Streams::Buffer((unsigned int)srcData.size()); + src->CopyToBuffer(ib); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ib); + reader->ReadBytes(Platform::ArrayReference(srcData.data(), (unsigned int)srcData.size())); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmapUniformToFill: failed to read src buffer\n"); + return nullptr; + } + + std::vector dstTmp((size_t)cropH * (size_t)dstDesc.Stride); + for (unsigned int y = 0; y < cropH; ++y) { + uint8_t* srcRow = srcData.data() + (size_t)(cropY + y) * srcDesc.Stride; + uint8_t* dstRow = dstTmp.data() + (size_t)y * dstDesc.Stride; + for (unsigned int x = 0; x < cropW; ++x) { + uint8_t* pSrc = srcRow + (size_t)(cropX + x) * 4; + uint8_t* pDst = dstRow + (size_t)x * 4; + pDst[0] = pSrc[0]; pDst[1] = pSrc[1]; pDst[2] = pSrc[2]; pDst[3] = pSrc[3]; + } + } + + try { + auto writer = ref new Windows::Storage::Streams::DataWriter(); + writer->WriteBytes(Platform::ArrayReference(dstTmp.data(), (unsigned int)dstTmp.size())); + auto buf = writer->DetachBuffer(); + dstBuf = nullptr; + cropped->CopyFromBuffer(buf); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmapUniformToFill: failed to write cropped buffer\n"); + return nullptr; + } + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmapUniformToFill: unexpected exception\n"); + return nullptr; + } + + return ResizeSoftwareBitmap(cropped, width, height); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "ResizeSoftwareBitmapUniformToFill: exception during operation\n"); + return nullptr; + } +} + +bool ImageHelpers::AdjustSaturation(SoftwareBitmap^ bmp, float saturation) { + if (bmp == nullptr) return false; + try { + auto b = EnsureBgra8Premultiplied(bmp); + if (b == nullptr) return false; + auto buf = b->LockBuffer(BitmapBufferAccessMode::ReadWrite); + auto desc = buf->GetPlaneDescription(0); + unsigned int w = b->PixelWidth; unsigned int h = b->PixelHeight; + + if (saturation == 1.0f) return true; + + const float satf = saturation; + + try { + auto ref = buf->CreateReference(); + Microsoft::WRL::ComPtr access; + IUnknown* unk = reinterpret_cast(ref); + BYTE* raw = nullptr; UINT32 cap = 0; + if (unk != nullptr && SUCCEEDED(unk->QueryInterface(IID_PPV_ARGS(&access))) + && SUCCEEDED(access->GetBuffer(&raw, &cap)) && raw != nullptr) { + for (unsigned int y = 0; y < h; ++y) { + uint8_t* row = raw + desc.StartIndex + (size_t)y * desc.Stride; + for (unsigned int x = 0; x < w; ++x) { + uint8_t b_px = row[x*4 + 0]; + uint8_t g_px = row[x*4 + 1]; + uint8_t r_px = row[x*4 + 2]; + uint8_t a_px = row[x*4 + 3]; + if (a_px == 0) continue; + + unsigned int invA = std::max(1, a_px); + float ur = (float)((uint32_t)r_px * 255u) / (float)invA; + float ug = (float)((uint32_t)g_px * 255u) / (float)invA; + float ub = (float)((uint32_t)b_px * 255u) / (float)invA; + + float gray = ur * 0.299f + ug * 0.587f + ub * 0.114f; + + float nrf = gray + satf * (ur - gray); + float ngf = gray + satf * (ug - gray); + float nbf = gray + satf * (ub - gray); + + int nri = (int)std::round(nrf); + int ngi = (int)std::round(ngf); + int nbi = (int)std::round(nbf); + nri = nri < 0 ? 0 : (nri > 255 ? 255 : nri); + ngi = ngi < 0 ? 0 : (ngi > 255 ? 255 : ngi); + nbi = nbi < 0 ? 0 : (nbi > 255 ? 255 : nbi); + + uint8_t pr = (uint8_t)((uint32_t)nri * a_px / 255u); + uint8_t pg = (uint8_t)((uint32_t)ngi * a_px / 255u); + uint8_t pb = (uint8_t)((uint32_t)nbi * a_px / 255u); + + row[x*4 + 0] = pb; row[x*4 + 1] = pg; row[x*4 + 2] = pr; row[x*4 + 3] = a_px; + } + } + return true; + } + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "AdjustSaturation: fast-path buffer access failed, falling back\n"); + } + + try { + buf = nullptr; + std::vector data((size_t)h * desc.Stride); + auto ib = ref new Windows::Storage::Streams::Buffer((unsigned int)data.size()); + b->CopyToBuffer(ib); + auto reader = Windows::Storage::Streams::DataReader::FromBuffer(ib); + reader->ReadBytes(Platform::ArrayReference(data.data(), (unsigned int)data.size())); + + for (unsigned int y = 0; y < h; ++y) { + uint8_t* row = data.data() + (size_t)y * desc.Stride; + for (unsigned int x = 0; x < w; ++x) { + uint8_t b_px = row[x*4 + 0]; + uint8_t g_px = row[x*4 + 1]; + uint8_t r_px = row[x*4 + 2]; + uint8_t a_px = row[x*4 + 3]; + if (a_px == 0) continue; + + unsigned int invA = std::max(1, a_px); + float ur = (float)((uint32_t)r_px * 255u) / (float)invA; + float ug = (float)((uint32_t)g_px * 255u) / (float)invA; + float ub = (float)((uint32_t)b_px * 255u) / (float)invA; + + float gray = ur * 0.299f + ug * 0.587f + ub * 0.114f; + + float nrf = gray + satf * (ur - gray); + float ngf = gray + satf * (ug - gray); + float nbf = gray + satf * (ub - gray); + + int nri = (int)std::round(nrf); + int ngi = (int)std::round(ngf); + int nbi = (int)std::round(nbf); + nri = nri < 0 ? 0 : (nri > 255 ? 255 : nri); + ngi = ngi < 0 ? 0 : (ngi > 255 ? 255 : ngi); + nbi = nbi < 0 ? 0 : (nbi > 255 ? 255 : nbi); + + uint8_t pr = (uint8_t)((uint32_t)nri * a_px / 255u); + uint8_t pg = (uint8_t)((uint32_t)ngi * a_px / 255u); + uint8_t pb = (uint8_t)((uint32_t)nbi * a_px / 255u); + + row[x*4 + 0] = pb; row[x*4 + 1] = pg; row[x*4 + 2] = pr; row[x*4 + 3] = a_px; + } + } + + auto writer = ref new Windows::Storage::Streams::DataWriter(); + writer->WriteBytes(Platform::ArrayReference(data.data(), (unsigned int)data.size())); + auto outBuf = writer->DetachBuffer(); + b->CopyFromBuffer(outBuf); + return true; + } catch(...) { return false; } + } catch(...) { return false; } +} + +concurrency::task ImageHelpers::CreateMaskedBlurredPngStreamAsync( + SoftwareBitmap^ src, + unsigned int targetW, + unsigned int targetH, + double dpi, + float blurDip) { + if (src == nullptr) return task_from_result(nullptr); + + try { + + unsigned int tW = targetW != 0 ? targetW : src->PixelWidth; + unsigned int tH = targetH != 0 ? targetH : src->PixelHeight; + + auto raster = ResizeSoftwareBitmapUniformToFill(src, tW, tH); + if (raster != nullptr) src = raster; + + unsigned int radiusPx = 0; + try { radiusPx = (unsigned int)std::round((double)blurDip * dpi / 96.0); } catch(...) { radiusPx = (unsigned int)std::round((double)blurDip); } + + SoftwareBitmap^ preBlurBitmap = src; + + try { + auto gpuResult = ::EffectsLibrary::GpuBoxBlurSoftwareBitmap(preBlurBitmap, (int)radiusPx, true); + if (gpuResult != nullptr) { + + try { AdjustSaturation(gpuResult, 1.0f); } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "CreateMaskedBlurredPngStreamAsync: AdjustSaturation on GPU result failed\n"); + } + return create_task(EncodeSoftwareBitmapToPngStreamAsync(gpuResult)).then([](IRandomAccessStream^ s) -> IRandomAccessStream^ { + if (s != nullptr) { + try { s->Seek(0); } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "CreateMaskedBlurredPngStreamAsync: failed to seek GPU-path stream\n"); + } + } + return s; + }); + } + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "CreateMaskedBlurredPngStreamAsync: GPU blur failed, falling back to CPU\n"); + } + + try { + auto cpuTarget = preBlurBitmap; + ::EffectsLibrary::BoxBlurSoftwareBitmap(cpuTarget, (int)radiusPx); + try { AdjustSaturation(cpuTarget, 1.0f); } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "CreateMaskedBlurredPngStreamAsync: AdjustSaturation on CPU result failed\n"); + } + return create_task(EncodeSoftwareBitmapToPngStreamAsync(cpuTarget)).then([](IRandomAccessStream^ s) -> IRandomAccessStream^ { + if (s != nullptr) { + try { s->Seek(0); } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Warning, "CreateMaskedBlurredPngStreamAsync: failed to seek CPU-path stream\n"); + } + } + return s; + }); + } catch(...) { + MLOG(moonlight_xbox_dx::Utils::LogLevel::Error, "CreateMaskedBlurredPngStreamAsync: CPU blur fallback also failed\n"); + } + + return task_from_result(nullptr); + } catch(...) { + return task_from_result(nullptr); + } +} diff --git a/UI/Utilities/ImageHelpers.h b/UI/Utilities/ImageHelpers.h new file mode 100644 index 00000000..058eb03e --- /dev/null +++ b/UI/Utilities/ImageHelpers.h @@ -0,0 +1,29 @@ + +#pragma once +#include "pch.h" +#include +#include +#include + +namespace ImageHelpers { + +concurrency::task LoadSoftwareBitmapFromUriOrPathAsync(Platform::String^ path); + +concurrency::task EncodeSoftwareBitmapToPngStreamAsync(Windows::Graphics::Imaging::SoftwareBitmap^ bitmap); + +Windows::Graphics::Imaging::SoftwareBitmap^ EnsureBgra8Premultiplied(Windows::Graphics::Imaging::SoftwareBitmap^ bitmap); + +Windows::Graphics::Imaging::SoftwareBitmap^ ResizeSoftwareBitmap(Windows::Graphics::Imaging::SoftwareBitmap^ src, unsigned int width, unsigned int height); + +Windows::Graphics::Imaging::SoftwareBitmap^ ResizeSoftwareBitmapUniformToFill(Windows::Graphics::Imaging::SoftwareBitmap^ src, unsigned int width, unsigned int height); + +bool AdjustSaturation(Windows::Graphics::Imaging::SoftwareBitmap^ bmp, float saturation); + +concurrency::task CreateMaskedBlurredPngStreamAsync( + Windows::Graphics::Imaging::SoftwareBitmap^ src, + unsigned int targetW, + unsigned int targetH, + double dpi, + float blurDip); + +} diff --git a/UI/Utilities/ToastService.cpp b/UI/Utilities/ToastService.cpp new file mode 100644 index 00000000..5a3ba38b --- /dev/null +++ b/UI/Utilities/ToastService.cpp @@ -0,0 +1,127 @@ +#include "pch.h" +#include "ToastService.h" +#include "XamlHelpers.h" + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Animation; + +static Border^ s_toastBorder = nullptr; +static TextBlock^ s_toastText = nullptr; +static Storyboard^ s_toastStoryboard = nullptr; +static AcrylicBrush^ s_toastBrush = nullptr; + +namespace moonlight_xbox_dx { + +void InitializeToastService(Grid^ rootGrid) { + s_toastText = ref new TextBlock(); + s_toastText->Foreground = ref new SolidColorBrush(Windows::UI::Colors::White); + s_toastText->FontSize = 12; + + s_toastBorder = ref new Border(); + s_toastBorder->HorizontalAlignment = HorizontalAlignment::Right; + s_toastBorder->VerticalAlignment = VerticalAlignment::Bottom; + s_toastBorder->Margin = Thickness{ 0, 0, 0, 48 }; + s_toastBorder->Padding = Thickness{ 16, 10, 48, 10 }; + s_toastBorder->CornerRadius = CornerRadius{ 8, 0, 0, 8 }; + s_toastBorder->Visibility = Visibility::Collapsed; + s_toastBorder->IsHitTestVisible = false; + s_toastBorder->Child = s_toastText; + + s_toastBrush = ref new AcrylicBrush(); + s_toastBrush->TintOpacity = 0.75; + s_toastBrush->TintLuminosityOpacity = 0.5; + s_toastBorder->Background = s_toastBrush; + + auto translate = ref new TranslateTransform(); + s_toastBorder->RenderTransform = translate; + + s_toastStoryboard = ref new Storyboard(); + + auto xAnim = ref new DoubleAnimationUsingKeyFrames(); + Storyboard::SetTarget(xAnim, translate); + Storyboard::SetTargetProperty(xAnim, "X"); + + auto kfX0 = ref new DiscreteDoubleKeyFrame(); + kfX0->KeyTime = KeyTime{ TimeSpan{ 0LL } }; + kfX0->Value = 400.0; + + auto kfX1 = ref new EasingDoubleKeyFrame(); + kfX1->KeyTime = KeyTime{ TimeSpan{ 3000000LL } }; + kfX1->Value = 0.0; + auto easeIn = ref new CubicEase(); + easeIn->EasingMode = EasingMode::EaseOut; + kfX1->EasingFunction = easeIn; + + auto kfX2 = ref new DiscreteDoubleKeyFrame(); + kfX2->KeyTime = KeyTime{ TimeSpan{ 23000000LL } }; + kfX2->Value = 0.0; + + auto kfX3 = ref new EasingDoubleKeyFrame(); + kfX3->KeyTime = KeyTime{ TimeSpan{ 28000000LL } }; + kfX3->Value = 400.0; + auto easeOut = ref new CubicEase(); + easeOut->EasingMode = EasingMode::EaseIn; + kfX3->EasingFunction = easeOut; + + xAnim->KeyFrames->Append(kfX0); + xAnim->KeyFrames->Append(kfX1); + xAnim->KeyFrames->Append(kfX2); + xAnim->KeyFrames->Append(kfX3); + + auto opAnim = ref new DoubleAnimationUsingKeyFrames(); + Storyboard::SetTarget(opAnim, s_toastBorder); + Storyboard::SetTargetProperty(opAnim, "Opacity"); + + auto kfOp0 = ref new DiscreteDoubleKeyFrame(); + kfOp0->KeyTime = KeyTime{ TimeSpan{ 0LL } }; + kfOp0->Value = 0.0; + + auto kfOp1 = ref new EasingDoubleKeyFrame(); + kfOp1->KeyTime = KeyTime{ TimeSpan{ 2500000LL } }; + kfOp1->Value = 1.0; + auto opEaseIn = ref new CubicEase(); + opEaseIn->EasingMode = EasingMode::EaseOut; + kfOp1->EasingFunction = opEaseIn; + + auto kfOp2 = ref new DiscreteDoubleKeyFrame(); + kfOp2->KeyTime = KeyTime{ TimeSpan{ 23000000LL } }; + kfOp2->Value = 1.0; + + auto kfOp3 = ref new EasingDoubleKeyFrame(); + kfOp3->KeyTime = KeyTime{ TimeSpan{ 28000000LL } }; + kfOp3->Value = 0.0; + auto opEaseOut = ref new CubicEase(); + opEaseOut->EasingMode = EasingMode::EaseIn; + kfOp3->EasingFunction = opEaseOut; + + opAnim->KeyFrames->Append(kfOp0); + opAnim->KeyFrames->Append(kfOp1); + opAnim->KeyFrames->Append(kfOp2); + opAnim->KeyFrames->Append(kfOp3); + + s_toastStoryboard->Children->Append(xAnim); + s_toastStoryboard->Children->Append(opAnim); + + s_toastStoryboard->Completed += ref new EventHandler([](Object^, Object^) { + s_toastBorder->Visibility = Visibility::Collapsed; + }); + + rootGrid->Children->Append(s_toastBorder); +} + +void ShowToast(Platform::String^ message) { + if (s_toastBorder == nullptr || s_toastText == nullptr || s_toastStoryboard == nullptr) + return; + if (s_toastBrush != nullptr) + s_toastBrush->TintColor = GetAppliedAccentColor(); + s_toastStoryboard->Stop(); + s_toastText->Text = message; + s_toastBorder->Visibility = Visibility::Visible; + s_toastStoryboard->Begin(); +} + +} diff --git a/UI/Utilities/ToastService.h b/UI/Utilities/ToastService.h new file mode 100644 index 00000000..5987a139 --- /dev/null +++ b/UI/Utilities/ToastService.h @@ -0,0 +1,9 @@ +#pragma once +#include "pch.h" + +namespace moonlight_xbox_dx { + +void InitializeToastService(Windows::UI::Xaml::Controls::Grid^ rootGrid); +void ShowToast(Platform::String^ message); + +} diff --git a/UI/Utilities/XamlHelpers.cpp b/UI/Utilities/XamlHelpers.cpp new file mode 100644 index 00000000..d4cd5094 --- /dev/null +++ b/UI/Utilities/XamlHelpers.cpp @@ -0,0 +1,253 @@ +#include "pch.h" +#include "XamlHelpers.h" +#define MLOG_TAG_OVERRIDE "XamlHelpers" +#include "Utils.hpp" +#include +#include +#include + +using namespace Windows::Foundation; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; + +namespace moonlight_xbox_dx { + +FrameworkElement^ FindChildByName(DependencyObject^ parent, Platform::String^ name) { + if (parent == nullptr) return nullptr; + int count = VisualTreeHelper::GetChildrenCount(parent); + for (int i = 0; i < count; ++i) { + auto child = VisualTreeHelper::GetChild(parent, i); + auto fe = dynamic_cast(child); + if (fe != nullptr && fe->Name == name) return fe; + auto rec = FindChildByName(child, name); + if (rec != nullptr) return rec; + } + return nullptr; +} + +ScrollViewer^ FindScrollViewer(DependencyObject^ parent) { + if (parent == nullptr) return nullptr; + int count = VisualTreeHelper::GetChildrenCount(parent); + for (int i = 0; i < count; ++i) { + auto child = VisualTreeHelper::GetChild(parent, i); + auto sv = dynamic_cast(child); + if (sv != nullptr) return sv; + auto rec = FindScrollViewer(child); + if (rec != nullptr) return rec; + } + return nullptr; +} + +static Windows::UI::Color s_appliedAccentColor = Windows::UI::Color{ 255, 0, 120, 215 }; + +Windows::UI::Color GetAppliedAccentColor() { + return s_appliedAccentColor; +} + +void ApplyAccentColor(Windows::UI::Color color) { + s_appliedAccentColor = color; + + wchar_t buf[16]; + swprintf_s(buf, L"#%02X%02X%02X%02X", color.A, color.R, color.G, color.B); + Windows::UI::Xaml::Interop::TypeName colorType; + colorType.Name = "Windows.UI.Color"; + colorType.Kind = Windows::UI::Xaml::Interop::TypeKind::Metadata; + auto boxed = Windows::UI::Xaml::Markup::XamlBindingHelper::ConvertValue( + colorType, ref new Platform::String(buf)); + + static const wchar_t* themeKeys[] = { L"Default", L"Dark", L"Light" }; + auto appThemes = Windows::UI::Xaml::Application::Current->Resources->ThemeDictionaries; + for (auto themeKey : themeKeys) { + auto key = ref new Platform::String(themeKey); + Windows::UI::Xaml::ResourceDictionary^ dict; + if (appThemes->HasKey(key)) { + dict = dynamic_cast(appThemes->Lookup(key)); + } else { + dict = ref new Windows::UI::Xaml::ResourceDictionary(); + appThemes->Insert(key, dict); + } + if (dict != nullptr) { + dict->Insert("SystemAccentColor", boxed); + + auto brush = ref new Windows::UI::Xaml::Media::SolidColorBrush(color); + dict->Insert("SystemControlHighlightAccentBrush", brush); + dict->Insert("SystemControlBackgroundAccentBrush", brush); + } + } +} + +SelectedContainerStatus ResolveSelectedContainer( + ListViewBase^ grid, ScrollViewer^& scrollViewer, ListViewItem^& container) +{ + container = nullptr; + if (grid == nullptr || grid->SelectedIndex < 0 || grid->SelectedItem == nullptr) + return SelectedContainerStatus::NoSelection; + + if (scrollViewer == nullptr) scrollViewer = FindScrollViewer(grid); + if (scrollViewer == nullptr) return SelectedContainerStatus::ScrollViewerMissing; + + auto item = grid->SelectedItem; + try { + container = dynamic_cast(grid->ContainerFromItem(item)); + } catch (...) {} + if (container == nullptr) { + try { + grid->ScrollIntoView(item); + grid->UpdateLayout(); + container = dynamic_cast(grid->ContainerFromItem(item)); + } catch (...) {} + } + if (container == nullptr) { + return SelectedContainerStatus::ContainerMissing; + } + + return SelectedContainerStatus::Ready; +} + +CenterContainerOutcome CenterContainerInScrollViewer( + ScrollViewer^ scrollViewer, UIElement^ container, bool vertical, bool immediate) +{ + if (scrollViewer == nullptr || container == nullptr) return CenterContainerOutcome::NotReady; + + auto fe = dynamic_cast(container); + double containerWidth = fe != nullptr ? fe->ActualWidth : 0.0; + double containerHeight = fe != nullptr ? fe->ActualHeight : 0.0; + if (containerWidth <= 0.0 || containerHeight <= 0.0) return CenterContainerOutcome::NotReady; + + double viewport = vertical ? scrollViewer->ViewportHeight : scrollViewer->ViewportWidth; + if (!std::isfinite(viewport) || viewport <= 0.0) return CenterContainerOutcome::NotReady; + + auto contentElement = dynamic_cast(scrollViewer->Content); + if (contentElement == nullptr) return CenterContainerOutcome::NotReady; + + Point origin; origin.X = 0.0f; origin.Y = 0.0f; + Point inContent; + try { + inContent = container->TransformToVisual(contentElement)->TransformPoint(origin); + } catch (...) { + return CenterContainerOutcome::NotReady; + } + + double itemCenter = vertical + ? (inContent.Y + (containerHeight * 0.5)) + : (inContent.X + (containerWidth * 0.5)); + + double scrollable = vertical ? scrollViewer->ScrollableHeight : scrollViewer->ScrollableWidth; + double current = vertical ? scrollViewer->VerticalOffset : scrollViewer->HorizontalOffset; + if (!std::isfinite(scrollable) || !std::isfinite(current)) return CenterContainerOutcome::NotReady; + + double target = itemCenter - (viewport * 0.5); + target = std::max(0.0, std::min(target, std::max(0.0, scrollable))); + + if (std::fabs(target - current) < 0.5) { + return CenterContainerOutcome::Applied; + } + + try { + if (vertical) scrollViewer->ChangeView(nullptr, target, nullptr, immediate); + else scrollViewer->ChangeView(target, nullptr, nullptr, immediate); + } catch (...) {} + + return CenterContainerOutcome::Applied; +} + +EdgeCenteringPaddingResult ApplyEdgeCenteringPadding(ListViewBase^ grid, double desiredPadding) +{ + if (grid == nullptr || !std::isfinite(desiredPadding)) return EdgeCenteringPaddingResult::Unchanged; + + auto padding = grid->Padding; + if (std::fabs(padding.Left - desiredPadding) < 0.5 && std::fabs(padding.Right - desiredPadding) < 0.5) + return EdgeCenteringPaddingResult::Unchanged; + + padding.Left = desiredPadding; + padding.Right = desiredPadding; + grid->Padding = padding; + return EdgeCenteringPaddingResult::Applied; +} + +Windows::Foundation::EventRegistrationToken ArmContainerRealizedWait(ListViewBase^ grid, std::function onFire) +{ + Windows::Foundation::EventRegistrationToken empty{ 0 }; + if (grid == nullptr) return empty; + auto selectedItem = grid->SelectedItem; + if (selectedItem == nullptr) return empty; + + auto tokenHolder = std::make_shared(); + tokenHolder->Value = 0; + *tokenHolder = grid->ContainerContentChanging += + ref new TypedEventHandler( + [grid, selectedItem, tokenHolder, onFire](ListViewBase^, ContainerContentChangingEventArgs^ args) { + if (args->Item != selectedItem) return; + try { grid->ContainerContentChanging -= *tokenHolder; } + catch (...) { MLOG(Utils::LogLevel::Error, "ArmContainerRealizedWait failed to unsubscribe"); } + tokenHolder->Value = 0; + onFire(); + }); + return *tokenHolder; +} + +void CancelContainerRealizedWait(ListViewBase^ grid, Windows::Foundation::EventRegistrationToken& token) +{ + if (grid == nullptr || token.Value == 0) return; + try { grid->ContainerContentChanging -= token; } + catch (...) { MLOG(Utils::LogLevel::Error, "CancelContainerRealizedWait failed to unsubscribe"); } + token.Value = 0; +} + +Windows::Foundation::EventRegistrationToken ArmLayoutUpdatedWait(ListViewBase^ grid, std::function onFire) +{ + Windows::Foundation::EventRegistrationToken empty{ 0 }; + if (grid == nullptr) return empty; + + auto tokenHolder = std::make_shared(); + tokenHolder->Value = 0; + *tokenHolder = grid->LayoutUpdated += + ref new EventHandler( + [grid, tokenHolder, onFire](Platform::Object^, Platform::Object^) { + try { grid->LayoutUpdated -= *tokenHolder; } + catch (...) { MLOG(Utils::LogLevel::Error, "ArmLayoutUpdatedWait failed to unsubscribe"); } + tokenHolder->Value = 0; + onFire(); + }); + return *tokenHolder; +} + +void CancelLayoutUpdatedWait(ListViewBase^ grid, Windows::Foundation::EventRegistrationToken& token) +{ + if (grid == nullptr || token.Value == 0) return; + try { grid->LayoutUpdated -= token; } + catch (...) { MLOG(Utils::LogLevel::Error, "CancelLayoutUpdatedWait failed to unsubscribe"); } + token.Value = 0; +} + +void AnimateCrossfadeText( + FrameworkElement^ visibilityAnchor, + Windows::UI::Xaml::Media::Animation::Storyboard^ showSb, + Windows::UI::Xaml::Media::Animation::Storyboard^ hideSb, + bool wantAnimate, + std::function applyNewText, + std::function isStillCurrent) +{ + bool alreadyVisible = visibilityAnchor != nullptr && visibilityAnchor->Opacity > 0.01; + if (wantAnimate && alreadyVisible && hideSb != nullptr && showSb != nullptr) { + auto token = std::make_shared(); + *token = hideSb->Completed += ref new EventHandler( + [hideSb, showSb, token, applyNewText, isStillCurrent](Platform::Object^, Platform::Object^) mutable { + try { hideSb->Completed -= *token; } + catch (...) { MLOG(Utils::LogLevel::Error, "AnimateCrossfadeText failed to unsubscribe hideSb.Completed"); } + if (!isStillCurrent()) return; + try { + applyNewText(); + showSb->Begin(); + } catch (...) { MLOG(Utils::LogLevel::Error, "AnimateCrossfadeText post-hide apply failed"); } + }); + hideSb->Begin(); + } else { + try { applyNewText(); } + catch (...) { MLOG(Utils::LogLevel::Error, "AnimateCrossfadeText direct apply failed"); } + if (showSb != nullptr) showSb->Begin(); + } +} + +} diff --git a/UI/Utilities/XamlHelpers.h b/UI/Utilities/XamlHelpers.h new file mode 100644 index 00000000..95d51233 --- /dev/null +++ b/UI/Utilities/XamlHelpers.h @@ -0,0 +1,72 @@ +#pragma once +#include "pch.h" +#include + +namespace moonlight_xbox_dx { + +Windows::UI::Xaml::Controls::ScrollViewer^ + FindScrollViewer(Windows::UI::Xaml::DependencyObject^ parent); + +Windows::UI::Xaml::FrameworkElement^ + FindChildByName(Windows::UI::Xaml::DependencyObject^ parent, Platform::String^ name); + +void ApplyAccentColor(Windows::UI::Color color); +Windows::UI::Color GetAppliedAccentColor(); + +enum class SelectedContainerStatus { + NoSelection, + ScrollViewerMissing, + ContainerMissing, + Ready, +}; + +SelectedContainerStatus ResolveSelectedContainer( + Windows::UI::Xaml::Controls::ListViewBase^ grid, + Windows::UI::Xaml::Controls::ScrollViewer^& scrollViewer, + Windows::UI::Xaml::Controls::ListViewItem^& container); + +enum class CenterContainerOutcome { + NotReady, + Applied, +}; + +CenterContainerOutcome CenterContainerInScrollViewer( + Windows::UI::Xaml::Controls::ScrollViewer^ scrollViewer, + Windows::UI::Xaml::UIElement^ container, + bool vertical, + bool immediate); + +enum class EdgeCenteringPaddingResult { + Unchanged, + Applied, +}; + +EdgeCenteringPaddingResult ApplyEdgeCenteringPadding( + Windows::UI::Xaml::Controls::ListViewBase^ grid, + double desiredPadding); + +Windows::Foundation::EventRegistrationToken ArmContainerRealizedWait( + Windows::UI::Xaml::Controls::ListViewBase^ grid, + std::function onFire); + +void CancelContainerRealizedWait( + Windows::UI::Xaml::Controls::ListViewBase^ grid, + Windows::Foundation::EventRegistrationToken& token); + +Windows::Foundation::EventRegistrationToken ArmLayoutUpdatedWait( + Windows::UI::Xaml::Controls::ListViewBase^ grid, + std::function onFire); + +void CancelLayoutUpdatedWait( + Windows::UI::Xaml::Controls::ListViewBase^ grid, + Windows::Foundation::EventRegistrationToken& token); + +void AnimateCrossfadeText( + Windows::UI::Xaml::FrameworkElement^ visibilityAnchor, + Windows::UI::Xaml::Media::Animation::Storyboard^ showSb, + Windows::UI::Xaml::Media::Animation::Storyboard^ hideSb, + bool wantAnimate, + std::function applyNewText, + std::function isStillCurrent); + +} diff --git a/Utils.cpp b/Utils.cpp index 365aa2a9..aec98e58 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -4,12 +4,18 @@ #include #include +#include +#include +#include +#include +#include #include #include #include using namespace std::chrono; constexpr auto LOG_LINES = 70; +constexpr size_t MAX_LOG_READ_BYTES = 512 * 1024; namespace moonlight_xbox_dx { namespace Utils { @@ -18,6 +24,117 @@ namespace moonlight_xbox_dx { bool showStats = false; std::mutex logMutex; + namespace { + HANDLE g_currentLogHandle = INVALID_HANDLE_VALUE; + std::wstring g_currentLogPath; + std::wstring g_lastLogPath; + + Platform::String^ ReadLogFileText(const std::wstring& path) { + if (path.empty()) { + return ref new Platform::String(L"(log unavailable)"); + } + HANDLE file = CreateFile2(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, nullptr); + if (file == INVALID_HANDLE_VALUE) { + return ref new Platform::String(L"(no log available)"); + } + LARGE_INTEGER size{}; + if (!GetFileSizeEx(file, &size) || size.QuadPart <= 0) { + CloseHandle(file); + return ref new Platform::String(L"(log is empty)"); + } + + bool truncated = false; + LARGE_INTEGER readSize = size; + if (static_cast(size.QuadPart) > MAX_LOG_READ_BYTES) { + truncated = true; + readSize.QuadPart = static_cast(MAX_LOG_READ_BYTES); + LARGE_INTEGER seekTo{}; + seekTo.QuadPart = size.QuadPart - readSize.QuadPart; + SetFilePointerEx(file, seekTo, nullptr, FILE_BEGIN); + } + + std::string buffer(static_cast(readSize.QuadPart), '\0'); + DWORD bytesRead = 0; + BOOL ok = ReadFile(file, buffer.data(), static_cast(buffer.size()), &bytesRead, nullptr); + CloseHandle(file); + if (!ok) { + return ref new Platform::String(L"(failed to read log)"); + } + buffer.resize(bytesRead); + + std::string result = truncated ? "(...truncated, showing tail...)\n" + buffer : buffer; + return StringFromStdString(result); + } + + void WriteCrashLine(const char* msg) { + if (g_currentLogHandle == INVALID_HANDLE_VALUE || msg == nullptr) return; + std::string line(msg); + line += '\n'; + DWORD written = 0; + WriteFile(g_currentLogHandle, line.data(), static_cast(line.size()), &written, nullptr); + FlushFileBuffers(g_currentLogHandle); + } + + const char* ExceptionCodeName(DWORD code) { + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: return "ACCESS_VIOLATION"; + case EXCEPTION_STACK_OVERFLOW: return "STACK_OVERFLOW"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "ILLEGAL_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "INT_DIVIDE_BY_ZERO"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_DATATYPE_MISALIGNMENT: return "DATATYPE_MISALIGNMENT"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_FLT_INVALID_OPERATION: return "FLT_INVALID_OPERATION"; + case EXCEPTION_PRIV_INSTRUCTION: return "PRIV_INSTRUCTION"; + case EXCEPTION_IN_PAGE_ERROR: return "IN_PAGE_ERROR"; + case EXCEPTION_BREAKPOINT: return "BREAKPOINT"; + default: return "UNKNOWN"; + } + } + + LONG WINAPI OnUnhandledSEHException(EXCEPTION_POINTERS* info) { + DWORD code = (info && info->ExceptionRecord) ? info->ExceptionRecord->ExceptionCode : 0; + void* addr = (info && info->ExceptionRecord) ? info->ExceptionRecord->ExceptionAddress : nullptr; + char buf[256]; + snprintf(buf, sizeof(buf), "*** CRASH: unhandled exception 0x%08lX (%s) at address 0x%p ***", + code, ExceptionCodeName(code), addr); + WriteCrashLine(buf); + return EXCEPTION_CONTINUE_SEARCH; + } + + void OnTerminate() { + try { + if (auto ex = std::current_exception()) { + std::rethrow_exception(ex); + } + WriteCrashLine("*** CRASH: std::terminate called (no active exception) ***"); + } catch (Platform::Exception^ ex) { + char buf[512]; + std::wstring msg = ex->Message != nullptr ? std::wstring(ex->Message->Data()) : L""; + snprintf(buf, sizeof(buf), "*** CRASH: unhandled Platform::Exception (HRESULT 0x%08X): %S ***", + ex->HResult, msg.c_str()); + WriteCrashLine(buf); + } catch (const std::exception& ex) { + char buf[512]; + snprintf(buf, sizeof(buf), "*** CRASH: unhandled C++ exception: %s ***", ex.what()); + WriteCrashLine(buf); + } catch (...) { + WriteCrashLine("*** CRASH: unhandled exception of unknown type ***"); + } + std::abort(); + } + + void OnPureCall() { + WriteCrashLine("*** CRASH: pure virtual function call ***"); + std::abort(); + } + + void OnInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, uintptr_t) { + WriteCrashLine("*** CRASH: invalid parameter passed to a CRT function ***"); + std::abort(); + } + } + Platform::String^ StringPrintf(const char* fmt, ...) { va_list args; va_start(args, fmt); @@ -56,16 +173,46 @@ namespace moonlight_xbox_dx { return std::wstring(buffer); } - void Log(const std::string_view& msg) { + const wchar_t* LogLevelTag(LogLevel level) { + switch (level) { + case LogLevel::Verbose: return L"VRB"; + case LogLevel::Debug: return L"DBG"; + case LogLevel::Info: return L"INF"; + case LogLevel::Warning: return L"WRN"; + case LogLevel::Error: return L"ERR"; + } + return L"INF"; + } + + LogLevel ParseLogLevelFromLine(const std::wstring& line) { + + if (line.find(L"[ERR]") != std::wstring::npos || line.find(L"{ERR}") != std::wstring::npos) return LogLevel::Error; + if (line.find(L"[WRN]") != std::wstring::npos || line.find(L"{WRN}") != std::wstring::npos) return LogLevel::Warning; + if (line.find(L"[INF]") != std::wstring::npos || line.find(L"{INF}") != std::wstring::npos) return LogLevel::Info; + if (line.find(L"[DBG]") != std::wstring::npos || line.find(L"{DBG}") != std::wstring::npos) return LogLevel::Debug; + if (line.find(L"[VRB]") != std::wstring::npos || line.find(L"{VRB}") != std::wstring::npos) return LogLevel::Verbose; + + return LogLevel::Error; + } + + void Log(LogLevel level, const std::string_view& msg) { try { - std::wstring string = GetCurrentTimestamp() + NarrowToWideString(msg); + std::wstring tag = std::wstring(L"[") + LogLevelTag(level) + L"] "; + std::wstring string = GetCurrentTimestamp() + tag + NarrowToWideString(msg); OutputDebugString(string.c_str()); { std::unique_lock lk(logMutex); + if (g_currentLogHandle != INVALID_HANDLE_VALUE) { + std::string line = WideToNarrowString(string); + if (line.empty() || line.back() != '\n') line += '\n'; + DWORD written = 0; + WriteFile(g_currentLogHandle, line.data(), static_cast(line.size()), &written, nullptr); + } if (logLines.size() == LOG_LINES) { logLines.erase(logLines.begin()); } - for (auto& ch : string) { + std::wstring displayString = string; + for (auto& ch : displayString) { // ModeSeven renders [ ] as left and right arrows, so we replace them // with { } which render as brackets if (ch == L'[') { @@ -75,7 +222,7 @@ namespace moonlight_xbox_dx { ch = L'}'; } } - logLines.push_back(string); + logLines.push_back(displayString); } } catch (...) { @@ -83,13 +230,13 @@ namespace moonlight_xbox_dx { } } - void Log(const char* msg) { + void Log(LogLevel level, const char* msg) { if (msg) { - Log(std::string_view(msg)); + Log(level, std::string_view(msg)); } } - void Logf(const char* format, ...) { + void Logf(LogLevel level, const char* format, ...) { va_list args; va_start(args, format); @@ -97,13 +244,89 @@ namespace moonlight_xbox_dx { std::vsnprintf(buf, sizeof(buf) - 1, format, args); va_end(args); - Log(std::string_view(buf)); + Log(level, std::string_view(buf)); + } + + std::string TagFromFunction(const char* function) { + if (!function) return std::string(); + std::string_view sv(function); + size_t lastSep = sv.rfind("::"); + if (lastSep == std::string_view::npos) { + return std::string(sv); + } + std::string_view withoutMethod = sv.substr(0, lastSep); + size_t prevSep = withoutMethod.rfind("::"); + std::string_view scope = (prevSep == std::string_view::npos) + ? withoutMethod + : withoutMethod.substr(prevSep + 2); + return std::string(scope); + } + + void LogTagged(LogLevel level, const std::string& tag, const char* msg) { + Log(level, "[" + tag + "] " + (msg ? msg : "")); + } + + void LogfTagged(LogLevel level, const std::string& tag, const char* format, ...) { + va_list args; + va_start(args, format); + + char buf[1024]; + std::vsnprintf(buf, sizeof(buf) - 1, format, args); + va_end(args); + + LogTagged(level, tag, buf); } std::vector GetLogLines() { return logLines; } + void InitFileLogging() { + try { + std::wstring folder(Windows::Storage::ApplicationData::Current->LocalFolder->Path->Data()); + + std::unique_lock lk(logMutex); + g_currentLogPath = folder + L"\\current_run.log"; + g_lastLogPath = folder + L"\\last_run.log"; + + DeleteFileW(g_lastLogPath.c_str()); + MoveFileExW(g_currentLogPath.c_str(), g_lastLogPath.c_str(), MOVEFILE_REPLACE_EXISTING); + + if (g_currentLogHandle != INVALID_HANDLE_VALUE) { + CloseHandle(g_currentLogHandle); + } + g_currentLogHandle = CreateFile2(g_currentLogPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS, nullptr); + } + catch (...) { + + } + } + + void InstallCrashHandlers() { + SetUnhandledExceptionFilter(&OnUnhandledSEHException); + std::set_terminate(&OnTerminate); + _set_purecall_handler(&OnPureCall); + _set_invalid_parameter_handler(&OnInvalidParameter); + } + + Platform::String^ ReadCurrentRunLogText() { + std::wstring path; + { + std::unique_lock lk(logMutex); + path = g_currentLogPath; + } + return ReadLogFileText(path); + } + + Platform::String^ ReadLastRunLogText() { + std::wstring path; + { + std::unique_lock lk(logMutex); + path = g_lastLogPath; + } + return ReadLogFileText(path); + } + Platform::String^ StringFromChars(const char* chars) { if (chars == nullptr) { @@ -159,5 +382,50 @@ namespace moonlight_xbox_dx { return result; } + + std::string Join(const std::vector& parts, const std::string& separator) { + std::string result; + for (size_t i = 0; i < parts.size(); i++) { + if (i > 0) result += separator; + result += parts[i]; + } + return result; + } + + double DurationStringToMs(Platform::String^ durationValue) { + if (durationValue == nullptr || durationValue->IsEmpty()) return 250.0; + + std::wstring text(durationValue->Data()); + std::wstringstream ss(text); + std::wstring segment; + std::vector parts; + + while (std::getline(ss, segment, L':')) { + if (segment.empty()) return 250.0; + try { + size_t idx = 0; + double value = std::stod(segment, &idx); + if (idx != segment.size()) return 250.0; + parts.push_back(value); + } catch (...) { + return 250.0; + } + } + + double totalSeconds = 0.0; + if (parts.size() == 3) { + totalSeconds = (parts[0] * 3600.0) + (parts[1] * 60.0) + parts[2]; + } else if (parts.size() == 2) { + totalSeconds = (parts[0] * 60.0) + parts[1]; + } else if (parts.size() == 1) { + totalSeconds = parts[0]; + } else { + return 250.0; + } + + if (!std::isfinite(totalSeconds) || totalSeconds <= 0.0) return 250.0; + return totalSeconds * 1000.0; + } + } } diff --git a/Utils.hpp b/Utils.hpp index 9f8f5c35..9224218f 100644 --- a/Utils.hpp +++ b/Utils.hpp @@ -3,6 +3,14 @@ namespace moonlight_xbox_dx { namespace Utils { + enum class LogLevel { + Verbose = 0, + Debug = 1, + Info = 2, + Warning = 3, + Error = 4 + }; + extern std::vector logLines; extern bool showLogs; extern bool showStats; @@ -10,14 +18,39 @@ namespace moonlight_xbox_dx { Platform::String^ StringPrintf(const char* fmt, ...); - void Log(const char* msg); - void Log(const std::string_view& msg); - void Logf(const char* msg, ...); + void Log(LogLevel level, const char* msg); + void Log(LogLevel level, const std::string_view& msg); + void Logf(LogLevel level, const char* msg, ...); + + std::string TagFromFunction(const char* function); + void LogTagged(LogLevel level, const std::string& tag, const char* msg); + void LogfTagged(LogLevel level, const std::string& tag, const char* format, ...); + + const wchar_t* LogLevelTag(LogLevel level); + LogLevel ParseLogLevelFromLine(const std::wstring& line); + + void InitFileLogging(); + void InstallCrashHandlers(); + Platform::String^ ReadCurrentRunLogText(); + Platform::String^ ReadLastRunLogText(); std::vector GetLogLines(); Platform::String^ StringFromChars(const char* chars); Platform::String^ StringFromStdString(std::string st); std::string PlatformStringToStdString(Platform::String^ input); std::string WideToNarrowString(const std::wstring_view& str); - std::wstring NarrowToWideString(const std::string_view& str); } + std::wstring NarrowToWideString(const std::string_view& str); + std::string Join(const std::vector& parts, const std::string& separator); + + double DurationStringToMs(Platform::String^ durationValue); + } } + +#ifdef MLOG_TAG_OVERRIDE +#define MLOG_TAG (MLOG_TAG_OVERRIDE) +#else +#define MLOG_TAG (moonlight_xbox_dx::Utils::TagFromFunction(__FUNCTION__)) +#endif + +#define MLOG(level, msg) moonlight_xbox_dx::Utils::LogTagged((level), MLOG_TAG, (msg)) +#define MLOGF(level, fmt, ...) moonlight_xbox_dx::Utils::LogfTagged((level), MLOG_TAG, (fmt), ##__VA_ARGS__) diff --git a/Utils/FloatBuffer.cpp b/Utils/FloatBuffer.cpp index b5c6239b..10268fcd 100644 --- a/Utils/FloatBuffer.cpp +++ b/Utils/FloatBuffer.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "FloatBuffer.h" +#define MLOG_TAG_OVERRIDE "FloatBuffer" #include "../Utils.hpp" #include @@ -155,7 +156,7 @@ void FloatBuffer::dump() const noexcept std::lock_guard lock(mtx_); if (count_ == 0) { - Utils::Logf("[FloatBuffer empty]\n"); + MLOGF(Utils::LogLevel::Debug, "[FloatBuffer empty]\n"); return; } @@ -177,5 +178,5 @@ void FloatBuffer::dump() const noexcept oss << ',' << buffer_[i]; } - Utils::Logf("%s\n", oss.str().c_str()); + MLOGF(Utils::LogLevel::Debug, "%s\n", oss.str().c_str()); } diff --git a/libgamestream/client.c b/libgamestream/client.c index 93808fa9..5dcb3843 100644 --- a/libgamestream/client.c +++ b/libgamestream/client.c @@ -33,6 +33,7 @@ #include #ifdef _WIN32 #define PATH_MAX 4096 +#include #include "winrt.h" #else #include @@ -834,16 +835,32 @@ int gs_quit_app(PSERVER_DATA server) { return ret; } +static bool identityLoaded = false; +static SRWLOCK identityLock = SRWLOCK_INIT; + +static int ensure_identity_loaded(const char* keyDirectory, int log_level) { + int ret = GS_OK; + AcquireSRWLockExclusive(&identityLock); + if (!identityLoaded) { + if (load_unique_id(keyDirectory) != GS_OK) { + ret = GS_FAILED; + } else if (load_cert(keyDirectory)) { + ret = GS_FAILED; + } else { + http_init(keyDirectory, log_level); + identityLoaded = true; + } + } + ReleaseSRWLockExclusive(&identityLock); + return ret; +} + int gs_init(PSERVER_DATA server, char *address, unsigned short httpPort, const char *keyDirectory, int log_level, bool unsupported) { mkdirtree(keyDirectory); - if (load_unique_id(keyDirectory) != GS_OK) - return GS_FAILED; - if (load_cert(keyDirectory)) + if (ensure_identity_loaded(keyDirectory, log_level) != GS_OK) return GS_FAILED; - http_init(keyDirectory, log_level); - LiInitializeServerInformation(&server->serverInfo); server->serverInfo.address = address; server->unsupported = unsupported; @@ -854,21 +871,19 @@ int gs_init(PSERVER_DATA server, char *address, unsigned short httpPort, const c int gs_appasset(PSERVER_DATA server, const char *keyDirectory, int appId) { int ret = GS_OK; - char url[4096]; - char* result = NULL; + char url[4096]; snprintf(url, sizeof(url), "https://%s:%u/appasset?appid=%d&AssetType=2&AssetIdx=0", server->serverInfo.address, server->httpsPort, appId); char uniqueFilePath[PATH_MAX]; snprintf(uniqueFilePath, PATH_MAX, "%s%d.png", keyDirectory, appId); FILE* fd = fopen(uniqueFilePath, "wb"); CURL* curl = get_curl_handle(); - if ((ret = http_request_binary(curl, url, fd)) != GS_OK) - goto cleanup; + ret = http_request_binary(curl, url, fd); fclose(fd); + if (ret != GS_OK) + remove(uniqueFilePath); cleanup: - if (result != NULL) - free(result); http_cleanup(curl); return ret; } diff --git a/libgamestream/http.c b/libgamestream/http.c index c308b603..14c0b50a 100644 --- a/libgamestream/http.c +++ b/libgamestream/http.c @@ -182,6 +182,14 @@ int http_request_binary(CURL *curl, char* url, FILE *data) { gs_error = curl_easy_strerror(res); return GS_FAILED; } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 200) { + gs_error = "HTTP error response"; + return GS_FAILED; + } + return GS_OK; } diff --git a/moonlight-xbox-dx.vcxproj b/moonlight-xbox-dx.vcxproj index d3bb8bd6..6286b5eb 100644 --- a/moonlight-xbox-dx.vcxproj +++ b/moonlight-xbox-dx.vcxproj @@ -272,12 +272,23 @@ + + + + + + + + + + + @@ -328,70 +339,170 @@ App.xaml + + + - - - MoonlightWelcome.xaml + + UI\Controls\AspectRatioBox.xaml + + + UI\Controls\SwatchPicker.xaml + + + UI\Controls\LunarPhaseControl.xaml + + + UI\Controls\TabsLayout.xaml + + + UI\Controls\SlidingMenu.xaml + + + + + + UI\Pages\MoonlightWelcome.xaml KeyboardControl.xaml - - Common\ModalDialog.xaml - + + UI\Modals\AddHostDialog.xaml + + + UI\Modals\HostActionsDialog.xaml + + + UI\Modals\TestConnectionResultDialog.xaml + + + UI\Modals\AlertDialog.xaml + + + UI\Modals\PairDialog.xaml + + + UI\Modals\ConfirmDialog.xaml + + + UI\Modals\StreamErrorDialog.xaml + + + + - - Pages\HostSettingsPage.xaml + + UI\Pages\HostSettingsPage.xaml - - Pages\MoonlightSettings.xaml + + UI\Pages\MoonlightSettings.xaml - - Pages\AppPage.xaml + + UI\Pages\AppPage\AppPage.xaml + + + UI\Modals\AppActionsDialog.xaml - - Pages\HostSelectorPage.xaml + + UI\Pages\HostSelectorPage.xaml - - Pages\StreamPage.xaml + + UI\Pages\StreamPage.xaml + + + + + UI\Backgrounds\Particles\ParticleBackground.xaml + + + UI\Backgrounds\Particles\ParticleSettingsControl.xaml + + + UI\Backgrounds\Spheres\SpheresBackground.xaml + + + UI\Backgrounds\Spheres\SpheresSettingsControl.xaml + + + UI\Backgrounds\Streaks\StreaksBackground.xaml + + + UI\Backgrounds\Streaks\StreaksSettingsControl.xaml + + + UI\Backgrounds\SwipeReveal\SwipeRevealBackground.xaml + + + UI\Backgrounds\GlobeGrid\GlobeGridBackground.xaml + + + UI\Backgrounds\Orbs\OrbsBackground.xaml + + + UI\Backgrounds\Orbs\OrbsSettingsControl.xaml + + + UI\Backgrounds\Blobs\BlobsBackground.xaml + + + UI\Backgrounds\Blobs\BlobsSettingsControl.xaml + + + UI\Backgrounds\DynamicBackgroundHost.xaml + + App.xaml - + + UI\Controls\AspectRatioBox.xaml + + + UI\Controls\SwatchPicker.xaml + + + UI\Controls\LunarPhaseControl.xaml + + + UI\Controls\TabsLayout.xaml + + + @@ -479,11 +590,68 @@ KeyboardControl.xaml - - MoonlightWelcome.xaml + + UI\Pages\MoonlightWelcome.xaml + + + UI\Backgrounds\Particles\ParticleBackground.xaml - - Common\ModalDialog.xaml + + UI\Backgrounds\Particles\ParticleSettingsControl.xaml + + + UI\Backgrounds\Spheres\SpheresBackground.xaml + + + UI\Backgrounds\Spheres\SpheresSettingsControl.xaml + + + UI\Backgrounds\Streaks\StreaksBackground.xaml + + + UI\Backgrounds\Streaks\StreaksSettingsControl.xaml + + + UI\Backgrounds\SwipeReveal\SwipeRevealBackground.xaml + + + UI\Backgrounds\GlobeGrid\GlobeGridBackground.xaml + + + UI\Backgrounds\Orbs\OrbsBackground.xaml + + + UI\Backgrounds\Orbs\OrbsSettingsControl.xaml + + + UI\Backgrounds\Blobs\BlobsBackground.xaml + + + UI\Backgrounds\Blobs\BlobsSettingsControl.xaml + + + UI\Backgrounds\DynamicBackgroundHost.xaml + + + UI\Modals\AddHostDialog.xaml + + + UI\Modals\HostActionsDialog.xaml + + + UI\Modals\TestConnectionResultDialog.xaml + + + UI\Modals\AlertDialog.xaml + + + UI\Modals\PairDialog.xaml + + + UI\Modals\ConfirmDialog.xaml + + + UI\Modals\StreamErrorDialog.xaml @@ -491,28 +659,40 @@ - - + + + + + + - - Pages\HostSettingsPage.xaml + + UI\Pages\HostSettingsPage.xaml - - Pages\MoonlightSettings.xaml + + UI\Pages\MoonlightSettings.xaml - - Pages\AppPage.xaml + + UI\Pages\AppPage\AppPage.xaml + + + UI\Modals\AppActionsDialog.xaml + + + + UI\Controls\SlidingMenu.xaml - - Pages\HostSelectorPage.xaml + + + UI\Pages\HostSelectorPage.xaml - - Pages\StreamPage.xaml + + UI\Pages\StreamPage.xaml Create @@ -560,6 +740,7 @@ + @@ -618,11 +799,11 @@ true - + true Document - + true Document @@ -634,24 +815,58 @@ - + + + + + + + + + + + + + + + + + + Designer - + + + + + + Designer + + + + + + + + + + Designer + + Designer - + Designer - + + + Designer - + Designer - - diff --git a/moonlight-xbox-dx.vcxproj.filters b/moonlight-xbox-dx.vcxproj.filters index a1df5136..6fa027d8 100644 --- a/moonlight-xbox-dx.vcxproj.filters +++ b/moonlight-xbox-dx.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -6,11 +6,8 @@ svg;png - {3fbf8cfc-2833-4477-9705-aa2b53149e20} - dll;pdb - - {c71dcdb3-0f05-4f2e-b87d-d392921f9242} + dll;pdb {4FC737F1-C7A5-4376-A066-2A32D752A2FF} @@ -20,473 +17,952 @@ {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;ipp;xsd - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - {93995380-89BD-4b04-88EB-625FBE52EBFB} + {93995380-89BD-4b04-88EB-625FBE52EBFC} hlsl {c5fa3ea7-182b-41f7-91ef-1c48589370b7} + + {2aace108-c3a8-52cf-826b-8c9cdea480f8} + + + {b8f174b0-5e31-566d-9201-73fcfb6e5f37} + + + {0d2cc763-ad0c-5756-b153-b595e4c93cfa} + + + {f4bb19a3-bfc0-55d3-a4a9-30d4a6a0db1e} + + + {f34ed542-7efa-50f9-99ef-bc1852f15a37} + + + {efe1c19e-a719-51bc-85e3-3305867278a4} + + + {e6cb9d4e-bbb2-5dfa-8e47-e6b2609544bc} + + + {6930fa43-8b19-56bd-b5c9-56afd0d688e3} + + + {c72fd6cf-9169-5619-a367-2a04d22259f9} + + + {5704adc6-b65f-5cd2-a5d7-2e3b8800e124} + + + {889a1e1a-f914-5a13-9ccf-f5be1130e6ef} + + + {6bac72a9-5993-5866-9970-6ecd54286ecd} + + + {4dcf855c-eec4-571b-b638-9e2b296931ee} + + + {116f9d2d-c1c6-5b6c-878b-87eca5788f07} + + + {45ee189f-14af-5f42-a1a2-ddc9c842f88f} + + + {26cb9df7-7ce8-5aeb-8c04-872b0f6a4c7c} + + + {40ce30a4-9150-5a67-93e8-bf57dd8055b7} + + + {890be602-8fe3-55ca-91d5-4a3f3e359c6e} + + + {847bb4eb-d94e-52df-9a09-8820dde367b9} + + + {4cafd1ae-b0cd-5927-85a2-f819ef06180c} + + + {bd62e9e7-ae66-5165-a7e2-2e48e023afe5} + + + {8b742987-262d-5ecd-9d2b-2ca577cdb354} + + + {522e4255-b673-5a87-a302-e4b083e14599} + + + {280fdf19-7ced-5d3b-9fd7-658388d53b10} + + + {b48b8cb7-a2b9-5642-9186-ca7ab781ac9c} + + + {1e23cefe-35ac-57b4-87a8-0a5a13fcb316} + + + {b6666f08-8ced-5907-9bd7-d8266c1fca2a} + + + {29f95cb9-c5b4-5b22-ad2c-8b1282efe5a1} + + + {eae1029b-01a6-547e-9715-1f13dbbeaace} + + + {e7b6af5a-4e97-579b-b40b-70fc2ba709c3} + + + {291aa1fc-74e0-5aa0-9930-3bed8801c11a} + + + {1ea73efe-bdbb-5566-889b-d3b42ef30e85} + + + {62add804-f1de-56ed-8aca-a16851b8cb65} + + + {d8480659-c2cd-58e5-b866-1ec230594064} + - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - + + UI\Backgrounds\Particles + + + UI\Backgrounds\Particles + + + UI\Backgrounds\Spheres + + + UI\Backgrounds\Spheres + + + UI\Backgrounds\Streaks + + + UI\Backgrounds\Streaks + + + UI\Backgrounds\SwipeReveal + + + UI\Backgrounds\GlobeGrid + + + UI\Backgrounds\Orbs + + + UI\Backgrounds\Orbs + + + UI\Backgrounds\Blobs + + + UI\Backgrounds\Blobs + + + UI\Backgrounds + + + UI\Controls + + + UI\Controls + + + UI\Controls + + + UI\Controls + + + UI\Pages + + + + UI\Modals + + + UI\Pages\AppPage + + + UI\Pages\AppPage + + + UI\Pages\AppPage + + + UI\Pages\AppPage + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Pages + + + UI\Pages + + + UI\Pages + + + UI\Pages + + + UI\Controls + + + UI\Styles + + + UI\Styles + + + + Source Files - - Source Files + + Common - - Source Files + + UI\Controls - - Source Files + + UI\Controls - - Source Files + + UI\Controls - - Source Files + + UI\Controls - - Source Files + + UI\Converters - - Source Files + + UI\Converters - - Source Files - - - Source Files - - - Source Files - - - Source Files + + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard - Source Files + Keyboard + Keyboard + + Source Files - + Source Files + + UI\Pages + + + UI\Backgrounds\Particles + + + UI\Backgrounds\Particles + + + UI\Backgrounds\Spheres + + + UI\Backgrounds\Spheres + + + UI\Backgrounds\Streaks + + + UI\Backgrounds\Streaks + + + UI\Backgrounds\SwipeReveal + + + UI\Backgrounds\GlobeGrid + + + UI\Backgrounds\Orbs + + + UI\Backgrounds\Orbs + + + UI\Backgrounds\Blobs + + + UI\Backgrounds\Blobs + + + UI\Backgrounds + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + Plot + - Source Files + State + + + State + State + + + State + + + Common + + + UI\Utilities + + + UI\Utilities + + + UI\Utilities + + + UI\Utilities + + + Streaming + + + Streaming + + + Streaming + + + Streaming + + + Streaming + + + State + + + UI\Pages + + + UI\Pages + + + State + + + State + + + UI\Pages\AppPage + + + UI\Modals + + + UI\Pages\AppPage + + + UI\Controls + + + UI\Models + + + UI\Pages + + + UI\Pages + + Source Files - - ImGui + + Streaming - - ImGui + + State - - ImGui + + State - - ImGui + + Streaming + + + Streaming + + + Streaming - ImGui + third_party\imgui-uwp\backends - - ImGui + + third_party\imgui\backends - ImGui + third_party\imgui - - Source Files + + third_party\imgui - - Source Files + + third_party\imgui - - Source Files + + third_party\imgui - - Source Files + + third_party\imgui - + Source Files - - Source Files + + Utils + + + UI\Models\ViewModels - + Header Files - + + UI\Utilities + + + UI\Utilities + + + UI\Utilities + + + Common + + + Common + + + UI\Controls + + + UI\Controls + + + UI\Controls + + + UI\Controls + + + UI\Controls + + + UI\Models + + + UI\Converters + + + UI\Converters + + + UI\Pages + + Header Files + + Keyboard + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + UI\Modals + + + Plot + + + Plot + + + State + + + State + + + State + + + State + + + State + + + State + - Header Files + Common - Header Files + Common + + + UI\Utilities - Header Files + Common + + + Streaming + + + Streaming + + + Streaming + + + Streaming + + + Streaming - Header Files + Streaming - Header Files + Streaming - Header Files + Streaming - Header Files + State + + + UI\Pages + + + UI\Pages - Header Files + State - Header Files + State + + + UI\Pages\AppPage + + + UI\Modals + + + UI\Pages + + + UI\Pages Header Files - Header Files + Streaming - Header Files + State - Header Files + State + + + UI\Models - Header Files + Streaming - Header Files + Streaming - Header Files + Streaming Header Files - - Header Files + + UI\Backgrounds - - Header Files + + UI\Backgrounds - - Header Files + + UI\Backgrounds\Particles - - Header Files + + UI\Backgrounds\Particles - - Header Files + + UI\Backgrounds\Spheres - - Header Files + + UI\Backgrounds\Spheres - - Header Files + + UI\Backgrounds\Streaks - - Header Files + + UI\Backgrounds\Streaks - - Header Files + + UI\Backgrounds\SwipeReveal - - Header Files + + UI\Backgrounds\GlobeGrid - - Header Files + + UI\Backgrounds\Orbs - - Header Files + + UI\Backgrounds\Orbs - - Header Files + + UI\Backgrounds\Blobs - - Header Files + + UI\Backgrounds\Blobs + + + UI\Backgrounds + + + Utils + + + UI\Models\ViewModels + + Assets + Assets @@ -502,9 +978,39 @@ Assets + + Assets + Assets + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + Assets @@ -641,14 +1147,6 @@ Assets - - - - - - DLLs - - Assets @@ -656,6 +1154,12 @@ Assets + + Assets\Font + + + Assets\Font + Assets @@ -665,33 +1169,29 @@ Assets - - - - Assets Assets - - Assets + + Assets\Shader - - Assets + + Assets\Shader - - + + + + - - - - - - - - + + + + + vcpkg_installed\x64-uwp\bin + diff --git a/pch.h b/pch.h index fb755620..416212a0 100644 --- a/pch.h +++ b/pch.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,8 @@ #include #include #include "App.xaml.h" +#include "UI\Converters\BoolToVisibilityConverter.h" +#include "UI\Converters\BoolToTextConverter.h" #define IMGUI_USER_CONFIG "Common\imconfig.moonlight.h" #include @@ -106,7 +109,7 @@ static inline int64_t MsToQpc(double ms) { do { \ static std::once_flag CONCAT(_onceFlag_, __LINE__); \ std::call_once(CONCAT(_onceFlag_, __LINE__), [&] { \ - Utils::Logf(fmt, ##__VA_ARGS__); \ + Utils::Logf(Utils::LogLevel::Debug, fmt, ##__VA_ARGS__); \ }); \ } while (0) @@ -118,14 +121,14 @@ static inline int64_t MsToQpc(double ms) { #ifdef FRAME_QUEUE_VERBOSE #define FQLog(fmt, ...) \ - moonlight_xbox_dx::Utils::Logf("[%lu] " fmt, ::GetCurrentThreadId(), ##__VA_ARGS__) + moonlight_xbox_dx::Utils::Logf(moonlight_xbox_dx::Utils::LogLevel::Verbose, "[%lu] " fmt, ::GetCurrentThreadId(), ##__VA_ARGS__) #else # ifdef FRAME_QUEUE_VERBOSE_LIMITED #include static std::atomic g_fqlog_counter{0}; #define FQLog(fmt, ...) \ if (++g_fqlog_counter > 200 && g_fqlog_counter < 1000) \ - moonlight_xbox_dx::Utils::Logf("[%lu] " fmt, ::GetCurrentThreadId(), ##__VA_ARGS__) + moonlight_xbox_dx::Utils::Logf(moonlight_xbox_dx::Utils::LogLevel::Verbose, "[%lu] " fmt, ::GetCurrentThreadId(), ##__VA_ARGS__) # else #if defined(_MSC_VER) #define FQLog(...) __noop