Remove three deprecated OpenGL extensions.
[0ad.git] / source / gui / CGUI.cpp
blob96f646a57e2b577261459296a88f12efc1e3fd24
1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "CGUI.h"
22 #include "graphics/Canvas2D.h"
23 #include "gui/IGUIScrollBar.h"
24 #include "gui/ObjectBases/IGUIObject.h"
25 #include "gui/ObjectTypes/CGUIDummyObject.h"
26 #include "gui/ObjectTypes/CTooltip.h"
27 #include "gui/Scripting/ScriptFunctions.h"
28 #include "gui/Scripting/JSInterface_GUIProxy.h"
29 #include "i18n/L10n.h"
30 #include "lib/allocators/DynamicArena.h"
31 #include "lib/allocators/STLAllocators.h"
32 #include "lib/bits.h"
33 #include "lib/input.h"
34 #include "lib/sysdep/sysdep.h"
35 #include "lib/timer.h"
36 #include "lib/utf8.h"
37 #include "maths/Size2D.h"
38 #include "ps/CLogger.h"
39 #include "ps/Filesystem.h"
40 #include "ps/GameSetup/Config.h"
41 #include "ps/Globals.h"
42 #include "ps/Hotkey.h"
43 #include "ps/Profile.h"
44 #include "ps/Pyrogenesis.h"
45 #include "ps/VideoMode.h"
46 #include "ps/XML/Xeromyces.h"
47 #include "scriptinterface/ScriptContext.h"
48 #include "scriptinterface/ScriptInterface.h"
50 #include <string>
51 #include <unordered_map>
52 #include <unordered_set>
54 const double SELECT_DBLCLICK_RATE = 0.5;
55 const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion
57 const CStr CGUI::EventNameLoad = "Load";
58 const CStr CGUI::EventNameTick = "Tick";
59 const CStr CGUI::EventNamePress = "Press";
60 const CStr CGUI::EventNameKeyDown = "KeyDown";
61 const CStr CGUI::EventNameRelease = "Release";
62 const CStr CGUI::EventNameMouseRightPress = "MouseRightPress";
63 const CStr CGUI::EventNameMouseLeftPress = "MouseLeftPress";
64 const CStr CGUI::EventNameMouseWheelDown = "MouseWheelDown";
65 const CStr CGUI::EventNameMouseWheelUp = "MouseWheelUp";
66 const CStr CGUI::EventNameMouseLeftDoubleClick = "MouseLeftDoubleClick";
67 const CStr CGUI::EventNameMouseLeftRelease = "MouseLeftRelease";
68 const CStr CGUI::EventNameMouseRightDoubleClick = "MouseRightDoubleClick";
69 const CStr CGUI::EventNameMouseRightRelease = "MouseRightRelease";
71 namespace
74 struct VisibleObject
76 IGUIObject* object;
77 // Index of the object in a depth-first search inside GUI tree.
78 u32 index;
79 // Cached value of GetBufferedZ to avoid recursive calls in a deep hierarchy.
80 float bufferedZ;
83 template<class Container>
84 void CollectVisibleObjectsRecursively(const std::vector<IGUIObject*>& objects, Container* visibleObjects)
86 for (IGUIObject* const& object : objects)
88 if (!object->IsHidden())
90 visibleObjects->emplace_back(VisibleObject{object, static_cast<u32>(visibleObjects->size()), 0.0f});
91 CollectVisibleObjectsRecursively(object->GetChildren(), visibleObjects);
96 } // anonynous namespace
98 CGUI::CGUI(const std::shared_ptr<ScriptContext>& context)
99 : m_BaseObject(std::make_unique<CGUIDummyObject>(*this)),
100 m_FocusedObject(nullptr),
101 m_InternalNameNumber(0),
102 m_MouseButtons(0)
104 m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "GUIPage", context);
105 m_ScriptInterface->SetCallbackData(this);
107 GuiScriptingInit(*m_ScriptInterface);
108 m_ScriptInterface->LoadGlobalScripts();
111 CGUI::~CGUI()
113 for (const std::pair<const CStr, IGUIObject*>& p : m_pAllObjects)
114 delete p.second;
117 InReaction CGUI::HandleEvent(const SDL_Event_* ev)
119 InReaction ret = IN_PASS;
121 if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYUP)
123 const char* hotkey = static_cast<const char*>(ev->ev.user.data1);
125 const CStr& eventName = ev->ev.type == SDL_HOTKEYPRESS ? EventNamePress : ev->ev.type == SDL_HOTKEYDOWN ? EventNameKeyDown : EventNameRelease;
127 if (m_GlobalHotkeys.find(hotkey) != m_GlobalHotkeys.end() && m_GlobalHotkeys[hotkey].find(eventName) != m_GlobalHotkeys[hotkey].end())
129 ret = IN_HANDLED;
131 ScriptRequest rq(m_ScriptInterface);
132 JS::RootedObject globalObj(rq.cx, rq.glob);
133 JS::RootedValue result(rq.cx);
134 if (!JS_CallFunctionValue(rq.cx, globalObj, m_GlobalHotkeys[hotkey][eventName], JS::HandleValueArray::empty(), &result))
135 ScriptException::CatchPending(rq);
138 std::map<CStr, std::vector<IGUIObject*> >::iterator it = m_HotkeyObjects.find(hotkey);
139 if (it != m_HotkeyObjects.end())
140 for (IGUIObject* const& obj : it->second)
142 if (!obj->IsEnabled())
143 continue;
144 if (ev->ev.type == SDL_HOTKEYPRESS)
145 ret = obj->SendEvent(GUIM_PRESSED, EventNamePress);
146 else if (ev->ev.type == SDL_HOTKEYDOWN)
147 ret = obj->SendEvent(GUIM_KEYDOWN, EventNameKeyDown);
148 else
149 ret = obj->SendEvent(GUIM_RELEASED, EventNameRelease);
153 else if (ev->ev.type == SDL_MOUSEMOTION)
155 // Yes the mouse position is stored as float to avoid
156 // constant conversions when operating in a
157 // float-based environment.
158 m_MousePos = CVector2D((float)ev->ev.motion.x / g_VideoMode.GetScale(), (float)ev->ev.motion.y / g_VideoMode.GetScale());
160 SGUIMessage msg(GUIM_MOUSE_MOTION);
161 m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::HandleMessage, msg);
164 // Update m_MouseButtons. (BUTTONUP is handled later.)
165 else if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
167 switch (ev->ev.button.button)
169 case SDL_BUTTON_LEFT:
170 case SDL_BUTTON_RIGHT:
171 case SDL_BUTTON_MIDDLE:
172 m_MouseButtons |= Bit<unsigned int>(ev->ev.button.button);
173 break;
174 default:
175 break;
179 // Update m_MousePos (for delayed mouse button events)
180 CVector2D oldMousePos = m_MousePos;
181 if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
183 m_MousePos = CVector2D((float)ev->ev.button.x / g_VideoMode.GetScale(), (float)ev->ev.button.y / g_VideoMode.GetScale());
186 // Allow the focused object to pre-empt regular GUI events.
187 if (GetFocusedObject())
188 ret = GetFocusedObject()->PreemptEvent(ev);
190 // Only one object can be hovered
191 // pNearest will after this point at the hovered object, possibly nullptr
192 IGUIObject* pNearest = FindObjectUnderMouse();
194 if (ret == IN_PASS)
196 // Now we'll call UpdateMouseOver on *all* objects,
197 // we'll input the one hovered, and they will each
198 // update their own data and send messages accordingly
199 m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
201 if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
203 switch (ev->ev.button.button)
205 case SDL_BUTTON_LEFT:
206 // Focus the clicked object (or focus none if nothing clicked on)
207 SetFocusedObject(pNearest);
209 if (pNearest)
210 ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT, EventNameMouseLeftPress);
211 break;
213 case SDL_BUTTON_RIGHT:
214 if (pNearest)
215 ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT, EventNameMouseRightPress);
216 break;
218 default:
219 break;
222 else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest)
224 if (ev->ev.wheel.y < 0)
225 ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_DOWN, EventNameMouseWheelDown);
226 else if (ev->ev.wheel.y > 0)
227 ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_UP, EventNameMouseWheelUp);
229 else if (ev->ev.type == SDL_MOUSEBUTTONUP)
231 switch (ev->ev.button.button)
233 case SDL_BUTTON_LEFT:
234 if (pNearest)
236 double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT];
237 pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time();
238 if (timeElapsed < SELECT_DBLCLICK_RATE)
239 ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT, EventNameMouseLeftDoubleClick);
240 else
241 ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_LEFT, EventNameMouseLeftRelease);
243 break;
244 case SDL_BUTTON_RIGHT:
245 if (pNearest)
247 double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT];
248 pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time();
249 if (timeElapsed < SELECT_DBLCLICK_RATE)
250 ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_RIGHT, EventNameMouseRightDoubleClick);
251 else
252 ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_RIGHT, EventNameMouseRightRelease);
254 break;
257 // Reset all states on all visible objects
258 m_BaseObject->RecurseObject(&IGUIObject::IsHidden, &IGUIObject::ResetStates);
260 // Since the hover state will have been reset, we reload it.
261 m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
265 // BUTTONUP's effect on m_MouseButtons is handled after
266 // everything else, so that e.g. 'press' handlers (activated
267 // on button up) see which mouse button had been pressed.
268 if (ev->ev.type == SDL_MOUSEBUTTONUP)
270 switch (ev->ev.button.button)
272 case SDL_BUTTON_LEFT:
273 case SDL_BUTTON_RIGHT:
274 case SDL_BUTTON_MIDDLE:
275 m_MouseButtons &= ~Bit<unsigned int>(ev->ev.button.button);
276 break;
277 default:
278 break;
282 // Restore m_MousePos (for delayed mouse button events)
283 if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
284 m_MousePos = oldMousePos;
286 // Let GUI items handle keys after everything else, e.g. for input boxes.
287 if (ret == IN_PASS && GetFocusedObject())
289 if (ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_KEYDOWN ||
290 ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYDOWN ||
291 ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING)
292 ret = GetFocusedObject()->ManuallyHandleKeys(ev);
293 // else will return IN_PASS because we never used the button.
296 return ret;
299 void CGUI::TickObjects()
301 m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::Tick);
302 SendEventToAll(EventNameTick);
303 m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
306 void CGUI::SendEventToAll(const CStr& eventName)
308 std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it = m_EventObjects.find(eventName);
309 if (it == m_EventObjects.end())
310 return;
312 std::vector<IGUIObject*> copy = it->second;
313 for (IGUIObject* object : copy)
314 object->ScriptEvent(eventName);
317 void CGUI::SendEventToAll(const CStr& eventName, const JS::HandleValueArray& paramData)
319 std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it = m_EventObjects.find(eventName);
320 if (it == m_EventObjects.end())
321 return;
323 std::vector<IGUIObject*> copy = it->second;
324 for (IGUIObject* object : copy)
325 object->ScriptEvent(eventName, paramData);
328 void CGUI::Draw(CCanvas2D& canvas)
330 using Arena = Allocators::DynamicArena<128 * KiB>;
331 using ObjectListAllocator = ProxyAllocator<VisibleObject, Arena>;
332 Arena arena;
334 std::vector<VisibleObject, ObjectListAllocator> visibleObjects((ObjectListAllocator(arena)));
335 CollectVisibleObjectsRecursively(m_BaseObject->GetChildren(), &visibleObjects);
336 for (VisibleObject& visibleObject : visibleObjects)
337 visibleObject.bufferedZ = visibleObject.object->GetBufferedZ();
339 std::sort(visibleObjects.begin(), visibleObjects.end(), [](const VisibleObject& visibleObject1, const VisibleObject& visibleObject2) -> bool {
340 if (visibleObject1.bufferedZ != visibleObject2.bufferedZ)
341 return visibleObject1.bufferedZ < visibleObject2.bufferedZ;
342 return visibleObject1.index < visibleObject2.index;
345 for (const VisibleObject& visibleObject : visibleObjects)
346 visibleObject.object->Draw(canvas);
349 void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, CCanvas2D& canvas, const CRect& Rect, const CRect& UNUSED(Clipping))
351 // If the sprite doesn't exist (name == ""), don't bother drawing anything
352 if (!Sprite)
353 return;
355 // TODO: Clipping?
357 Sprite.Draw(*this, canvas, Rect, m_Sprites);
360 void CGUI::UpdateResolution()
362 m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
365 IGUIObject* CGUI::ConstructObject(const CStr& str)
367 std::map<CStr, ConstructObjectFunction>::iterator it = m_ObjectTypes.find(str);
369 if (it == m_ObjectTypes.end())
370 return nullptr;
372 return (*it->second)(*this);
375 bool CGUI::AddObject(IGUIObject& parent, IGUIObject& child)
377 if (child.m_Name.empty())
379 LOGERROR("Can't register an object without name!");
380 return false;
383 if (m_pAllObjects.find(child.m_Name) != m_pAllObjects.end())
385 LOGERROR("Can't register more than one object of the name %s", child.m_Name.c_str());
386 return false;
389 m_pAllObjects[child.m_Name] = &child;
390 parent.RegisterChild(&child);
391 return true;
394 IGUIObject* CGUI::GetBaseObject()
396 return m_BaseObject.get();
399 bool CGUI::ObjectExists(const CStr& Name) const
401 return m_pAllObjects.find(Name) != m_pAllObjects.end();
404 IGUIObject* CGUI::FindObjectByName(const CStr& Name) const
406 map_pObjects::const_iterator it = m_pAllObjects.find(Name);
408 if (it == m_pAllObjects.end())
409 return nullptr;
411 return it->second;
414 IGUIObject* CGUI::FindObjectUnderMouse()
416 IGUIObject* pNearest = nullptr;
417 m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::ChooseMouseOverAndClosest, pNearest);
418 return pNearest;
421 CSize2D CGUI::GetWindowSize() const
423 return CSize2D{static_cast<float>(g_xres) / g_VideoMode.GetScale(), static_cast<float>(g_yres) / g_VideoMode.GetScale() };
426 void CGUI::SendFocusMessage(EGUIMessageType msgType)
428 if (m_FocusedObject)
430 SGUIMessage msg(msgType);
431 m_FocusedObject->HandleMessage(msg);
435 void CGUI::SetFocusedObject(IGUIObject* pObject)
437 if (pObject == m_FocusedObject)
438 return;
440 if (m_FocusedObject)
442 SGUIMessage msg(GUIM_LOST_FOCUS);
443 m_FocusedObject->HandleMessage(msg);
446 m_FocusedObject = pObject;
448 if (m_FocusedObject)
450 SGUIMessage msg(GUIM_GOT_FOCUS);
451 m_FocusedObject->HandleMessage(msg);
455 void CGUI::SetObjectStyle(IGUIObject* pObject, const CStr& styleName)
457 // If the style is not recognised (or an empty string) then ApplyStyle will
458 // emit an error message. Thus we don't need to handle it here.
459 pObject->ApplyStyle(styleName);
462 void CGUI::UnsetObjectStyle(IGUIObject* pObject)
464 SetObjectStyle(pObject, "default");
467 void CGUI::SetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag)
469 if (!hotkeyTag.empty())
470 m_HotkeyObjects[hotkeyTag].push_back(pObject);
473 void CGUI::UnsetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag)
475 if (hotkeyTag.empty())
476 return;
478 std::vector<IGUIObject*>& assignment = m_HotkeyObjects[hotkeyTag];
480 assignment.erase(
481 std::remove_if(
482 assignment.begin(),
483 assignment.end(),
484 [&pObject](const IGUIObject* hotkeyObject)
485 { return pObject == hotkeyObject; }),
486 assignment.end());
489 void CGUI::SetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName, JS::HandleValue function)
491 ScriptRequest rq(*m_ScriptInterface);
493 if (hotkeyTag.empty())
495 ScriptException::Raise(rq, "Cannot assign a function to an empty hotkey identifier!");
496 return;
499 // Only support "Press", "Keydown" and "Release" events.
500 if (eventName != EventNamePress && eventName != EventNameKeyDown && eventName != EventNameRelease)
502 ScriptException::Raise(rq, "Cannot assign a function to an unsupported event!");
503 return;
506 if (!function.isObject() || !JS_ObjectIsFunction(&function.toObject()))
508 ScriptException::Raise(rq, "Cannot assign non-function value to global hotkey '%s'", hotkeyTag.c_str());
509 return;
512 UnsetGlobalHotkey(hotkeyTag, eventName);
513 m_GlobalHotkeys[hotkeyTag][eventName].init(rq.cx, function);
516 void CGUI::UnsetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName)
518 std::map<CStr, std::map<CStr, JS::PersistentRootedValue>>::iterator it = m_GlobalHotkeys.find(hotkeyTag);
519 if (it == m_GlobalHotkeys.end())
520 return;
522 m_GlobalHotkeys[hotkeyTag].erase(eventName);
524 if (m_GlobalHotkeys.count(hotkeyTag) == 0)
525 m_GlobalHotkeys.erase(it);
528 const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const
530 std::map<CStr, const SGUIScrollBarStyle>::const_iterator it = m_ScrollBarStyles.find(style);
531 if (it == m_ScrollBarStyles.end())
532 return nullptr;
534 return &it->second;
538 * @callgraph
540 void CGUI::LoadXmlFile(const VfsPath& Filename, std::unordered_set<VfsPath>& Paths)
542 Paths.insert(Filename);
544 CXeromyces xeroFile;
545 if (xeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK)
546 // The error has already been reported by CXeromyces
547 return;
549 XMBElement node = xeroFile.GetRoot();
550 std::string_view root_name(xeroFile.GetElementStringView(node.GetNodeName()));
552 if (root_name == "objects")
553 Xeromyces_ReadRootObjects(xeroFile, node, Paths);
554 else if (root_name == "sprites")
555 Xeromyces_ReadRootSprites(xeroFile, node);
556 else if (root_name == "styles")
557 Xeromyces_ReadRootStyles(xeroFile, node);
558 else if (root_name == "setup")
559 Xeromyces_ReadRootSetup(xeroFile, node);
560 else
561 LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name.data());
564 void CGUI::LoadedXmlFiles()
566 m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
568 SGUIMessage msg(GUIM_LOAD);
569 m_BaseObject->RecurseObject(nullptr, &IGUIObject::HandleMessage, msg);
571 SendEventToAll(EventNameLoad);
574 //===================================================================
575 // XML Reading Xeromyces Specific Sub-Routines
576 //===================================================================
578 void CGUI::Xeromyces_ReadRootObjects(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
580 int el_script = xmb.GetElementID("script");
582 std::vector<std::pair<CStr, CStr> > subst;
584 // Iterate main children
585 // they should all be <object> or <script> elements
586 for (XMBElement child : element.GetChildNodes())
588 if (child.GetNodeName() == el_script)
589 // Execute the inline script
590 Xeromyces_ReadScript(xmb, child, Paths);
591 else
592 // Read in this whole object into the GUI
593 Xeromyces_ReadObject(xmb, child, m_BaseObject.get(), subst, Paths, 0);
597 void CGUI::Xeromyces_ReadRootSprites(const XMBData& xmb, XMBElement element)
599 for (XMBElement child : element.GetChildNodes())
600 Xeromyces_ReadSprite(xmb, child);
603 void CGUI::Xeromyces_ReadRootStyles(const XMBData& xmb, XMBElement element)
605 for (XMBElement child : element.GetChildNodes())
606 Xeromyces_ReadStyle(xmb, child);
609 void CGUI::Xeromyces_ReadRootSetup(const XMBData& xmb, XMBElement element)
611 for (XMBElement child : element.GetChildNodes())
613 std::string_view name(xmb.GetElementStringView(child.GetNodeName()));
614 if (name == "scrollbar")
615 Xeromyces_ReadScrollBarStyle(xmb, child);
616 else if (name == "icon")
617 Xeromyces_ReadIcon(xmb, child);
618 else if (name == "tooltip")
619 Xeromyces_ReadTooltip(xmb, child);
620 else if (name == "color")
621 Xeromyces_ReadColor(xmb, child);
622 else
623 debug_warn(L"Invalid data - DTD shouldn't allow this");
627 IGUIObject* CGUI::Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObject* pParent, std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth)
629 ENSURE(pParent);
631 XMBAttributeList attributes = element.GetAttributes();
633 CStr type(attributes.GetNamedItem(xmb.GetAttributeID("type")));
634 if (type.empty())
635 type = "empty";
637 // Construct object from specified type
638 // henceforth, we need to do a rollback before aborting.
639 // i.e. releasing this object
640 IGUIObject* object = ConstructObject(type);
642 if (!object)
644 LOGERROR("GUI: Unrecognized object type \"%s\"", type.c_str());
645 return nullptr;
648 // Cache some IDs for element attribute names, to avoid string comparisons
649 #define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
650 #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
651 ELMT(object);
652 ELMT(action);
653 ELMT(script);
654 ELMT(repeat);
655 ELMT(translatableAttribute);
656 ELMT(translate);
657 ELMT(attribute);
658 ELMT(keep);
659 ELMT(include);
660 ATTR(style);
661 ATTR(type);
662 ATTR(name);
663 ATTR(z);
664 ATTR(on);
665 ATTR(file);
666 ATTR(directory);
667 ATTR(id);
668 ATTR(context);
671 // Read Style and set defaults
673 // If the setting "style" is set, try loading that setting.
675 // Always load default (if it's available) first!
677 SetObjectStyle(object, "default");
679 CStr argStyle(attributes.GetNamedItem(attr_style));
680 if (!argStyle.empty())
681 SetObjectStyle(object, argStyle);
683 bool NameSet = false;
684 bool ManuallySetZ = false;
686 for (XMBAttribute attr : attributes)
688 // If value is "null", then it is equivalent as never being entered
689 if (attr.Value == "null")
690 continue;
692 // Ignore "type" and "style", we've already checked it
693 if (attr.Name == attr_type || attr.Name == attr_style)
694 continue;
696 if (attr.Name == attr_name)
698 CStr name(attr.Value);
700 if (name.Left(2) == "__")
702 LOGERROR("GUI: Names starting with '__' are reserved for the engine (object: %s)", name.c_str());
703 continue;
706 for (const std::pair<CStr, CStr>& sub : NameSubst)
707 name.Replace(sub.first, sub.second);
709 object->SetName(name);
710 NameSet = true;
711 continue;
714 if (attr.Name == attr_z)
715 ManuallySetZ = true;
717 object->SetSettingFromString(xmb.GetAttributeString(attr.Name), attr.Value.FromUTF8(), false);
720 // Check if name isn't set, generate an internal name in that case.
721 if (!NameSet)
723 object->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber) + ")");
724 ++m_InternalNameNumber;
727 CStrW caption(element.GetText().FromUTF8());
728 if (!caption.empty())
729 object->SetSettingFromString("caption", caption, false);
731 for (XMBElement child : element.GetChildNodes())
733 // Check what name the elements got
734 int element_name = child.GetNodeName();
736 if (element_name == elmt_object)
738 // Call this function on the child
739 Xeromyces_ReadObject(xmb, child, object, NameSubst, Paths, nesting_depth);
741 else if (element_name == elmt_action)
743 // Scripted <action> element
745 // Check for a 'file' parameter
746 CStrW filename(child.GetAttributes().GetNamedItem(attr_file).FromUTF8());
748 CStr code;
750 // If there is a file, open it and use it as the code
751 if (!filename.empty())
753 Paths.insert(filename);
754 CVFSFile scriptfile;
755 if (scriptfile.Load(g_VFS, filename) != PSRETURN_OK)
757 LOGERROR("Error opening GUI script action file '%s'", utf8_from_wstring(filename));
758 continue;
761 code = scriptfile.DecodeUTF8(); // assume it's UTF-8
764 XMBElementList grandchildren = child.GetChildNodes();
765 if (!grandchildren.empty()) // The <action> element contains <keep> and <translate> tags.
766 for (XMBElement grandchild : grandchildren)
768 if (grandchild.GetNodeName() == elmt_translate)
769 code += g_L10n.Translate(grandchild.GetText());
770 else if (grandchild.GetNodeName() == elmt_keep)
771 code += grandchild.GetText();
773 else // It's pure JavaScript code.
774 // Read the inline code (concatenating to the file code, if both are specified)
775 code += CStr(child.GetText());
777 CStr eventName = child.GetAttributes().GetNamedItem(attr_on);
778 object->RegisterScriptHandler(eventName, code, *this);
780 else if (child.GetNodeName() == elmt_script)
782 Xeromyces_ReadScript(xmb, child, Paths);
784 else if (element_name == elmt_repeat)
786 Xeromyces_ReadRepeat(xmb, child, object, NameSubst, Paths, nesting_depth);
788 else if (element_name == elmt_translatableAttribute)
790 // This is an element in the form "<translatableAttribute id="attributeName">attributeValue</translatableAttribute>".
791 CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name.
792 if (attributeName.empty())
794 LOGERROR("GUI: 'translatableAttribute' XML element with empty 'id' XML attribute found. (object: %s)", object->GetPresentableName().c_str());
795 continue;
798 CStr value(child.GetText());
799 if (value.empty())
800 continue;
802 CStr context(child.GetAttributes().GetNamedItem(attr_context)); // Read the context if any.
804 CStr translatedValue = context.empty() ?
805 g_L10n.Translate(value) :
806 g_L10n.TranslateWithContext(context, value);
808 object->SetSettingFromString(attributeName, translatedValue.FromUTF8(), false);
810 else if (element_name == elmt_attribute)
812 // This is an element in the form "<attribute id="attributeName"><keep>Don't translate this part
813 // </keep><translate>but translate this one.</translate></attribute>".
814 CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name.
815 if (attributeName.empty())
817 LOGERROR("GUI: 'attribute' XML element with empty 'id' XML attribute found. (object: %s)", object->GetPresentableName().c_str());
818 continue;
821 CStr translatedValue;
823 for (XMBElement grandchild : child.GetChildNodes())
825 if (grandchild.GetNodeName() == elmt_translate)
826 translatedValue += g_L10n.Translate(grandchild.GetText());
827 else if (grandchild.GetNodeName() == elmt_keep)
828 translatedValue += grandchild.GetText();
830 object->SetSettingFromString(attributeName, translatedValue.FromUTF8(), false);
832 else if (element_name == elmt_include)
834 CStrW filename(child.GetAttributes().GetNamedItem(attr_file).FromUTF8());
835 CStrW directory(child.GetAttributes().GetNamedItem(attr_directory).FromUTF8());
836 if (!filename.empty())
838 if (!directory.empty())
839 LOGWARNING("GUI: Include element found with file name (%s) and directory name (%s). Only the file will be processed.", utf8_from_wstring(filename), utf8_from_wstring(directory));
841 Paths.insert(filename);
843 CXeromyces xeroIncluded;
844 if (xeroIncluded.Load(g_VFS, filename, "gui") != PSRETURN_OK)
846 LOGERROR("GUI: Error reading included XML: '%s'", utf8_from_wstring(filename));
847 continue;
850 XMBElement node = xeroIncluded.GetRoot();
851 if (node.GetNodeName() != xeroIncluded.GetElementID("object"))
853 LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", utf8_from_wstring(filename));
854 continue;
857 if (nesting_depth+1 >= MAX_OBJECT_DEPTH)
859 LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(filename));
860 continue;
863 Xeromyces_ReadObject(xeroIncluded, node, object, NameSubst, Paths, nesting_depth+1);
865 else if (!directory.empty())
867 if (nesting_depth+1 >= MAX_OBJECT_DEPTH)
869 LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(directory));
870 continue;
873 VfsPaths pathnames;
874 vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames);
875 for (const VfsPath& path : pathnames)
877 // as opposed to loading scripts, don't care if it's loaded before
878 // one might use the same parts of the GUI in different situations
879 Paths.insert(path);
880 CXeromyces xeroIncluded;
881 if (xeroIncluded.Load(g_VFS, path, "gui") != PSRETURN_OK)
883 LOGERROR("GUI: Error reading included XML: '%s'", path.string8());
884 continue;
887 XMBElement node = xeroIncluded.GetRoot();
888 if (node.GetNodeName() != xeroIncluded.GetElementID("object"))
890 LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", path.string8());
891 continue;
893 Xeromyces_ReadObject(xeroIncluded, node, object, NameSubst, Paths, nesting_depth+1);
897 else
898 LOGERROR("GUI: 'include' XML element must have valid 'file' or 'directory' attribute found. (object %s)", object->GetPresentableName().c_str());
900 else
902 // Try making the object read the tag.
903 if (!object->HandleAdditionalChildren(xmb, child))
904 LOGERROR("GUI: (object: %s) Reading unknown children for its type", object->GetPresentableName().c_str());
908 object->AdditionalChildrenHandled();
910 if (!ManuallySetZ)
912 // Set it automatically to 10 plus its parents
913 if (object->m_Absolute)
914 // If the object is absolute, we'll have to get the parent's Z buffered,
915 // and add to that!
916 object->m_Z.Set(pParent->GetBufferedZ() + 10.f, false);
917 else
918 // If the object is relative, then we'll just store Z as "10"
919 object->m_Z.Set(10.f, false);
922 if (!AddObject(*pParent, *object))
924 delete object;
925 return nullptr;
927 return object;
930 void CGUI::Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObject* pParent, std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth)
932 #define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
933 #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
934 ELMT(object);
935 ATTR(count);
936 ATTR(var);
938 XMBAttributeList attributes = element.GetAttributes();
940 int count = CStr(attributes.GetNamedItem(attr_count)).ToInt();
941 CStr var("["+attributes.GetNamedItem(attr_var)+"]");
942 if (var.size() < 3)
943 var = "[n]";
945 for (int n = 0; n < count; ++n)
947 NameSubst.emplace_back(var, "[" + CStr::FromInt(n) + "]");
949 XERO_ITER_EL(element, child)
951 if (child.GetNodeName() == elmt_object)
952 Xeromyces_ReadObject(xmb, child, pParent, NameSubst, Paths, nesting_depth);
954 NameSubst.pop_back();
958 void CGUI::Xeromyces_ReadScript(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
960 // Check for a 'file' parameter
961 CStrW fileAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("file")).FromUTF8());
963 // If there is a file specified, open and execute it
964 if (!fileAttr.empty())
966 if (!VfsPath(fileAttr).IsDirectory())
968 Paths.insert(fileAttr);
969 m_ScriptInterface->LoadGlobalScriptFile(fileAttr);
971 else
972 LOGERROR("GUI: Script path %s is not a file path", fileAttr.ToUTF8().c_str());
975 // If it has a directory attribute, read all JS files in that directory
976 CStrW directoryAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("directory")).FromUTF8());
977 if (!directoryAttr.empty())
979 if (VfsPath(directoryAttr).IsDirectory())
981 VfsPaths pathnames;
982 vfs::GetPathnames(g_VFS, directoryAttr, L"*.js", pathnames);
983 for (const VfsPath& path : pathnames)
985 // Only load new files (so when the insert succeeds)
986 if (Paths.insert(path).second)
987 m_ScriptInterface->LoadGlobalScriptFile(path);
990 else
991 LOGERROR("GUI: Script path %s is not a directory path", directoryAttr.ToUTF8().c_str());
994 CStr code(element.GetText());
995 if (!code.empty())
996 m_ScriptInterface->LoadGlobalScript(L"Some XML file", code);
999 void CGUI::Xeromyces_ReadSprite(const XMBData& xmb, XMBElement element)
1001 auto sprite = std::make_unique<CGUISprite>();
1003 // Get name, we know it exists because of DTD requirements
1004 CStr name = element.GetAttributes().GetNamedItem(xmb.GetAttributeID("name"));
1006 if (m_Sprites.find(name) != m_Sprites.end())
1007 LOGWARNING("GUI sprite name '%s' used more than once; first definition will be discarded", name.c_str());
1009 // shared_ptr to link the effect to every image, faster than copy.
1010 std::shared_ptr<SGUIImageEffects> effects;
1012 for (XMBElement child : element.GetChildNodes())
1014 std::string_view ElementName(xmb.GetElementStringView(child.GetNodeName()));
1015 if (ElementName == "image")
1016 Xeromyces_ReadImage(xmb, child, *sprite);
1017 else if (ElementName == "effect")
1019 if (effects)
1020 LOGERROR("GUI <sprite> must not have more than one <effect>");
1021 else
1023 effects = std::make_shared<SGUIImageEffects>();
1024 Xeromyces_ReadEffects(xmb, child, *effects);
1027 else
1028 debug_warn(L"Invalid data - DTD shouldn't allow this");
1031 // Apply the effects to every image (unless the image overrides it with
1032 // different effects)
1033 if (effects)
1035 for (const std::unique_ptr<SGUIImage>& image : sprite->m_Images)
1036 if (!image->m_Effects)
1037 image->m_Effects = effects;
1040 m_Sprites.erase(name);
1041 m_Sprites.emplace(name, std::move(sprite));
1044 void CGUI::Xeromyces_ReadImage(const XMBData& xmb, XMBElement element, CGUISprite& parent)
1046 auto image = std::make_unique<SGUIImage>();
1048 // TODO Gee: Setup defaults here (or maybe they are in the SGUIImage ctor)
1050 for (XMBAttribute attr : element.GetAttributes())
1052 std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1053 CStrW attr_value(attr.Value.FromUTF8());
1055 if (attr_name == "texture")
1057 image->m_TextureName = VfsPath("art/textures/ui") / attr_value;
1059 else if (attr_name == "size")
1061 image->m_Size.FromString(attr.Value);
1063 else if (attr_name == "texture_size")
1065 image->m_TextureSize.FromString(attr.Value);
1067 else if (attr_name == "real_texture_placement")
1069 CRect rect;
1070 if (!ParseString<CRect>(this, attr_value, rect))
1071 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1072 else
1073 image->m_TexturePlacementInFile = rect;
1075 else if (attr_name == "fixed_h_aspect_ratio")
1077 float val;
1078 if (!ParseString<float>(this, attr_value, val))
1079 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1080 else
1081 image->m_FixedHAspectRatio = val;
1083 else if (attr_name == "round_coordinates")
1085 bool b;
1086 if (!ParseString<bool>(this, attr_value, b))
1087 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1088 else
1089 image->m_RoundCoordinates = b;
1091 else if (attr_name == "wrap_mode")
1093 if (attr_value == L"repeat")
1094 image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::REPEAT;
1095 else if (attr_value == L"mirrored_repeat")
1096 image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT;
1097 else if (attr_value == L"clamp_to_edge")
1098 image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE;
1099 else
1100 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1102 else if (attr_name == "backcolor")
1104 if (!ParseString<CGUIColor>(this, attr_value, image->m_BackColor))
1105 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1107 else
1108 debug_warn(L"Invalid data - DTD shouldn't allow this");
1111 // Look for effects
1112 for (XMBElement child : element.GetChildNodes())
1114 std::string_view ElementName(xmb.GetElementStringView(child.GetNodeName()));
1115 if (ElementName == "effect")
1117 if (image->m_Effects)
1118 LOGERROR("GUI <image> must not have more than one <effect>");
1119 else
1121 image->m_Effects = std::make_shared<SGUIImageEffects>();
1122 Xeromyces_ReadEffects(xmb, child, *image->m_Effects);
1125 else
1126 debug_warn(L"Invalid data - DTD shouldn't allow this");
1129 parent.AddImage(std::move(image));
1132 void CGUI::Xeromyces_ReadEffects(const XMBData& xmb, XMBElement element, SGUIImageEffects& effects)
1134 for (XMBAttribute attr : element.GetAttributes())
1136 std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1137 if (attr_name == "add_color")
1139 if (!effects.m_AddColor.ParseString(*this, attr.Value, 0))
1140 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr.Value);
1142 else if (attr_name == "grayscale")
1143 effects.m_Greyscale = true;
1144 else
1145 debug_warn(L"Invalid data - DTD shouldn't allow this");
1149 void CGUI::Xeromyces_ReadStyle(const XMBData& xmb, XMBElement element)
1151 SGUIStyle style;
1152 CStr name;
1154 for (XMBAttribute attr : element.GetAttributes())
1156 std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1157 // The "name" setting is actually the name of the style
1158 // and not a new default
1159 if (attr_name == "name")
1160 name = attr.Value;
1161 else
1162 style.m_SettingsDefaults.emplace(std::string(attr_name), attr.Value.FromUTF8());
1165 m_Styles.erase(name);
1166 m_Styles.emplace(name, std::move(style));
1169 void CGUI::Xeromyces_ReadScrollBarStyle(const XMBData& xmb, XMBElement element)
1171 SGUIScrollBarStyle scrollbar;
1172 CStr name;
1174 // Setup some defaults.
1175 scrollbar.m_MinimumBarSize = 0.f;
1176 // Using 1.0e10 as a substitute for infinity
1177 scrollbar.m_MaximumBarSize = 1.0e10;
1178 scrollbar.m_UseEdgeButtons = false;
1180 for (XMBAttribute attr : element.GetAttributes())
1182 std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1183 CStr attr_value(attr.Value);
1185 if (attr_value == "null")
1186 continue;
1188 if (attr_name == "name")
1189 name = attr_value;
1190 else if (attr_name == "show_edge_buttons")
1192 bool b;
1193 if (!ParseString<bool>(this, attr_value.FromUTF8(), b))
1194 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1195 else
1196 scrollbar.m_UseEdgeButtons = b;
1198 else if (attr_name == "width")
1200 float f;
1201 if (!ParseString<float>(this, attr_value.FromUTF8(), f))
1202 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1203 else
1204 scrollbar.m_Width = f;
1206 else if (attr_name == "minimum_bar_size")
1208 float f;
1209 if (!ParseString<float>(this, attr_value.FromUTF8(), f))
1210 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1211 else
1212 scrollbar.m_MinimumBarSize = f;
1214 else if (attr_name == "maximum_bar_size")
1216 float f;
1217 if (!ParseString<float>(this, attr_value.FromUTF8(), f))
1218 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1219 else
1220 scrollbar.m_MaximumBarSize = f;
1222 else if (attr_name == "sprite_button_top")
1223 scrollbar.m_SpriteButtonTop = attr_value;
1224 else if (attr_name == "sprite_button_top_pressed")
1225 scrollbar.m_SpriteButtonTopPressed = attr_value;
1226 else if (attr_name == "sprite_button_top_disabled")
1227 scrollbar.m_SpriteButtonTopDisabled = attr_value;
1228 else if (attr_name == "sprite_button_top_over")
1229 scrollbar.m_SpriteButtonTopOver = attr_value;
1230 else if (attr_name == "sprite_button_bottom")
1231 scrollbar.m_SpriteButtonBottom = attr_value;
1232 else if (attr_name == "sprite_button_bottom_pressed")
1233 scrollbar.m_SpriteButtonBottomPressed = attr_value;
1234 else if (attr_name == "sprite_button_bottom_disabled")
1235 scrollbar.m_SpriteButtonBottomDisabled = attr_value;
1236 else if (attr_name == "sprite_button_bottom_over")
1237 scrollbar.m_SpriteButtonBottomOver = attr_value;
1238 else if (attr_name == "sprite_back_vertical")
1239 scrollbar.m_SpriteBackVertical = attr_value;
1240 else if (attr_name == "sprite_bar_vertical")
1241 scrollbar.m_SpriteBarVertical = attr_value;
1242 else if (attr_name == "sprite_bar_vertical_over")
1243 scrollbar.m_SpriteBarVerticalOver = attr_value;
1244 else if (attr_name == "sprite_bar_vertical_pressed")
1245 scrollbar.m_SpriteBarVerticalPressed = attr_value;
1248 m_ScrollBarStyles.erase(name);
1249 m_ScrollBarStyles.emplace(name, std::move(scrollbar));
1252 void CGUI::Xeromyces_ReadIcon(const XMBData& xmb, XMBElement element)
1254 SGUIIcon icon;
1255 CStr name;
1257 for (XMBAttribute attr : element.GetAttributes())
1259 std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1260 CStr attr_value(attr.Value);
1262 if (attr_value == "null")
1263 continue;
1265 if (attr_name == "name")
1266 name = attr_value;
1267 else if (attr_name == "sprite")
1268 icon.m_SpriteName = attr_value;
1269 else if (attr_name == "size")
1271 CSize2D size;
1272 if (!ParseString<CSize2D>(this, attr_value.FromUTF8(), size))
1273 LOGERROR("Error parsing '%s' (\"%s\") inside <icon>.", attr_name, attr_value);
1274 else
1275 icon.m_Size = size;
1277 else
1278 debug_warn(L"Invalid data - DTD shouldn't allow this");
1281 m_Icons.erase(name);
1282 m_Icons.emplace(name, std::move(icon));
1285 void CGUI::Xeromyces_ReadTooltip(const XMBData& xmb, XMBElement element)
1287 IGUIObject* object = new CTooltip(*this);
1289 for (XMBAttribute attr : element.GetAttributes())
1291 std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1292 CStr attr_value(attr.Value);
1294 if (attr_name == "name")
1295 object->SetName("__tooltip_" + attr_value);
1296 else
1297 object->SetSettingFromString(std::string(attr_name), attr_value.FromUTF8(), true);
1300 if (!AddObject(*m_BaseObject, *object))
1301 delete object;
1304 void CGUI::Xeromyces_ReadColor(const XMBData& xmb, XMBElement element)
1306 XMBAttributeList attributes = element.GetAttributes();
1307 CStr name = attributes.GetNamedItem(xmb.GetAttributeID("name"));
1309 // Try parsing value
1310 CStr value(element.GetText());
1311 if (value.empty())
1312 return;
1314 CColor color;
1315 if (color.ParseString(value))
1317 m_PreDefinedColors.erase(name);
1318 m_PreDefinedColors.emplace(
1319 std::piecewise_construct,
1320 std::forward_as_tuple(name),
1321 std::forward_as_tuple(color.r, color.g, color.b, color.a));
1323 else
1324 LOGERROR("GUI: Unable to create custom color '%s'. Invalid color syntax.", name.c_str());