Embed server browser as main menu modal

Refactors the browser UI into a reusable `CO_Browser.create()` module and adds embedded modal styling so MULTIPLAYER opens inside the main menu instead of swapping to a separate view. Updates main-menu JS/CSS to host, animate, and route input for the browser panel while keeping nav and prompts in sync. On the C++ side, PrismaUI now drives browser show/hide and event dispatch through the main menu view, and `ServerBrowserBridge::SetView()` was added so bridge events target the active host view.
This commit is contained in:
2026-07-07 17:35:15 +12:00
parent 2961eb6512
commit 9447bc308f
12 changed files with 1514 additions and 982 deletions
+1
View File
@@ -10,6 +10,7 @@ namespace F4T::ServerBrowser
{
public:
static void Initialize(PRISMA_UI_API::IVPrismaUI4* a_api, PrismaView a_view);
static void SetView(PrismaView a_view);
static void OnBrowserShown();
static void OnBrowserHidden();
static void HandleAction(const nlohmann::json& a_action);
+104 -27
View File
@@ -66,6 +66,8 @@ namespace F4T::PrismaUI
void CreateServerBrowserView();
void SyncBrowserHover();
void WatchMainMenuState();
[[nodiscard]] PrismaView GetActiveBrowserView();
void ActivateEmbeddedBrowser();
void InstallMainMenuCursorReleaseHook();
void RegisterMenuAdvanceSink();
void ReleaseMenuCursorConstraints();
@@ -473,7 +475,8 @@ namespace F4T::PrismaUI
void SyncBrowserHover()
{
if (!g_browserVisible || !g_browserDomReady || !g_api || !g_api->IsValid(g_serverBrowserView)) {
const auto activeView = GetActiveBrowserView();
if (!g_browserVisible || !g_api || activeView == 0 || !g_api->IsValid(activeView)) {
return;
}
@@ -493,12 +496,48 @@ namespace F4T::PrismaUI
::ScreenToClient(reinterpret_cast<HWND>(renderWindow->hwnd), &cursor);
const auto script = std::format(
"if(window.CO_Hover&&window.CO_Hover.updateFromPoint){{window.CO_Hover.updateFromPoint({},{});}}",
cursor.x,
cursor.y);
g_api->Invoke(g_serverBrowserView, script.c_str());
}
const auto script = std::format(
"if(window.CO_Hover&&window.CO_Hover.updateFromPoint){{window.CO_Hover.updateFromPoint({},{});}}",
cursor.x,
cursor.y);
g_api->Invoke(activeView, script.c_str());
}
PrismaView GetActiveBrowserView()
{
if (!g_api) {
return 0;
}
if (g_browserVisible && g_mainMenuVisible && g_mainMenuDomReady && g_api->IsValid(g_mainMenuView)) {
return g_mainMenuView;
}
if (g_browserVisible && g_browserDomReady && g_api->IsValid(g_serverBrowserView)) {
return g_serverBrowserView;
}
return 0;
}
void ActivateEmbeddedBrowser()
{
if (g_browserVisible) {
return;
}
g_browserVisible = true;
F4T::MenuInput::SetBrowserActive(true);
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
if (g_api && g_mainMenuDomReady && g_api->IsValid(g_mainMenuView)) {
F4T::ServerBrowser::ServerBrowserBridge::SetView(g_mainMenuView);
}
InitializeBrowserPointerMode();
ApplyBrowserPointerMode(!g_menuPreferGamepad);
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
}
void DispatchMainMenuControllerInput(const char* a_button, const char* a_phase)
{
@@ -518,16 +557,24 @@ namespace F4T::PrismaUI
const bool wasVisible = g_browserVisible;
g_browserVisible = false;
F4T::MenuInput::SetBrowserActive(false);
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
if (!g_api || !g_api->IsValid(g_serverBrowserView)) {
return;
}
F4T::MainMenuInject::SetMainMenuInputSuppressed(g_mainMenuVisible);
if (wasVisible) {
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserHidden();
}
if (g_api && g_mainMenuVisible && g_mainMenuDomReady && g_api->IsValid(g_mainMenuView)) {
g_api->Invoke(
g_mainMenuView,
"if(window.closeBrowser){window.closeBrowser();}");
return;
}
if (!g_api || !g_api->IsValid(g_serverBrowserView)) {
RequestNativeCursorRestore();
return;
}
g_api->Unfocus(g_serverBrowserView);
g_api->Hide(g_serverBrowserView);
RequestNativeCursorRestore();
@@ -552,6 +599,12 @@ namespace F4T::PrismaUI
void HideMainMenuInternal()
{
if (g_browserVisible) {
g_browserVisible = false;
F4T::MenuInput::SetBrowserActive(false);
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserHidden();
}
const bool wasVisible = g_mainMenuVisible;
g_mainMenuVisible = false;
F4T::MenuInput::SetMainMenuActive(false);
@@ -595,6 +648,9 @@ namespace F4T::PrismaUI
F4T::MainMenu::MainMenuBridge::Initialize();
g_mainMenuBridgeReady = true;
F4T::ServerBrowser::ServerBrowserBridge::Initialize(g_api, a_view);
F4T::ServerBrowser::ServerBrowserBridge::PushUITheme();
g_api->BindUIEvent(a_view, "coAction", [](const char* a_json) {
if (!a_json) {
return;
@@ -603,14 +659,16 @@ namespace F4T::PrismaUI
try {
const auto action = nlohmann::json::parse(a_json);
const auto actionName = action.value("action", "");
if (actionName == "openView") {
const auto viewName = action.value("view", "");
if (viewName == "multiplayer") {
HideMainMenuInternal();
F4T::PrismaUI::ShowServerBrowser();
ActivateEmbeddedBrowser();
return;
}
} else if (actionName == "closeBrowser") {
HideServerBrowserInternal();
return;
} else if (actionName == "openGameSettings") {
LogInfo("openGameSettings requested from main menu.");
F4T::MainMenuVanillaSettings::OpenGameSettings();
@@ -619,11 +677,33 @@ namespace F4T::PrismaUI
LogInfo("quit requested from main menu (not yet implemented).");
return;
}
if (!actionName.empty()) {
F4T::ServerBrowser::ServerBrowserBridge::HandleAction(action);
}
} catch (const std::exception& ex) {
REX::WARN("{} [JS] Failed to parse coAction JSON: {}", GetLogPrefix(), ex.what());
}
});
g_api->BindUIEvent(a_view, "closeBrowser", [](const char*) {
HideServerBrowserInternal();
});
g_api->BindUIEvent(a_view, "connectServer", [](const char*) {
if (!F4T::Networking::IsConnectedToServer()) {
F4T::Networking::ConnectToLocalServer();
}
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
});
g_api->BindUIEvent(a_view, "disconnectServer", [](const char*) {
F4T::Networking::DisconnectFromLocalServer();
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
});
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
if (g_mainMenuPendingShow) {
ShowMainMenuInternal();
}
@@ -694,27 +774,24 @@ namespace F4T::PrismaUI
return;
}
if (!g_api->IsValid(g_serverBrowserView)) {
if (!g_api->IsValid(g_mainMenuView)) {
g_mainMenuPendingShow = true;
CreateServerBrowserView();
CreateMainMenuView();
return;
}
if (!g_browserDomReady) {
if (!g_mainMenuDomReady) {
g_mainMenuPendingShow = true;
return;
}
g_mainMenuPendingShow = false;
g_browserVisible = true;
F4T::MenuInput::SetBrowserActive(true);
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
InitializeBrowserPointerMode();
g_api->Show(g_serverBrowserView);
ApplyBrowserPointerMode(!g_menuPreferGamepad);
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
if (!g_mainMenuVisible) {
ShowMainMenuInternal();
}
ActivateEmbeddedBrowser();
g_api->Invoke(
g_serverBrowserView,
g_mainMenuView,
"if(window.onBrowserShown){window.onBrowserShown();}");
}
+5
View File
@@ -377,6 +377,11 @@ namespace F4T::ServerBrowser
g_data.SetAllServers(ServerBrowserData::CreateMockServers());
}
void ServerBrowserBridge::SetView(PrismaView a_view)
{
g_view = a_view;
}
void ServerBrowserBridge::OnBrowserShown()
{
PushUIThemeInternal();