1
0
forked from 0ad/0ad

Enable page-inits to return a Promise

This commit is contained in:
phosit 2024-09-21 20:20:35 +02:00
parent e89d93b95f
commit b28b2343d8
5 changed files with 91 additions and 44 deletions

View File

@ -2,7 +2,7 @@
* Currently limited to at most 3 buttons per message box.
* The convention is to have "cancel" appear first.
*/
async function init(data)
function init(data)
{
// Set title
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
@ -28,5 +28,5 @@ async function init(data)
const closePromise = setButtonCaptionsAndVisibitily(mbButton, captions, mbCancelHotkey, "mbButton");
distributeButtonsHorizontally(mbButton, captions);
Engine.PopGuiPage(await closePromise);
return closePromise;
}

View File

@ -137,26 +137,15 @@ JS::Value CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone i
return promise;
}
void CGUIManager::PopPage(Script::StructuredClone args)
void CGUIManager::PopPage(JS::HandleValue arg)
{
if (m_PageStack.size() < 2)
{
debug_warn(L"Tried to pop GUI page when there's < 2 in the stack");
return;
}
// Make sure we unfocus anything on the current page.
m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
m_PageStack.pop_back();
m_PageStack.back().ResolvePromise(args);
// We return to a page where some object might have been focused.
m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS);
SGUIPage& topmostPage{m_PageStack.back()};
const ScriptRequest rq{topmostPage.gui->GetScriptInterface()};
JS::ResolvePromise(rq.cx, *topmostPage.sendingPromise, arg);
}
CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const Script::StructuredClone initData)
: m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction()
: m_Name(pageName), initData(initData)
{
}
@ -178,6 +167,9 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
g_VideoMode.ResetCursor();
inputs.clear();
gui.reset(new CGUI(scriptContext));
const ScriptRequest rq{gui->GetScriptInterface()};
sendingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx,
JS::NewPromiseObject(rq.cx, nullptr));
gui->AddObjectTypes();
@ -232,9 +224,6 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
gui->LoadedXmlFiles();
std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue initDataVal(rq.cx);
JS::RootedValue hotloadDataVal(rq.cx);
JS::RootedValue global(rq.cx, rq.globalValue());
@ -245,40 +234,68 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
if (hotloadData)
Script::ReadStructuredClone(rq, hotloadData, &hotloadDataVal);
if (Script::HasProperty(rq, global, "init") &&
!ScriptFunction::CallVoid(rq, global, "init", initDataVal, hotloadDataVal))
if (!Script::HasProperty(rq, global, "init"))
return;
JS::RootedValue returnValue{rq.cx};
if (!ScriptFunction::Call(rq, global, "init", &returnValue, initDataVal, hotloadDataVal))
{
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name));
return;
}
if (!returnValue.isObject())
return;
JS::RootedObject returnObject{rq.cx, &returnValue.toObject()};
if (!JS::IsPromiseObject(returnObject))
return;
sendingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx, returnObject);
}
JS::Value CGUIManager::SGUIPage::ReplacePromise(ScriptInterface& scriptInterface)
{
JSContext* generalContext{scriptInterface.GetGeneralJSContext()};
callbackFunction = std::make_shared<JS::PersistentRootedObject>(generalContext,
JS::NewPromiseObject(generalContext, nullptr));
return JS::ObjectValue(**callbackFunction);
receivingPromise = std::make_shared<JS::PersistentRootedObject>(generalContext,
JS::NewPromiseObject(generalContext, nullptr));
return JS::ObjectValue(**receivingPromise);
}
void CGUIManager::SGUIPage::ResolvePromise(Script::StructuredClone args)
CGUIManager::SGUIPage::CloseResult CGUIManager::SGUIPage::Close()
{
if (!callbackFunction)
return;
// Make sure we unfocus anything on the current page.
gui->SendFocusMessage(GUIM_LOST_FOCUS);
const ScriptRequest rq{gui->GetScriptInterface()};
JS::RootedValue arg{rq.cx, JS::GetPromiseResult(*sendingPromise)};
return {Script::WriteStructuredClone(rq, arg),
JS::GetPromiseState(*sendingPromise) == JS::PromiseState::Rejected};
}
void CGUIManager::SGUIPage::Refocus(const CloseResult& result)
{
ENSURE(receivingPromise);
std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedObject globalObj(rq.cx, rq.glob);
JS::RootedObject funcVal(rq.cx, *callbackFunction);
JS::RootedObject recv(rq.cx, *receivingPromise);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
receivingPromise.reset();
JS::RootedValue argVal(rq.cx);
if (args)
Script::ReadStructuredClone(rq, args, &argVal);
Script::ReadStructuredClone(rq, result.arg, &argVal);
// This only resolves the promise, it doesn't call the continuation.
JS::ResolvePromise(rq.cx, funcVal, argVal);
(result.rejected ? JS::RejectPromise : JS::ResolvePromise)(rq.cx, recv, argVal);
// We return to a page where some object might have been focused.
gui->SendFocusMessage(GUIM_GOT_FOCUS);
}
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
@ -368,6 +385,19 @@ void CGUIManager::TickObjects()
{
PROFILE3("gui tick");
while (!m_PageStack.empty() &&
JS::GetPromiseState(*m_PageStack.back().sendingPromise) != JS::PromiseState::Pending)
{
const size_t stackSize{m_PageStack.size()};
const SGUIPage::CloseResult result{m_PageStack.back().Close()};
ENSURE(m_PageStack.size() == stackSize);
m_PageStack.pop_back();
if (!m_PageStack.empty())
m_PageStack.back().Refocus(result);
g_ScriptContext->RunJobs();
}
// We share the script context with everything else that runs in the same thread.
// This call makes sure we trigger GC regularly even if the simulation is not running.
m_ScriptContext.MaybeIncrementalGC(1.0f);

View File

@ -76,7 +76,7 @@ public:
* Unload the currently active GUI page, and make the previous page active.
* (There must be at least two pages when you call this.)
*/
void PopPage(Script::StructuredClone args);
void PopPage(JS::HandleValue arg);
/**
* Called when a file has been modified, to hotload changes.
@ -151,10 +151,17 @@ private:
*/
JS::Value ReplacePromise(ScriptInterface& scriptInterface);
struct CloseResult
{
Script::StructuredClone arg;
bool rejected;
};
CloseResult Close();
/**
* Execute the stored callback function with the given arguments.
*/
void ResolvePromise(Script::StructuredClone args);
void Refocus(const CloseResult& result);
std::wstring m_Name;
std::unordered_set<VfsPath> inputs; // for hotloading
@ -162,10 +169,16 @@ private:
std::shared_ptr<CGUI> gui; // the actual GUI page
/**
* Function executed by this parent GUI page when the child GUI page it pushed is popped.
* Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset.
* When this promise is setteled this page wants to be closed. It
* fullfils with the page completion value.
*/
std::shared_ptr<JS::PersistentRootedObject> callbackFunction;
std::shared_ptr<JS::PersistentRootedObject> sendingPromise;
/**
* The parrent page waits on this promise. It also gets the
* completion value through this promise.
*/
std::shared_ptr<JS::PersistentRootedObject> receivingPromise;
};
std::shared_ptr<CGUI> top() const;

View File

@ -51,7 +51,7 @@ void PopGuiPage(const ScriptRequest& rq, JS::HandleValue args)
return;
}
g_GUI->PopPage(Script::WriteStructuredClone(rq, args));
g_GUI->PopPage(args);
}
void SetCursor(const std::wstring& name)

View File

@ -202,6 +202,12 @@ public:
UnloadHotkeys();
}
static void CloseTopmostPage()
{
g_GUI->PopPage(JS::NullHandleValue);
g_GUI->TickObjects();
}
void test_PageRegainedFocusEvent()
{
// Load up a test page.
@ -220,7 +226,7 @@ public:
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
g_GUI->PopPage(data);
CloseTopmostPage();
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
// This page instantly pushes an empty page with a callback that pops another page again.
@ -228,9 +234,7 @@ public:
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 3);
// Pop the empty page
g_GUI->PopPage(data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
scriptInterface->GetContext().RunJobs();
CloseTopmostPage();
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
}
};