Enable page-inits to return a Promise

Allows to return the page-completion-value instead of passing it to an
`Engine` function.

Closes: #7000
This commit is contained in:
phosit 2024-09-21 20:20:35 +02:00 committed by phosit
parent 5741f77c6e
commit d3bc5bc802
5 changed files with 107 additions and 50 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;
@ -27,5 +27,5 @@ async function init(data)
const closePromise = setButtonCaptionsAndVisibility(mbButton, captions, mbCancelHotkey, "mbButton");
distributeButtonsHorizontally(mbButton, captions);
Engine.PopGuiPage(await closePromise);
return closePromise;
}

View file

@ -124,9 +124,10 @@ JS::Value CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone i
if (m_PageStack.empty())
return JS::UndefinedValue();
CGUI& currentPage = *m_PageStack.back().gui;
// Make sure we unfocus anything on the current page.
m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
return m_PageStack.back().ReplacePromise(m_ScriptInterface);
currentPage.SendFocusMessage(GUIM_LOST_FOCUS);
return m_PageStack.back().ReplacePromise(*currentPage.GetScriptInterface());
}()};
// Push the page prior to loading its contents, because that may push
@ -137,26 +138,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 +168,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 +225,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 +235,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);
const ScriptRequest rq{scriptInterface};
receivingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx,
JS::NewPromiseObject(rq.cx, nullptr));
return JS::ObjectValue(**receivingPromise);
}
void CGUIManager::SGUIPage::ResolvePromise(Script::StructuredClone args)
std::optional<CGUIManager::SGUIPage::CloseResult> CGUIManager::SGUIPage::MaybeClose()
{
if (!callbackFunction)
return;
if (JS::GetPromiseState(*sendingPromise) == JS::PromiseState::Pending)
return std::nullopt;
// 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 CGUIManager::SGUIPage::CloseResult{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);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
JS::RootedObject recv(rq.cx, *std::exchange(receivingPromise, nullptr));
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)
@ -379,6 +397,20 @@ void CGUIManager::TickObjects()
p.gui->TickObjects();
m_ScriptContext.RunJobs();
while (!m_PageStack.empty())
{
const size_t stackSize{m_PageStack.size()};
const std::optional<SGUIPage::CloseResult> result{m_PageStack.back().MaybeClose()};
if (!result.has_value())
break;
ENSURE(m_PageStack.size() == stackSize);
m_PageStack.pop_back();
if (!m_PageStack.empty())
m_PageStack.back().Refocus(result.value());
m_ScriptContext.RunJobs();
}
}
void CGUIManager::Draw(CCanvas2D& canvas) const

View file

@ -25,6 +25,7 @@
#include "scriptinterface/StructuredClone.h"
#include <deque>
#include <optional>
#include <string>
#include <unordered_set>
@ -76,7 +77,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 +152,24 @@ private:
*/
JS::Value ReplacePromise(ScriptInterface& scriptInterface);
struct CloseResult
{
Script::StructuredClone arg;
bool rejected;
};
/**
* Execute the stored callback function with the given arguments.
* 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.
*/
void ResolvePromise(Script::StructuredClone args);
std::optional<CloseResult> MaybeClose();
/**
* 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);
std::wstring m_Name;
std::unordered_set<VfsPath> inputs; // for hotloading
@ -162,10 +177,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 settled this page wants to be closed. It
* settles with the page completion value.
*/
std::shared_ptr<JS::PersistentRootedObject> callbackFunction;
std::shared_ptr<JS::PersistentRootedObject> sendingPromise;
/**
* The parent 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

@ -203,6 +203,12 @@ public:
UnloadHotkeys();
}
static void CloseTopmostPage()
{
g_GUI->PopPage(JS::NullHandleValue);
g_GUI->TickObjects();
}
void test_PageRegainedFocusEvent()
{
// Load up a test page.
@ -221,7 +227,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.
@ -229,9 +235,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);
}
};