Add Engine.openRequest

Pages can replace themselfe by another (continuation) page.
This commit is contained in:
phosit 2025-04-23 13:01:54 +02:00
parent 456e2a0b56
commit 6ead0d2f92
No known key found for this signature in database
GPG key ID: C9430B600671C268
12 changed files with 125 additions and 22 deletions

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/OpenRequest/Continuation/Script.js"/>
</objects>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>OpenRequest/Continuation/Object.xml</include>
</page>

View file

@ -0,0 +1,4 @@
async function init(arg)
{
return arg + " Continuation";
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/OpenRequest/Entry/Script.js"/>
</objects>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>OpenRequest/Entry/Object.xml</include>
</page>

View file

@ -0,0 +1,7 @@
async function init()
{
return { [Engine.openRequest]: {
"page": "OpenRequest/Continuation/Page.xml",
"argument": "Entry"
} };
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/OpenRequest/Root/Script.js"/>
</objects>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>OpenRequest/Root/Object.xml</include>
</page>

View file

@ -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;
};
});
}

View file

@ -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<JS::PersistentRootedObject>(rq.cx,
const ScriptRequest rq{gui->GetScriptInterface()};
if (receivingPromise == nullptr)
{
receivingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx,
JS::NewPromiseObject(rq.cx, nullptr));
}
return JS::ObjectValue(**receivingPromise);
}
std::optional<CGUIManager::SGUIPage::CloseResult> CGUIManager::SGUIPage::MaybeClose(const bool topmostPage)
std::optional<CGUIManager::SGUIPage::CloseResult> CGUIManager::SGUIPage::MaybeClose(const bool isRootPage)
{
if (JS::GetPromiseState(*sendingPromise) == JS::PromiseState::Pending)
return std::nullopt;
@ -287,9 +292,29 @@ std::optional<CGUIManager::SGUIPage::CloseResult> 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::CloseResult> 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<bool> 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<SGUIPage::OpenRequest>(&result.value())})
OpenChildPage(request->path, request->arg);
else if (const SGUIPage::Close& ret{std::get<SGUIPage::Close>(result.value())};
m_PageStack.empty())
{
return !ret.arg;
}
else
m_PageStack.back().Refocus(result.value());
m_PageStack.back().Refocus(ret);
m_ScriptContext.RunJobs();
}

View file

@ -34,6 +34,7 @@
#include <optional>
#include <string>
#include <unordered_set>
#include <variant>
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<Close, OpenRequest>;
/**
* 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<CloseResult> MaybeClose(const bool topmostPage);
std::optional<CloseResult> 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<VfsPath> inputs; // for hotloading

View file

@ -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");
}
};