diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Object.xml b/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Object.xml
new file mode 100644
index 0000000000..6a868ccdf9
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Object.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Page.xml b/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Page.xml
new file mode 100644
index 0000000000..58c5fed05c
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Page.xml
@@ -0,0 +1,4 @@
+
+
+ OpenRequest/Continuation/Object.xml
+
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Script.js b/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Script.js
new file mode 100644
index 0000000000..d53fc37a40
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Continuation/Script.js
@@ -0,0 +1,4 @@
+async function init(arg)
+{
+ return arg + " Continuation";
+}
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Object.xml b/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Object.xml
new file mode 100644
index 0000000000..728205de9c
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Object.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Page.xml b/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Page.xml
new file mode 100644
index 0000000000..08441f3ae0
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Page.xml
@@ -0,0 +1,4 @@
+
+
+ OpenRequest/Entry/Object.xml
+
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Script.js b/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Script.js
new file mode 100644
index 0000000000..6eaaaa4910
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Entry/Script.js
@@ -0,0 +1,7 @@
+async function init()
+{
+ return { [Engine.openRequest]: {
+ "page": "OpenRequest/Continuation/Page.xml",
+ "argument": "Entry"
+ } };
+}
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Object.xml b/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Object.xml
new file mode 100644
index 0000000000..7679a2ff47
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Object.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Page.xml b/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Page.xml
new file mode 100644
index 0000000000..e1bbb869db
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Page.xml
@@ -0,0 +1,4 @@
+
+
+ OpenRequest/Root/Object.xml
+
diff --git a/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Script.js b/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Script.js
new file mode 100644
index 0000000000..2264e4e1d3
--- /dev/null
+++ b/binaries/data/mods/_test.gui/gui/OpenRequest/Root/Script.js
@@ -0,0 +1,13 @@
+async function init()
+{
+ const result = await Engine.OpenChildPage("OpenRequest/Entry/Page.xml");
+
+ await new Promise(closePageCallback =>
+ {
+ globalThis.closePageCallback = () =>
+ {
+ closePageCallback();
+ return result;
+ };
+ });
+}
diff --git a/source/gui/GUIManager.cpp b/source/gui/GUIManager.cpp
index 73e6d948f1..965c2b5b9b 100644
--- a/source/gui/GUIManager.cpp
+++ b/source/gui/GUIManager.cpp
@@ -65,6 +65,7 @@ namespace
const CStr EVENT_NAME_GAME_LOAD_PROGRESS = "GameLoadProgress";
const CStr EVENT_NAME_WINDOW_RESIZED = "WindowResized";
constexpr const char* START_ATLAS{"startAtlas"};
+constexpr const char* OPEN_REQUEST{"openRequest"};
} // anonymous namespace
@@ -152,7 +153,7 @@ JS::Value CGUIManager::OpenChildPage(const CStrW& pageName, Script::StructuredCl
CGUI& currentPage = *m_PageStack.back().gui;
// Make sure we unfocus anything on the current page.
currentPage.SendFocusMessage(GUIM_LOST_FOCUS);
- return m_PageStack.back().ReplacePromise(*currentPage.GetScriptInterface());
+ return m_PageStack.back().GetPromise();
}()};
// Emplace the page prior to loading its contents, because that may open
@@ -185,11 +186,12 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
gui.reset(new CGUI(scriptContext));
const ScriptRequest rq{gui->GetScriptInterface()};
+ for (const char* name : {START_ATLAS, OPEN_REQUEST})
{
- JS::RootedString jsName{rq.cx, JS_NewStringCopyZ(rq.cx, START_ATLAS)};
+ JS::RootedString jsName{rq.cx, JS_NewStringCopyZ(rq.cx, name)};
JS::RootedValue symbol{rq.cx, JS::SymbolValue(JS::NewSymbol(rq.cx, jsName))};
JS::RootedValue nativeScope{rq.cx, JS::ObjectValue(*rq.nativeScope)};
- Script::SetProperty(rq, nativeScope, START_ATLAS, symbol, true);
+ Script::SetProperty(rq, nativeScope, name, symbol, true);
}
gui->AddObjectTypes();
@@ -267,16 +269,19 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
*localPromise = returnObject ? returnObject : JS::NewPromiseObject(rq.cx, nullptr);
}
-JS::Value CGUIManager::SGUIPage::ReplacePromise(ScriptInterface& scriptInterface)
+JS::Value CGUIManager::SGUIPage::GetPromise()
{
- const ScriptRequest rq{scriptInterface};
- receivingPromise = std::make_shared(rq.cx,
+ const ScriptRequest rq{gui->GetScriptInterface()};
+ if (receivingPromise == nullptr)
+ {
+ receivingPromise = std::make_shared(rq.cx,
JS::NewPromiseObject(rq.cx, nullptr));
+ }
return JS::ObjectValue(**receivingPromise);
}
-std::optional CGUIManager::SGUIPage::MaybeClose(const bool topmostPage)
+std::optional CGUIManager::SGUIPage::MaybeClose(const bool isRootPage)
{
if (JS::GetPromiseState(*sendingPromise) == JS::PromiseState::Pending)
return std::nullopt;
@@ -287,9 +292,29 @@ std::optional CGUIManager::SGUIPage::MaybeCl
const ScriptRequest rq{gui->GetScriptInterface()};
JS::RootedValue arg{rq.cx, JS::GetPromiseResult(*sendingPromise)};
const bool rejected{JS::GetPromiseState(*sendingPromise) == JS::PromiseState::Rejected};
- if (topmostPage)
+
+ JS::RootedValue nativeScope{rq.cx, JS::ObjectValue(*rq.nativeScope)};
+ if (arg.isObject())
+ {
+ JS::RootedObject argObject{rq.cx, &arg.toObject()};
+ JS::RootedValue symbol{rq.cx};
+ Script::GetProperty(rq, nativeScope, OPEN_REQUEST, &symbol);
+ JS::RootedId key{rq.cx, JS::PropertyKey::Symbol(symbol.toSymbol())};
+ JS::RootedValue openRequest{rq.cx};
+ if (JS_GetPropertyById(rq.cx, argObject, key, &openRequest) &&
+ Script::HasProperty(rq, openRequest, "page"))
+ {
+ std::wstring page;
+ Script::GetProperty(rq, openRequest, "page", page);
+ JS::RootedValue openArg{rq.cx};
+ Script::GetProperty(rq, openRequest, "argument", &openArg);
+ return CGUIManager::SGUIPage::OpenRequest{page,
+ Script::WriteStructuredClone(rq, openArg)};
+ }
+ }
+
+ if (isRootPage)
{
- JS::RootedValue nativeScope{rq.cx, JS::ObjectValue(*rq.nativeScope)};
JS::RootedValue symbol{rq.cx};
Script::GetProperty(rq, nativeScope, START_ATLAS, &symbol);
bool equals;
@@ -297,12 +322,12 @@ std::optional CGUIManager::SGUIPage::MaybeCl
throw std::runtime_error{"Error while comparing return value to a symbol."};
if (equals)
- return CGUIManager::SGUIPage::CloseResult{nullptr, rejected};
+ return CGUIManager::SGUIPage::Close{nullptr, rejected};
}
- return CGUIManager::SGUIPage::CloseResult{Script::WriteStructuredClone(rq, arg), rejected};
+ return CGUIManager::SGUIPage::Close{Script::WriteStructuredClone(rq, arg), rejected};
}
-void CGUIManager::SGUIPage::Refocus(const CloseResult& result)
+void CGUIManager::SGUIPage::Refocus(const Close& result)
{
ENSURE(receivingPromise);
@@ -432,10 +457,15 @@ std::optional CGUIManager::TickObjects()
break;
ENSURE(m_PageStack.size() == stackSize);
m_PageStack.pop_back();
- if (m_PageStack.empty())
- return !result.value().arg;
+ if (const SGUIPage::OpenRequest* request{std::get_if(&result.value())})
+ OpenChildPage(request->path, request->arg);
+ else if (const SGUIPage::Close& ret{std::get(result.value())};
+ m_PageStack.empty())
+ {
+ return !ret.arg;
+ }
else
- m_PageStack.back().Refocus(result.value());
+ m_PageStack.back().Refocus(ret);
m_ScriptContext.RunJobs();
}
diff --git a/source/gui/GUIManager.h b/source/gui/GUIManager.h
index d96af3738d..5e093529e3 100644
--- a/source/gui/GUIManager.h
+++ b/source/gui/GUIManager.h
@@ -34,6 +34,7 @@
#include
#include
#include
+#include
class CCanvas2D;
class CGUI;
@@ -155,29 +156,36 @@ private:
void LoadPage(ScriptContext& context);
/**
- * A new promise gets set. A reference to that promise is returned. The promise will settle when
- * the page is closed.
+ * A reference to the promise is returned. The promise will settle when the page is closed.
*/
- JS::Value ReplacePromise(ScriptInterface& scriptInterface);
+ JS::Value GetPromise();
- struct CloseResult
+ struct Close
{
Script::StructuredClone arg;
bool rejected;
};
+
+ struct OpenRequest
+ {
+ std::wstring path;
+ Script::StructuredClone arg;
+ };
+
+ using CloseResult = std::variant;
/**
* If the page should be closed this function closes the page and
* returns the result of the @c init function.
* If this page wasn't closed an empty optional is returned.
*/
- std::optional MaybeClose(const bool topmostPage);
+ std::optional MaybeClose(const bool isRootPage);
/**
* This function should be called when a child page got closed. The
* result of the closed page should be the argument of this
* function. This function resolves the @c receivingPromise.
*/
- void Refocus(const CloseResult& result);
+ void Refocus(const Close& result);
std::wstring m_Name;
std::unordered_set inputs; // for hotloading
diff --git a/source/gui/tests/test_GuiManager.h b/source/gui/tests/test_GuiManager.h
index 6e18183957..dcaccc86f5 100644
--- a/source/gui/tests/test_GuiManager.h
+++ b/source/gui/tests/test_GuiManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2025 Wildfire Games.
+/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -325,4 +325,21 @@ public:
TS_ASSERT_THROWS(g_GUI->OpenChildPage(L"await/page.xml",
Script::WriteStructuredClone(rq, JS::NullHandleValue)), const std::bad_variant_access&);
}
+
+ void test_OpenRequest()
+ {
+ const ScriptRequest rq{g_GUI->GetScriptInterface()};
+ g_GUI->OpenChildPage(L"OpenRequest/Root/Page.xml",
+ Script::WriteStructuredClone(rq, JS::UndefinedHandleValue));
+ TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
+
+ g_GUI->TickObjects();
+
+ const ScriptRequest pageRq{g_GUI->GetActiveGUI()->GetScriptInterface()};
+ JS::RootedValue global{pageRq.cx, pageRq.globalValue()};
+ std::string result;
+ TS_ASSERT(ScriptFunction::Call(pageRq, global, "closePageCallback", result));
+
+ TS_ASSERT_STR_EQUALS(result, "Entry Continuation");
+ }
};