Page MenuHomeWildfire Games

Let GUI scripts create and delete Objects. [somewhat proof of concept]
Needs ReviewPublic

Authored by wraitii on Jun 8 2020, 8:50 PM.
This revision needs review, but there are no reviewers specified.

Details

Reviewers
None
Summary

This lets GUI scripts create and remove GUI Objects dynamically.
It leverages Template Literals and D2768.

My simple GUI mod works here but I haven't actually checked for all edge cases, and there's probably an opportunity to do more before merging this.

Test Plan

Check out the in game mod with mod=public mod=test.

Event Timeline

wraitii created this revision.Jun 8 2020, 8:50 PM
Vulcan added a comment.Jun 8 2020, 8:51 PM

Build failure - The Moirai have given mortals hearts that can endure.

Link to build: https://jenkins.wildfiregames.com/job/docker-differential/2379/display/redirect

Vulcan added a comment.Jun 8 2020, 8:51 PM

Build failure - The Moirai have given mortals hearts that can endure.

Link to build: https://jenkins.wildfiregames.com/job/vs2015-differential/1848/display/redirect

elexis added a subscriber: elexis.Jun 9 2020, 12:05 AM
  • Topic and possible syntax discussed with nani in PM on January 10th and Feb 1st 2020

(16:29:35) nani: let myButton = Engine.CreateGUIObject("button",{"text": "Ready", "onPress": () => warn("clicked"), "style": "ModernButton"} )
(16:30:18) nani: parentObject.attach(mybutton,[zindex])

  • nani said there was something about possible Z issues
  • The JS only syntax Engine.CreateGUIObject("button",{"text": "Ready", "onPress": () => warn("clicked"), "style": "ModernButton"} ) might not be feasible since not all object properties that can can be specified in XML can be specified in JS accessible gui object settings (for example COList columns can't be added or removed from JS yet, which would be important for the optional lobby columns).
  • let myObject = myParent.CreateGUIObject("MyObject.xml"); if the alternative is let myObject = myParent.CreateGUIObject("<my><xml/>...</my>");, thereby not specifying XML data in JS strings in JS files in JS code and set the one or two gui object settings such as object dynamic size and dynamic caption with JS statements. The disadvantage of that would be performance due to 2-3 JS statements instead of 1 to create a new GUI object. The advantage of having XML in XML files is that one can edit the XML files the way XML files were inteded to be. See also https://en.wikipedia.org/wiki/Separation_of_concerns. For example this would allow the GUI mod editors to be able to make UI style or string changes without having to dig through the JS code. (For the equal reason the JS code had been moved out of XML files as well.)

rP66 introduced a comment removed in rP23067

	/**
	 * Adds an object to the GUI's object database
	 * Private, since you can only add objects through
	 * XML files. Why? Because it enables the GUI to
	 * be much more encapsulated and safe.
	 *
	 * @throws	Rethrows PSERROR_GUI from IGUIObject::AddChild().
	 */
	void AddObject(IGUIObject* pObject);

finding that took me way too long, but I knew it was there. I didn't search for further clues on that comment, but perhaps there was a more elaborate argument given for not creating GUI objects from JS by gee in 2003 on the forums or meetinglogs.

Having every GUI object constructed when the GUI page loads certainly has determinable benefits, such as

  • having 100% of the XML input validating when opening the page, whereas adding and validating conditionally means that one has to do dynamic testing to validate all XML, and test coverage may be less than 100%.
  • Hypothetically it could be faster to create objects only once.
  • It could be inviting for devs to creating and deleting new objects all the time instead of reusing the same N existing hidden GUI objects.

But it seems that is probably dwarfed by the use case of JS mods not having to overwrite XML pages.
(One can change XML files to use directory includes to address that use case, but it will not scale to insertion of GUI objects at every GUI object, so the code would be required if one wanted to insert at arbitrary GUI objects.)

I didn't make any further investigation on the implementation or further catch22's, hidden gotchas or such things and can't speak on code quality.

source/gui/ObjectBases/IGUIObject.cpp
92

case inconsistency

source/gui/ObjectBases/IGUIObject.h
140

void removeFromDOM();

The phrase DOM does not appear anywhere in source/gui/.

Using existing terminology makes the code more cohesive and requires less guesswork by the reader what the author could refer to.

GetGUIObjectByName is the most commonly used word of the JS Interface, so CreateGUIObject and DeleteGUIObject or Delete for example might be less irritating or easier to understand, since it's closer to the phrasing used in the codebase and avoids the guesswork or potential codesearches and metadev searches what the DOM at pyrogenesis could be, which version of the DOM language is supported etc. before finding out that it's actually not the HTML DOM but something custom even if it is a object tree described by a markup file.

wraitii added a subscriber: nani.Jun 9 2020, 9:57 AM

(16:29:35) nani: let myButton = Engine.CreateGUIObject("button",{"text": "Ready", "onPress": () => warn("clicked"), "style": "ModernButton"} )
(16:30:18) nani: parentObject.attach(mybutton,[zindex])

The trouble with that API is that if the JS object is never attached, the C++ Object will be left dangling unless we were to do something fancy with the GC.
I also find it strictly inferior to inline XML.

  • nani said there was something about possible Z issues

@nani < input?

See also https://en.wikipedia.org/wiki/Separation_of_concerns.

See the funny thing about separation of concerns is that it deals with concerns. And if you were to look at the concern here, it's the same between XML files and a large part of our GUI JS -> presenting the GUI (what Martin Fowler calls the 'Screen State' here to get some real documentation). So, merging XML and JS makes perfect sense and improves readability.
In fact, there is a small precedent for it in the industry -> literally all the most popular JS GUI frameworks: React, AngularJS, Vue.

perhaps there was a more elaborate argument given for not creating GUI objects from JS by gee in 2003 on the forums or meetinglogs.

Well, you'll be happy to learn that I've checked, and there isn't. I did find something by the way, a post by Acumen from 2006: https://wildfiregames.com/forum/index.php?/topic/10012-gui-engine-approach/
This is in the staff forums, but I'll quote relevant bits here for posterity:

I've been meaning to post this for a couple of weeks. At a semi-recent meeting, we discussed the GUI and how its features are being used. Here's a couple of pertinent quotes:

"I'd agree that there are problems with the way the GUI is being used - it was designed to have static layouts in XML, with a bit of scripting on top for minor things, but it seems like almost all the layout is now being done dynamically in scripts."
"It seems like the main problem is that it's meant to do statically-placed controls defined in XML, where you specify the exact position of everything; but it's being used dynamically, laying out grids of dozens of buttons (but having to define the existence of each of those dozens of buttons explicitly in the XML) in the right places and allowing it to be changed without updating hundreds of coordinates by hand."

Acumen saw two "extremes" in the spectrum of GUI tech:

  1. Visual: Centre UI development around a WYSIWYG editor. You drag and drop controls onto a screen, select their properties, give them events. The developer never has to directly modify or view interface files, so the editor data is saved/loaded as static XML.
  1. Technical: We assume that all GUI scripting will be done by hand, and so maximise control at the script level. We drop the XML component and GUI is designed through extensible objects in JS. Objects can be created or destroyed and new properties can be added to them (eg an array of sprite coordinates). Styles become inherited classes. Essentially, the GUI engine exposes a pack of control classes and events, and everything else is handled at the script level.

We are currently far more towards 2) than 1), and the world has moved to 2) (HTML/CSS/JS is (2) personified). This is merely a nudge more in that direction, making the GUI more consistent. After all, we already do basically everything relevant in JS, and we use dirty tricks such as repeatable ([n]) objects in XML that are hidden/revealed at runtime.

Ykkrosh wrote an answer that's too long to quote, but basically saying that the GUI was working very well for very static content, not at all for very dynamic content (such as the selection panel's units buttons). He suggested using C++ to create components to do the heavy lifting for the latter and that was "probably sufficient". I think now, 15 years later, we can say that it really wasn't. The GameSetup screens are too dynamic for C++ to make much sense, and they would be much cleaned up by having JS create the layout since that's basically what it's doing already. Regarding the selection panel, I don't think we ever implemented the C++ component, JS has been quite fast enough.
See also COList and List for components where we did do the C++ heavy lifting, and it's complex code, hard to maintain, somewhat buggy, and still too slow anyways (D1781).

  • Hypothetically it could be faster to create objects only once.

This lets us do lazy loading on the other hand, which could mean faster starting times for some screens. Not that I think it's a very relevant concern.

  • It could be inviting for devs to creating and deleting new objects all the time instead of reusing the same N existing hidden GUI objects.

That's, thankfully, up to the developers, and it wouldn't pass the review process, of course.


The main concern the "old guard" had with this, from what I can tell, is performance. This does let people do rather stupid thing. But there's nothing preventing performance issues right now either, one in fact already has performance issues with the GUI (see D1781). There is also nothing preventing this in actual JS code in the browser, and yet the whole world has moved to making GUIs in the browser in JS interacting with the DOM. I don't find it a very striking argument.

To me anyways, the real advantage is merging the JS and the XML together. It's a vastly better experience for the developper.

elexis added a comment.Jun 9 2020, 2:12 PM

the real advantage is merging the JS and the XML together.

This may be true for XML that is 1 or 2 lines long and becomes false with increasing amounts of XML.
Consider the GUI modders who want to add a frame with a label, an image and two button, perhaps even a COList with columns. How is that an advantage not being able to construct that from an XML file?

Take your example

function init()
{
	Engine.GetGUIObjectByName("root").createChildFromXML(`
		<object name="MainPanel" type="image" style="ModernWindow">
			<object
				name="addButton"
				type="button"
				style="StoneButtonFancy"
				size="100%-250 100%-50 100%-20 100%-20"
				caption="Click me to add a button"/>
		</object>
	`);
	Engine.GetGUIObjectByName("addButton").onMouseLeftPress = () => {
		let obj = Engine.GetGUIObjectByName("MainPanel");
		let size = obj.getChildren().length;
		let pos = 50 * size;
		obj.createChildFromXML(`
			<object type="button" name="button_${size}"
				style="StoneButtonFancy" size="20 ${pos} 240 ${pos+30}"
				caption="Delete me #${size}"/>
		`);
		Engine.GetGUIObjectByName(`button_${size}`).onMouseLeftRelease = () => {
			Engine.GetGUIObjectByName(`button_${size}`).removeFromDOM();
			reSizeChildren(obj);
		};
	};
}

If someone wants to edit that XML in this codebase, they have to do it in JS instead of XML.
If someone wants to edit that XML from a mod, how is it more comfortable to overwrite the JS function and duplicate the surrounding code instead of editing the XML?
It's a small difference, but small differences accumulate when repeated.
Another example would be someone wanting to change/add/delete one property of all GUI objects. In one case they'd have to go through the XML files, in the other case through XML and JS files.

The longer the XML code to be inserted becomes, the more advantageous it becomes to be able to load it from an XML file.
What if there are 20+ objects, 100 lines of XML, is that still advantageous to be specified in one giant JS string?
Take this one for example:

<object name="playerAssignmentsPanel" type="image" sprite="ModernDarkBoxGold">
	<object size="0 6 100% 30">

		<object name="playerNameHeading" type="text" style="ModernLabelText" size="0 0 20%+5 100%">
			<translatableAttribute id="caption">Player Name</translatableAttribute>
		</object>

		<object name="playerColorHeading" type="text" style="ModernLabelText" size="20%+5 0 22%+45 100%">
			<translatableAttribute id="caption">Color</translatableAttribute>
		</object>

		<object name="playerPlacementHeading" type="text" style="ModernLabelText" size="22%+45 0 50%+35 100%">
			<translatableAttribute id="caption">Player Placement</translatableAttribute>
		</object>

		<object name="playerCivHeading" type="text" style="ModernLabelText" size="50%+69 0 85%-37 100%">
			<translatableAttribute id="caption">Civilization</translatableAttribute>
		</object>

		<object name="civInfoButton" type="button" style="IconButton" sprite="iconInfoGold" sprite_over="iconInfoWhite" size="85%-37 0 85%-21 16"/>
		<object name="civResetButton" type="button" style="IconButton" sprite="iconResetGold" sprite_over="iconResetWhite" size="85%-16 0 85% 16"/>

		<object name="playerTeamHeading" type="text" style="ModernLabelText" size="85%+5 0 100%-21 100%">
			<translatableAttribute id="caption">Team</translatableAttribute>
		</object>

		<object name="teamResetButton" type="button" style="IconButton" sprite="iconResetGold" sprite_over="iconResetWhite" size="100%-21 0 100%-5 16" />
	</object>

	<object size="1 36 100%-1 100%">
	<repeat count="8">
		<object name="playerFrame[n]" size="0 0 100% 32" hidden="true">

			<object name="playerBackgroundColor[n]" type="image"/>

			<object name="playerName[n]" type="text" style="ModernLabelText" size="0 2 22% 30"/>

			<object name="playerColor[n]" type="dropdown" style="ModernDropDown" size="22%+5 2 22%+33 30" sprite="" scrollbar="false" button_width="22" font="sans-stroke-14" tooltip_style="onscreenToolTip"/>

			<object name="playerAssignment[n]" type="dropdown" style="ModernDropDown" size="22%+37 2 50%+35 30" tooltip_style="onscreenToolTip"/>
			<object name="playerAssignmentText[n]" type="text" style="ModernLabelText" size="22%+37 0 50%+35 30"/>

			<object name="aiConfigButton[n]" type="button" style="StoneButton" size="50%+40 4 50%+64 28" tooltip_style="onscreenToolTip" font="sans-bold-stroke-12" sprite="ModernGear" sprite_over="ModernGearHover" sprite_pressed="ModernGearPressed"/>

			<object name="playerCiv[n]" type="dropdown" style="ModernDropDown" size="50%+69 2 85% 30" tooltip_style="onscreenToolTip" dropdown_size="424"/>
			<object name="playerCivText[n]" type="text" style="ModernLabelText" size="50%+65 0 85% 30"/>

			<object name="playerTeam[n]" type="dropdown" style="ModernDropDown" size="85%+5 2 100%-5 30" tooltip_style="onscreenToolTip"/>
			<object name="playerTeamText[n]" type="text" style="ModernLabelText" size="85%+5 0 100%-5 100%"/>

		</object>
	</repeat>
	</object>
</object>

For this one it would be quite obvious that it would be better to specify it in a separate XML file, correct?
It's not even so unlikely, because the playerFrame is repeated once per player, so it could be created or destroyed whenever the number of players changes.

The performance aspect might become relevant when I think about that, since there are quite many GUI objects that change hidden/shown state all the time in the gamesetup (cascadingly when using the mousewheel to scroll through settings).

If it's about the user, it's probably best to have both syntax available to chose from.

See the funny thing about separation of concerns is that it deals with concerns. And if you were to look at the concern here, it's the same between XML files and a large part of our GUI JS -> presenting the GUI (what Martin Fowler calls the 'Screen State' here to get some real documentation). So, merging XML and JS makes perfect sense and improves readability.
In fact, there is a small precedent for it in the industry -> literally all the most popular JS GUI frameworks: React, AngularJS, Vue.

The article you link distinguishes between record state, screen state and session state which doesn't have much to do with the issue at hand since we're not making a webinterface. I suppose the record would be the simulation and map data, the session data could be something like g_GameAttributes and screenstate could be the information whether the local client opened the mapbrowser for instance.

Then the article goes on to speak about the Model-View-Controller pattern.
View is the presentation, how the data from the model is shown. That's what XML is for, and it also requires JS to push the data from the model to the GUI objects.
The Controller and Model are the components that determine how the model is operated with which is the JS side.
For 0ad JS GUI the distinction between Model and Controller only happens in places where there are classes for a Model that never interact with the GUI except through Controller classes. There are few of such classes that are completely abstracted from the presentation and controller. For example the Game class in the lobby, or the Controls/ folder in the gamesetup. In many other cases the Model and Controller are one component.

But none of the MVC split is relating to the question whether it's better to specify XML in XML or XML in JS,
because you can have JS code in any of the three components.

The purpose of XML is to specify how the GUI is structured and styled (View/Presentation), the purpose of JS is how it should be operated with (Model/Controller) or styled (for example dynamic resizing or hiding of GUI objects that can't be done from XML).

It's possible to specify HTML, JS, CSS, PHP and MySQL all in the same file, doesn't mean that it's good practice.
JS is a programming language, XML is a markup language, by design it is meant to have no processing capacity to focus on and restrict to the document description use case.

wraitii added a comment.EditedJun 9 2020, 3:28 PM

If someone wants to edit that XML from a mod, how is it more comfortable to overwrite the JS function and duplicate the surrounding code instead of editing the XML?

This seems to be a limitation of our modding system, as it should be quite easy to overwrite a single function. That being said, nothing prevents writing a ghost XML element and copying that, though the API for that isn't implemented in this "somewhat proof of concept" patch.

Another example would be someone wanting to change/add/delete one property of all GUI objects. In one case they'd have to go through the XML files, in the other case through XML and JS files.

Or run their own JS to do it at runtime, which seems much easier?

What if there are 20+ objects, 100 lines of XML, is that still advantageous to be specified in one giant JS string?
...
For this one it would be quite obvious that it would be better to specify it in a separate XML file, correct?

I don't really find that obvious at all, being familiar with React. Inlining JS, for example, would be really easy if the template was specified in JS, whereas now we separate them.
For example one could design a "factory function" in JS that gets called in a loop to create all the slightly different buttons, instead of writing 40 lines of XML with plenty of repetition. Our interface becomes data. It seems to me you should see the appeal of this approach for the gamesetup for example, but maybe I can try and give a more realistic example.

If it's about the user, it's probably best to have both syntax available to chose from.

I'd agree. Right now only one syntax is available, this adds the second.

[...] doesn't have much to do with the issue at hand since we're not making a webinterface.

The only difference with the situation at hand is that the "record state" does not necessarily exist, but we do have a Screen State and a Session State, as you point out. That being said, I was mostly reusing the terms for clearer definitions, as the rest of the article is not extremely relevant.

View is the presentation, how the data from the model is shown. That's what XML is for, and it also requires JS to push the data from the model to the GUI objects.
because you can have JS code in any of the three components.

Indeed JS is used for the presentation itself.

The purpose of XML is to specify how the GUI is structured and styled (View/Presentation), the purpose of JS is how it should be operated with (Model/Controller) or styled (for example dynamic resizing or hiding of GUI objects that can't be done from XML).
JS is a programming language, XML is a markup language, by design it is meant to have no processing capacity to focus on and restrict to the document description use case.

I don't really know what you're arguing against here. JS is obviously a necessary part of our "presentation", you say so yourself, so it is necessary to interwtine it with XML. At this point, using a single file instead of two seems objectively better for readability and ease-of-creation.

nani added a comment.Thu, Jun 11, 10:14 PM
  • nani said there was something about possible Z issues

@nani < input?

Each object z_index is assigned when the xml is parsed, with the same order as the tree traversal at loading. Each object is assigned a z_index+10 value from the previous one to leave some "space" but once we start creating objects from js and attaching them to existing objects, the z_index is no longer "sorted", even in the case we have 9 z_index spaces between each original object that is limited and will be become impossible to know what or how to make some objects visible on top of others. From what I read, HTML DOM specification solves this by using the concep of "views" where all children of an object with a z_index explicitly specified are rendered from top down in the tree structure of that object branch and so,initially , you don't have any z_index and all objects are rendered with the same order as the traversal of the tree structure. So given 0ad now uses plain old z_index for each object, controlling overlapping objects created with js will become unmanageable unless you have some idea like the html dom view one.

So if we don't want to create this hypothetical z-order "mess" we have to consider how the js creation will blend with the existing xml we have.