Enable modules in the GUI

Doesn't actually change a GUI-page to use modules. Adopting a GUI-page
now only requires JS and XML changes.

Ref: #8081
This commit is contained in:
phosit
2025-03-09 21:47:03 +01:00
committed by phosit
parent b8348bec55
commit 77b1addb45
15 changed files with 173 additions and 43 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script module="gui/await/script.js"/>
</objects>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>await/object.xml</include>
</page>
@@ -0,0 +1 @@
await new Promise(() => {});
+3
View File
@@ -26,6 +26,9 @@
<element name="script">
<interleave>
<text/>
<optional>
<attribute name="module"/>
</optional>
<optional>
<attribute name="file"/>
</optional>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script module="gui/multiple_root-modules/script.js"/>
<script module="gui/multiple_root-modules/script.js"/>
</objects>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>multiple_root-modules/object.xml</include>
</page>
+3
View File
@@ -549,6 +549,9 @@
<element name="script">
<interleave>
<text/>
<optional>
<attribute name="module"/>
</optional>
<optional>
<attribute name="file"/>
</optional>
+77 -7
View File
@@ -26,8 +26,8 @@
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/ObjectTypes/CGUIDummyObject.h"
#include "gui/ObjectTypes/CTooltip.h"
#include "gui/Scripting/ScriptFunctions.h"
#include "gui/Scripting/JSInterface_GUIProxy.h"
#include "gui/Scripting/ScriptFunctions.h"
#include "i18n/L10n.h"
#include "lib/bits.h"
#include "lib/input.h"
@@ -44,6 +44,9 @@
#include "ps/Pyrogenesis.h"
#include "ps/VideoMode.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/Promises.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
@@ -77,7 +80,10 @@ CGUI::CGUI(ScriptContext& context)
m_InternalNameNumber(0),
m_MouseButtons(0)
{
m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "GUIPage", context);
m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "GUIPage", context,
[](const VfsPath& path){
return path.string8().find("gui/") == 0;
});
m_ScriptInterface->SetCallbackData(this);
GuiScriptingInit(*m_ScriptInterface);
@@ -277,12 +283,61 @@ InReaction CGUI::HandleEvent(const SDL_Event_* ev)
return ret;
}
void CGUI::TickObjects()
JS::Value CGUI::GetHotloadData(const ScriptRequest& rq)
{
JS::RootedValue oldNamespace{rq.cx, m_LoadModuleResult.has_value() ?
JS::ObjectValue(*m_LoadModuleResult->moduleNamespace) : rq.globalValue()};
JS::RootedValue hotloadDataVal(rq.cx);
ScriptFunction::Call(rq, oldNamespace, "getHotloadData", &hotloadDataVal);
return hotloadDataVal;
}
JSObject* CGUI::CallPageInit(const ScriptRequest& rq, Script::StructuredClone initData,
JS::HandleValue hotloadDataVal, const std::string_view scriptName)
{
JS::RootedValue initDataVal{rq.cx};
if (initData)
Script::ReadStructuredClone(rq, initData, &initDataVal);
JS::RootedValue newNamespace{rq.cx, m_LoadModuleResult.has_value() ?
JS::ObjectValue(*m_LoadModuleResult->moduleNamespace) : rq.globalValue()};
if (!Script::HasProperty(rq, newNamespace, "init"))
return nullptr;
JS::RootedValue returnValue{rq.cx};
if (!ScriptFunction::Call(rq, newNamespace, "init", &returnValue, initDataVal, hotloadDataVal))
{
LOGERROR("GUI page '%s': Failed to call init() function", scriptName);
return nullptr;
}
if (!returnValue.isObject())
return nullptr;
JS::RootedObject returnObject{rq.cx, &returnValue.toObject()};
if (!JS::IsPromiseObject(returnObject))
return nullptr;
return returnObject;
}
JSObject* CGUI::TickObjects(const ScriptRequest& rq, Script::StructuredClone initData,
const std::string_view scriptName)
{
JS::RootedObject sendingPromise{rq.cx};
if (m_LoadModuleResult.has_value() && m_LoadModuleResult->iterator->IsDone())
{
JS::RootedValue hotloadData{rq.cx, GetHotloadData(rq)};
m_LoadModuleResult->moduleNamespace = m_LoadModuleResult->iterator->Get();
++m_LoadModuleResult->iterator;
sendingPromise = CallPageInit(rq, initData, hotloadData, scriptName);
}
m_BaseObject->RecurseObject(&IGUIObject::IsHidden, &IGUIObject::DispatchDelayedSettingChanges);
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhostOrOutOfBoundaries, &IGUIObject::Tick);
SendEventToAll(EventNameTick);
m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
return sendingPromise;
}
void CGUI::SendEventToAll(const CStr& eventName)
@@ -925,10 +980,20 @@ void CGUI::Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObje
void CGUI::Xeromyces_ReadScript(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
{
// Check for a 'file' parameter
CStrW fileAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("file")).FromUTF8());
// If there is a "module" attribute, save it so it can be loaded at the end. It isn't saved to `Path`
// because modules automatically get reloaded.
const std::string moduleAttribute{element.GetAttributes().GetNamedItem(xmb.GetAttributeID("module"))};
if (!moduleAttribute.empty())
{
if (m_LoadModuleResult.has_value())
throw std::logic_error{"There can only be one root module per page."};
// If there is a file specified, open and execute it
const ScriptRequest rq{m_ScriptInterface};
m_LoadModuleResult.emplace(rq, moduleAttribute);
}
// If there is a "file" attribute, open and execute it
CStrW fileAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("file")).FromUTF8());
if (!fileAttr.empty())
{
if (!VfsPath(fileAttr).IsDirectory())
@@ -940,7 +1005,7 @@ void CGUI::Xeromyces_ReadScript(const XMBData& xmb, XMBElement element, std::uno
LOGERROR("GUI: Script path %s is not a file path", fileAttr.ToUTF8().c_str());
}
// If it has a directory attribute, read all JS files in that directory
// If there is a "directory" attribute, read all JS files in that directory
CStrW directoryAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("directory")).FromUTF8());
if (!directoryAttr.empty())
{
@@ -1299,3 +1364,8 @@ void CGUI::Xeromyces_ReadColor(const XMBData& xmb, XMBElement element)
else
LOGERROR("GUI: Unable to create custom color '%s'. Invalid color syntax.", name.c_str());
}
CGUI::ModuleArtifact::ModuleArtifact(const ScriptRequest& rq, VfsPath filename):
result{rq, std::move(filename)},
moduleNamespace{rq.cx}
{}
+29 -5
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2024 Wildfire Games.
/* Copyright (C) 2025 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -33,10 +33,13 @@
#include "maths/Size2D.h"
#include "maths/Vector2D.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/ModuleLoader.h"
#include "scriptinterface/StructuredClone.h"
#include "scriptinterface/ScriptForward.h"
#include <map>
#include <memory>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -74,11 +77,17 @@ public:
*/
void AddObjectTypes();
JS::Value GetHotloadData(const ScriptRequest& rq);
JSObject* CallPageInit(const ScriptRequest& rq, Script::StructuredClone initDataVal,
JS::HandleValue hotloadDataVal, const std::string_view scriptName);
/**
* Performs processing that should happen every frame
* (including sending the "Tick" event to scripts)
*/
void TickObjects();
JSObject* TickObjects(const ScriptRequest& rq, Script::StructuredClone initData,
const std::string_view scriptName);
/**
* Sends a specified script event to every object
@@ -370,7 +379,8 @@ private:
*
* @see LoadXmlFile()
*/
void Xeromyces_ReadRootObjects(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths);
void Xeromyces_ReadRootObjects(const XMBData& xmb, XMBElement element,
std::unordered_set<VfsPath>& Paths);
/**
* Reads in the root element \<sprites\> (the DOMElement).
@@ -429,7 +439,9 @@ private:
*
* @see LoadXmlFile()
*/
IGUIObject* Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObject* pParent, std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth);
IGUIObject* Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObject* pParent,
std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths,
u32 nesting_depth);
/**
* Reads in the element \<repeat\>, which repeats its child \<object\>s
@@ -437,7 +449,9 @@ private:
* 'var' enclosed in square brackets) in its descendants' names with "[0]",
* "[1]", etc.
*/
void Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObject* pParent, std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth);
void Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObject* pParent,
std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths,
u32 nesting_depth);
/**
* Reads in the element \<script\> (the XMBElement) and executes
@@ -691,6 +705,16 @@ private:
std::map<CStr, const SGUIIcon> m_Icons;
public:
struct ModuleArtifact
{
ModuleArtifact(const ScriptRequest& rq, VfsPath filename);
Script::ModuleLoader::Result result;
Script::ModuleLoader::Result::iterator iterator{result.begin()};
JS::PersistentRootedObject moduleNamespace;
};
std::optional<ModuleArtifact> m_LoadModuleResult;
/**
* Map from event names to object which listen to a given event.
*/
+20 -29
View File
@@ -156,10 +156,7 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
{
std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue global(rq.cx, rq.globalValue());
JS::RootedValue hotloadDataVal(rq.cx);
ScriptFunction::Call(rq, global, "getHotloadData", &hotloadDataVal);
JS::RootedValue hotloadDataVal(rq.cx, gui->GetHotloadData(rq));
hotloadData = Script::WriteStructuredClone(rq, hotloadDataVal);
}
@@ -167,8 +164,6 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
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));
{
JS::RootedString jsName{rq.cx, JS_NewStringCopyZ(rq.cx, START_ATLAS)};
@@ -197,6 +192,7 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
return;
}
VfsPath rootModule;
XERO_ITER_EL(root, node)
{
if (node.GetNodeName() != elmt_include)
@@ -229,34 +225,23 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
gui->LoadedXmlFiles();
JS::RootedValue initDataVal(rq.cx);
JS::RootedValue hotloadDataVal(rq.cx);
JS::RootedValue global(rq.cx, rq.globalValue());
scriptContext.RunJobs();
if (gui->m_LoadModuleResult.has_value())
{
gui->m_LoadModuleResult->moduleNamespace = gui->m_LoadModuleResult->iterator->Get();
++gui->m_LoadModuleResult->iterator;
}
if (initData)
Script::ReadStructuredClone(rq, initData, &initDataVal);
JS::RootedValue hotloadDataVal(rq.cx);
if (hotloadData)
Script::ReadStructuredClone(rq, hotloadData, &hotloadDataVal);
if (!Script::HasProperty(rq, global, "init"))
return;
JS::RootedObject returnObject{rq.cx, gui->CallPageInit(rq, initData, hotloadDataVal,
utf8_from_wstring(m_Name))};
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);
sendingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx,
returnObject ? returnObject : JS::NewPromiseObject(rq.cx, nullptr));
}
JS::Value CGUIManager::SGUIPage::ReplacePromise(ScriptInterface& scriptInterface)
@@ -407,7 +392,13 @@ std::optional<bool> CGUIManager::TickObjects()
const auto pageStack = GetCopyOfFrozenStack();
for (const SGUIPage& p : pageStack)
p.gui->TickObjects();
{
const ScriptRequest rq{p.gui->GetScriptInterface()};
JS::RootedObject newSendingPromise{rq.cx, p.gui->TickObjects(rq, p.initData,
utf8_from_wstring(p.m_Name))};
if (newSendingPromise)
(*p.sendingPromise) = newSendingPromise;
}
m_ScriptContext.RunJobs();
+1 -1
View File
@@ -141,7 +141,7 @@ private:
/**
* Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed.
*/
void LoadPage(ScriptContext& scriptContext);
void LoadPage(ScriptContext& context);
/**
* A new promise gets set. A reference to that promise is returned. The promise will settle when
+2
View File
@@ -330,6 +330,8 @@ public:
void test_regression_rP26522()
{
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST));
CGUI gui{*g_ScriptContext};
const CStrW font{L"sans-bold-13"};
+18
View File
@@ -296,4 +296,22 @@ public:
Script::WriteStructuredClone(rq, JS::TrueHandleValue));
TS_ASSERT(g_GUI->TickObjects().value());
}
void test_MultipleRootModules()
{
ScriptRequest rq{g_GUI->GetScriptInterface()};
TS_ASSERT_THROWS_EQUALS(g_GUI->OpenChildPage(
L"multiple_root-modules/page.xml",
Script::WriteStructuredClone(rq, JS::NullHandleValue)),
const std::logic_error& e, e.what(), "There can only be one root module per page.");
}
void test_Await()
{
ScriptRequest rq{g_GUI->GetScriptInterface()};
TS_ASSERT_THROWS(g_GUI->OpenChildPage(L"await/page.xml",
Script::WriteStructuredClone(rq, JS::NullHandleValue)), const std::bad_variant_access&);
}
};
+2 -1
View File
@@ -152,8 +152,9 @@ private:
class ModuleLoader::Result
{
class iterator;
public:
class iterator;
explicit Result(const ScriptRequest& rq, const VfsPath& modulePath);
Result(const Result&) = delete;
Result& operator=(const Result&) = delete;