forked from mirrors/0ad
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:
@@ -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(() => {});
|
||||
@@ -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>
|
||||
@@ -549,6 +549,9 @@
|
||||
<element name="script">
|
||||
<interleave>
|
||||
<text/>
|
||||
<optional>
|
||||
<attribute name="module"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="file"/>
|
||||
</optional>
|
||||
|
||||
+77
-7
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"};
|
||||
|
||||
@@ -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&);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user