Index: ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 22133) +++ ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 22134) @@ -1,225 +1,221 @@ /** * Holder for the template file being displayed. */ var g_Template = {}; /** * Holder for the template lists generated by compileTemplateLists(). */ var g_TemplateLists = {}; /** * Used to display textual information and the build/train lists of the * template being displayed. * * At present, these are drawn in the main body of the page. */ var g_InfoFunctions = [ getEntityTooltip, getHistoryTooltip, getDescriptionTooltip, getAurasTooltip, getVisibleEntityClassesFormatted, getBuiltByText, getTrainedByText, getResearchedByText, getBuildText, getTrainText, getResearchText, getUpgradeText ]; /** * Override style so we can get a bigger specific name. */ g_TooltipTextFormats.nameSpecificBig.font = "sans-bold-20"; g_TooltipTextFormats.nameSpecificSmall.font = "sans-bold-16"; g_TooltipTextFormats.nameGeneric.font = "sans-bold-16"; /** * Path to unit rank icons. */ var g_RankIconPath = "session/icons/ranks/"; /** * Page initialisation. May also eventually pre-draw/arrange objects. * * @param {object} data - Contains the civCode and the name of the template to display. * @param {string} data.templateName * @param {string} [data.civ] * @param {*} [data.callback] - If set and loosely equivalent to true, a callback is * assumed to be setup ready be called by the Engine upon * closure of this page. */ function init(data) { if (!data || !data.templateName) { error("Viewer: No template provided"); closePage(); return; } if (data.callback) g_CallbackSet = true; let templateName = removeFiltersFromTemplateName(data.templateName); let isTech = techDataExists(templateName); // Attempt to get the civ code from the template, or, if // it's a technology, from the researcher's template. if (!isTech) { // Catch and redirect if template is a non-promotion variant of // another (ie. units/{civ}_support_female_citizen_house). templateName = getBaseTemplateName(templateName); g_SelectedCiv = loadTemplate(templateName).Identity.Civ; } if (g_SelectedCiv == "gaia" && data.civ) g_SelectedCiv = data.civ; g_TemplateLists = compileTemplateLists(g_SelectedCiv); g_CurrentModifiers = deriveModifications(g_AutoResearchTechList); g_Template = isTech ? loadTechnology(templateName) : loadEntityTemplate(templateName); if (!g_Template) { error("Viewer: unable to recognise or load template (" + templateName + ")"); closePage(); return; } g_StatsFunctions = [getEntityCostTooltip].concat(g_StatsFunctions); draw(); } /** * Populate the UI elements. - * - * @todo (c++ change) Implement and use a function that fetches height of rendered text block from text object. */ function draw() { Engine.GetGUIObjectByName("entityName").caption = getEntityNamesFormatted(g_Template); let entityIcon = Engine.GetGUIObjectByName("entityIcon"); entityIcon.sprite = "stretched:session/portraits/" + g_Template.icon; let entityStats = Engine.GetGUIObjectByName("entityStats"); entityStats.caption = buildText(g_Template, g_StatsFunctions); - // This is something of a crude hack. See above todo. let entityInfo = Engine.GetGUIObjectByName("entityInfo"); - let lines = entityStats.caption.split("\n").length; - let fontSize = +entityStats.font.split("-")[1] + 4; let infoSize = entityInfo.size; - infoSize.top = Math.max(entityIcon.size.bottom, lines * fontSize + entityStats.size.top) + 8; + // The magic '8' below provides a gap between the bottom of the icon, and the start of the info text. + infoSize.top = Math.max(entityIcon.size.bottom + 8, entityStats.size.top + entityStats.getTextSize().height); entityInfo.size = infoSize; entityInfo.caption = buildText(g_Template, g_InfoFunctions, "\n\n"); if (g_Template.promotion) Engine.GetGUIObjectByName("entityRankGlyph").sprite = "stretched:" + g_RankIconPath + g_Template.promotion.current_rank + ".png"; Engine.GetGUIObjectByName("entityRankGlyph").hidden = !g_Template.promotion; } function getBuiltByText(template) { if (g_SelectedCiv == "gaia" || !g_TemplateLists.structures.has(template.name.internal)) return ""; let builders = g_TemplateLists.structures.get(template.name.internal); if (!builders.length) return ""; // Translation: Label before a list of the names of units that build the structure selected. return buildListText(translate("Built by:"), builders.map(builder => getEntityNames(loadEntityTemplate(builder)))); } function getTrainedByText(template) { if (g_SelectedCiv == "gaia" || !g_TemplateLists.units.has(template.name.internal)) return ""; let trainers = g_TemplateLists.units.get(template.name.internal); if (!trainers.length) return ""; // Translation: Label before a list of the names of structures or units that train the unit selected. return buildListText(translate("Trained by:"), trainers.map(trainer => getEntityNames(loadEntityTemplate(trainer)))); } function getResearchedByText(template) { if (g_SelectedCiv == "gaia" || !g_TemplateLists.techs.has(template.name.internal)) return ""; let researchers = g_TemplateLists.techs.get(template.name.internal); if (!researchers.length) return ""; // Translation: Label before a list of names of structures or units that research the technology selected. return buildListText(translate("Researched at:"), researchers.map(researcher => getEntityNames(loadEntityTemplate(researcher)))); } /** * @return {string} List of the names of the buildings the selected unit can build. */ function getBuildText(template) { if (!template.builder || !template.builder.length) return ""; // Translation: Label before a list of the names of structures the selected unit can construct or build. return buildListText(translate("Builds:"), template.builder.map(prod => getEntityNames(loadEntityTemplate(prod)))); } /** * @return {string} List of the names of the technologies the selected structure/unit can research. */ function getResearchText(template) { if (!template.production || !template.production.techs || !template.production.techs.length) return ""; let researchNames = []; for (let tech of template.production.techs) { let techTemplate = loadTechnology(tech); if (techTemplate.reqs) researchNames.push(getEntityNames(techTemplate)); } // Translation: Label before a list of the names of technologies the selected unit or structure can research. return buildListText(translate("Researches:"), researchNames); } /** * @return {string} List of the names of the units the selected unit can train. */ function getTrainText(template) { if (!template.production || !template.production.units || !template.production.units.length) return ""; // Translation: Label before a list of the names of units the selected unit or structure can train. return buildListText(translate("Trains:"), template.production.units.map(prod => getEntityNames(loadEntityTemplate(prod)))); } /** * @return {string} List of the names of the buildings/units the selected structure/unit can upgrade to. */ function getUpgradeText(template) { if (!template.upgrades) return ""; // Translation: Label before a list of the names of units or structures the selected unit or structure can be upgradable to. return buildListText(translate("Upgradable to:"), template.upgrades.map(upgrade => getEntityNames(upgrade.name ? upgrade : loadEntityTemplate(upgrade.entity)))); } Index: ps/trunk/source/gui/CGUI.cpp =================================================================== --- ps/trunk/source/gui/CGUI.cpp (revision 22133) +++ ps/trunk/source/gui/CGUI.cpp (revision 22134) @@ -1,1745 +1,1755 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include #include #include "GUI.h" // Types - when including them into the engine. #include "CButton.h" #include "CChart.h" #include "CCheckBox.h" #include "CDropDown.h" #include "CImage.h" #include "CInput.h" #include "CList.h" #include "COList.h" #include "CProgressBar.h" #include "CRadioButton.h" #include "CSlider.h" #include "CText.h" #include "CTooltip.h" #include "MiniMap.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "i18n/L10n.h" #include "lib/bits.h" #include "lib/input.h" #include "lib/sysdep/sysdep.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "scripting/ScriptFunctions.h" #include "scriptinterface/ScriptInterface.h" extern int g_yres; const double SELECT_DBLCLICK_RATE = 0.5; const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion InReaction CGUI::HandleEvent(const SDL_Event_* ev) { InReaction ret = IN_PASS; if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYUP) { const char* hotkey = static_cast(ev->ev.user.data1); std::map >::iterator it = m_HotkeyObjects.find(hotkey); if (it != m_HotkeyObjects.end()) for (IGUIObject* const& obj : it->second) { // Update hotkey status before sending the event, // else the status will be outdated when processing the GUI event. HotkeyInputHandler(ev); ret = IN_HANDLED; if (ev->ev.type == SDL_HOTKEYDOWN) obj->SendEvent(GUIM_PRESSED, "press"); else obj->SendEvent(GUIM_RELEASED, "release"); } } else if (ev->ev.type == SDL_MOUSEMOTION) { // Yes the mouse position is stored as float to avoid // constant conversions when operating in a // float-based environment. m_MousePos = CPos((float)ev->ev.motion.x / g_GuiScale, (float)ev->ev.motion.y / g_GuiScale); SGUIMessage msg(GUIM_MOUSE_MOTION); GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::HandleMessage, msg); } // Update m_MouseButtons. (BUTTONUP is handled later.) else if (ev->ev.type == SDL_MOUSEBUTTONDOWN) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: case SDL_BUTTON_RIGHT: case SDL_BUTTON_MIDDLE: m_MouseButtons |= Bit(ev->ev.button.button); break; default: break; } } // Update m_MousePos (for delayed mouse button events) CPos oldMousePos = m_MousePos; if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP) { m_MousePos = CPos((float)ev->ev.button.x / g_GuiScale, (float)ev->ev.button.y / g_GuiScale); } // Only one object can be hovered IGUIObject* pNearest = NULL; // TODO Gee: (2004-09-08) Big TODO, don't do the below if the SDL_Event is something like a keypress! try { PROFILE("mouse events"); // TODO Gee: Optimizations needed! // these two recursive function are quite overhead heavy. // pNearest will after this point at the hovered object, possibly NULL pNearest = FindObjectUnderMouse(); // Now we'll call UpdateMouseOver on *all* objects, // we'll input the one hovered, and they will each // update their own data and send messages accordingly GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest); if (ev->ev.type == SDL_MOUSEBUTTONDOWN) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: // Focus the clicked object (or focus none if nothing clicked on) SetFocusedObject(pNearest); if (pNearest) ret = pNearest->SendEvent(GUIM_MOUSE_PRESS_LEFT, "mouseleftpress"); break; case SDL_BUTTON_RIGHT: if (pNearest) ret = pNearest->SendEvent(GUIM_MOUSE_PRESS_RIGHT, "mouserightpress"); break; default: break; } } else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest) { if (ev->ev.wheel.y < 0) ret = pNearest->SendEvent(GUIM_MOUSE_WHEEL_DOWN, "mousewheeldown"); else if (ev->ev.wheel.y > 0) ret = pNearest->SendEvent(GUIM_MOUSE_WHEEL_UP, "mousewheelup"); } else if (ev->ev.type == SDL_MOUSEBUTTONUP) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: if (pNearest) { double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT]; pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time(); if (timeElapsed < SELECT_DBLCLICK_RATE) ret = pNearest->SendEvent(GUIM_MOUSE_DBLCLICK_LEFT, "mouseleftdoubleclick"); else ret = pNearest->SendEvent(GUIM_MOUSE_RELEASE_LEFT, "mouseleftrelease"); } break; case SDL_BUTTON_RIGHT: if (pNearest) { double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT]; pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time(); if (timeElapsed < SELECT_DBLCLICK_RATE) ret = pNearest->SendEvent(GUIM_MOUSE_DBLCLICK_RIGHT, "mouserightdoubleclick"); else ret = pNearest->SendEvent(GUIM_MOUSE_RELEASE_RIGHT, "mouserightrelease"); } break; } // Reset all states on all visible objects GUI<>::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::ResetStates); // Since the hover state will have been reset, we reload it. GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest); } } catch (PSERROR_GUI& e) { UNUSED2(e); debug_warn(L"CGUI::HandleEvent error"); // TODO Gee: Handle } // BUTTONUP's effect on m_MouseButtons is handled after // everything else, so that e.g. 'press' handlers (activated // on button up) see which mouse button had been pressed. if (ev->ev.type == SDL_MOUSEBUTTONUP) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: case SDL_BUTTON_RIGHT: case SDL_BUTTON_MIDDLE: m_MouseButtons &= ~Bit(ev->ev.button.button); break; default: break; } } // Restore m_MousePos (for delayed mouse button events) if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP) m_MousePos = oldMousePos; // Handle keys for input boxes if (GetFocusedObject()) { if ((ev->ev.type == SDL_KEYDOWN && ev->ev.key.keysym.sym != SDLK_ESCAPE && !g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] && !g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) || ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING) { ret = GetFocusedObject()->ManuallyHandleEvent(ev); } // else will return IN_PASS because we never used the button. } return ret; } void CGUI::TickObjects() { CStr action = "tick"; GUI::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, action); m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, this); } void CGUI::SendEventToAll(const CStr& EventName) { // janwas 2006-03-03: spoke with Ykkrosh about EventName case. // when registering, case is converted to lower - this avoids surprise // if someone were to get the case wrong and then not notice their // handler is never called. however, until now, the other end // (sending events here) wasn't converting to lower case, // leading to a similar problem. // now fixed; case is irrelevant since all are converted to lower. GUI::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, EventName.LowerCase()); } CGUI::CGUI(const shared_ptr& runtime) : m_MouseButtons(0), m_FocusedObject(NULL), m_InternalNameNumber(0) { m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIPage", runtime)); GuiScriptingInit(*m_ScriptInterface); m_ScriptInterface->LoadGlobalScripts(); m_BaseObject = new CGUIDummyObject; m_BaseObject->SetGUI(this); } CGUI::~CGUI() { Destroy(); if (m_BaseObject) delete m_BaseObject; } IGUIObject* CGUI::ConstructObject(const CStr& str) { if (m_ObjectTypes.count(str) > 0) return (*m_ObjectTypes[str])(); else { // Error reporting will be handled with the NULL return. return NULL; } } void CGUI::Initialize() { // Add base types! // You can also add types outside the GUI to extend the flexibility of the GUI. // Pyrogenesis though will have all the object types inserted from here. AddObjectType("empty", &CGUIDummyObject::ConstructObject); AddObjectType("button", &CButton::ConstructObject); AddObjectType("image", &CImage::ConstructObject); AddObjectType("text", &CText::ConstructObject); AddObjectType("checkbox", &CCheckBox::ConstructObject); AddObjectType("radiobutton", &CRadioButton::ConstructObject); AddObjectType("progressbar", &CProgressBar::ConstructObject); AddObjectType("minimap", &CMiniMap::ConstructObject); AddObjectType("input", &CInput::ConstructObject); AddObjectType("list", &CList::ConstructObject); AddObjectType("olist", &COList::ConstructObject); AddObjectType("dropdown", &CDropDown::ConstructObject); AddObjectType("tooltip", &CTooltip::ConstructObject); AddObjectType("chart", &CChart::ConstructObject); AddObjectType("slider", &CSlider::ConstructObject); } void CGUI::Draw() { // Clear the depth buffer, so the GUI is // drawn on top of everything else glClear(GL_DEPTH_BUFFER_BIT); try { // Recurse IGUIObject::Draw() with restriction: hidden // meaning all hidden objects won't call Draw (nor will it recurse its children) GUI<>::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::Draw); } catch (PSERROR_GUI& e) { LOGERROR("GUI draw error: %s", e.what()); } } void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, int CellID, const float& Z, const CRect& Rect, const CRect& UNUSED(Clipping)) { // If the sprite doesn't exist (name == ""), don't bother drawing anything if (Sprite.IsEmpty()) return; // TODO: Clipping? Sprite.Draw(Rect, CellID, m_Sprites, Z); } void CGUI::Destroy() { // We can use the map to delete all // now we don't want to cancel all if one Destroy fails for (const std::pair& p : m_pAllObjects) { try { p.second->Destroy(); } catch (PSERROR_GUI& e) { UNUSED2(e); debug_warn(L"CGUI::Destroy error"); // TODO Gee: Handle } delete p.second; } m_pAllObjects.clear(); for (const std::pair& p : m_Sprites) delete p.second; m_Sprites.clear(); m_Icons.clear(); } void CGUI::UpdateResolution() { // Update ALL cached GUI<>::RecurseObject(0, m_BaseObject, &IGUIObject::UpdateCachedSize); } void CGUI::AddObject(IGUIObject* pObject) { try { // Add CGUI pointer GUI::RecurseObject(0, pObject, &IGUIObject::SetGUI, this); m_BaseObject->AddChild(pObject); // Cache tree GUI<>::RecurseObject(0, pObject, &IGUIObject::UpdateCachedSize); SGUIMessage msg(GUIM_LOAD); GUI::RecurseObject(0, pObject, &IGUIObject::HandleMessage, msg); } catch (PSERROR_GUI&) { throw; } } void CGUI::UpdateObjects() { // We'll fill a temporary map until we know everything succeeded map_pObjects AllObjects; try { // Fill freshly GUI::RecurseObject(0, m_BaseObject, &IGUIObject::AddToPointersMap, AllObjects); } catch (PSERROR_GUI&) { throw; } // Else actually update the real one m_pAllObjects.swap(AllObjects); } bool CGUI::ObjectExists(const CStr& Name) const { return m_pAllObjects.count(Name) != 0; } IGUIObject* CGUI::FindObjectByName(const CStr& Name) const { map_pObjects::const_iterator it = m_pAllObjects.find(Name); if (it == m_pAllObjects.end()) return NULL; else return it->second; } IGUIObject* CGUI::FindObjectUnderMouse() const { IGUIObject* pNearest = NULL; GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::ChooseMouseOverAndClosest, pNearest); return pNearest; } void CGUI::SetFocusedObject(IGUIObject* pObject) { if (pObject == m_FocusedObject) return; if (m_FocusedObject) { SGUIMessage msg(GUIM_LOST_FOCUS); m_FocusedObject->HandleMessage(msg); } m_FocusedObject = pObject; if (m_FocusedObject) { SGUIMessage msg(GUIM_GOT_FOCUS); m_FocusedObject->HandleMessage(msg); } } +const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const +{ + std::map::const_iterator it = m_ScrollBarStyles.find(style); + if (it == m_ScrollBarStyles.end()) + return nullptr; + + return &it->second; +} + // private struct used only in GenerateText(...) struct SGenerateTextImage { float m_YFrom, // The image's starting location in Y m_YTo, // The image's end location in Y m_Indentation; // The image width in other words // Some help functions // TODO Gee: CRect => CPoint ? void SetupSpriteCall(const bool Left, SGUIText::SSpriteCall& SpriteCall, const float width, const float y, const CSize& Size, const CStr& TextureName, const float BufferZone, const int CellID) { // TODO Gee: Temp hardcoded values SpriteCall.m_Area.top = y+BufferZone; SpriteCall.m_Area.bottom = y+BufferZone + Size.cy; if (Left) { SpriteCall.m_Area.left = BufferZone; SpriteCall.m_Area.right = Size.cx+BufferZone; } else { SpriteCall.m_Area.left = width-BufferZone - Size.cx; SpriteCall.m_Area.right = width-BufferZone; } SpriteCall.m_CellID = CellID; SpriteCall.m_Sprite = TextureName; m_YFrom = SpriteCall.m_Area.top-BufferZone; m_YTo = SpriteCall.m_Area.bottom+BufferZone; m_Indentation = Size.cx+BufferZone*2; } }; SGUIText CGUI::GenerateText(const CGUIString& string, const CStrW& FontW, const float& Width, const float& BufferZone, const IGUIObject* pObject) { SGUIText Text; CStrIntern Font(FontW.ToUTF8()); if (string.m_Words.empty()) return Text; float x = BufferZone, y = BufferZone; // drawing pointer int from = 0; bool done = false; bool FirstLine = true; // Necessary because text in the first line is shorter // (it doesn't count the line spacing) // Images on the left or the right side. std::vector Images[2]; int pos_last_img = -1; // Position in the string where last img (either left or right) were encountered. // in order to avoid duplicate processing. // Easier to read. bool WordWrapping = (Width != 0); // get the alignment type for the control we are computing the text for since // we are computing the horizontal alignment in this method in order to not have // to run through the TextCalls a second time in the CalculateTextPosition method again - EAlign align; - GUI::GetSetting(pObject, "text_align", align); + EAlign align = EAlign_Left; + if (pObject->SettingExists("text_align")) + GUI::GetSetting(pObject, "text_align", align); // Go through string word by word for (int i = 0; i < (int)string.m_Words.size()-1 && !done; ++i) { // Pre-process each line one time, so we know which floating images // will be added for that line. // Generated stuff is stored in Feedback. CGUIString::SFeedback Feedback; // Preliminary line_height, used for word-wrapping with floating images. float prelim_line_height = 0.f; // Width and height of all text calls generated. string.GenerateTextCall(this, Feedback, Font, string.m_Words[i], string.m_Words[i+1], FirstLine); // Loop through our images queues, to see if images has been added. // Check if this has already been processed. // Also, floating images are only applicable if Word-Wrapping is on if (WordWrapping && i > pos_last_img) { // Loop left/right for (int j = 0; j < 2; ++j) { for (const CStr& imgname : Feedback.m_Images[j]) { SGUIText::SSpriteCall SpriteCall; SGenerateTextImage Image; // Y is if no other floating images is above, y. Else it is placed // after the last image, like a stack downwards. float _y; if (!Images[j].empty()) _y = std::max(y, Images[j].back().m_YTo); else _y = y; // Get Size from Icon database SGUIIcon icon = GetIcon(imgname); CSize size = icon.m_Size; Image.SetupSpriteCall((j == CGUIString::SFeedback::Left), SpriteCall, Width, _y, size, icon.m_SpriteName, BufferZone, icon.m_CellID); // Check if image is the lowest thing. Text.m_Size.cy = std::max(Text.m_Size.cy, Image.m_YTo); Images[j].push_back(Image); Text.m_SpriteCalls.push_back(SpriteCall); } } } pos_last_img = std::max(pos_last_img, i); x += Feedback.m_Size.cx; prelim_line_height = std::max(prelim_line_height, Feedback.m_Size.cy); // If Width is 0, then there's no word-wrapping, disable NewLine. if ((WordWrapping && (x > Width-BufferZone || Feedback.m_NewLine)) || i == (int)string.m_Words.size()-2) { // Change 'from' to 'i', but first keep a copy of its value. int temp_from = from; from = i; static const int From = 0, To = 1; //int width_from=0, width_to=width; float width_range[2]; width_range[From] = BufferZone; width_range[To] = Width - BufferZone; // Floating images are only applicable if word-wrapping is enabled. if (WordWrapping) { // Decide width of the line. We need to iterate our floating images. // this won't be exact because we're assuming the line_height // will be as our preliminary calculation said. But that may change, // although we'd have to add a couple of more loops to try straightening // this problem out, and it is very unlikely to happen noticeably if one // structures his text in a stylistically pure fashion. Even if not, it // is still quite unlikely it will happen. // Loop through left and right side, from and to. for (int j = 0; j < 2; ++j) { for (const SGenerateTextImage& img : Images[j]) { // We're working with two intervals here, the image's and the line height's. // let's find the union of these two. float union_from, union_to; union_from = std::max(y, img.m_YFrom); union_to = std::min(y+prelim_line_height, img.m_YTo); // The union is not empty if (union_to > union_from) { if (j == From) width_range[From] = std::max(width_range[From], img.m_Indentation); else width_range[To] = std::min(width_range[To], Width - img.m_Indentation); } } } } // Reset X for the next loop x = width_range[From]; // Now we'll do another loop to figure out the height and width of // the line (the height of the largest character and the width is // the sum of all of the individual widths). This // couldn't be determined in the first loop (main loop) // because it didn't regard images, so we don't know // if all characters processed, will actually be involved // in that line. float line_height = 0.f; float line_width = 0.f; for (int j = temp_from; j <= i; ++j) { // We don't want to use Feedback now, so we'll have to use // another. CGUIString::SFeedback Feedback2; // Don't attach object, it'll suppress the errors // we want them to be reported in the final GenerateTextCall() // so that we don't get duplicates. string.GenerateTextCall(this, Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine); // Append X value. x += Feedback2.m_Size.cx; if (WordWrapping && x > width_range[To] && j!=temp_from && !Feedback2.m_NewLine) { // The calculated width of each word includes the space between the current // word and the next. When we're wrapping, we need subtract the width of the // space after the last word on the line before the wrap. CFontMetrics currentFont(Font); line_width -= currentFont.GetCharacterWidth(*L" "); break; } // Let line_height be the maximum m_Height we encounter. line_height = std::max(line_height, Feedback2.m_Size.cy); // If the current word is an explicit new line ("\n"), // break now before adding the width of this character. // ("\n" doesn't have a glyph, thus is given the same width as // the "missing glyph" character by CFont::GetCharacterWidth().) if (WordWrapping && Feedback2.m_NewLine) break; line_width += Feedback2.m_Size.cx; } float dx = 0.f; // compute offset based on what kind of alignment switch (align) { case EAlign_Left: // don't add an offset dx = 0.f; break; case EAlign_Center: dx = ((width_range[To] - width_range[From]) - line_width) / 2; break; case EAlign_Right: dx = width_range[To] - line_width; break; default: debug_warn(L"Broken EAlign in CGUI::GenerateText()"); break; } // Reset x once more x = width_range[From]; // Move down, because font drawing starts from the baseline y += line_height; // Do the real processing now for (int j = temp_from; j <= i; ++j) { // We don't want to use Feedback now, so we'll have to use // another one. CGUIString::SFeedback Feedback2; // Defaults string.GenerateTextCall(this, Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine, pObject); // Iterate all and set X/Y values // Since X values are not set, we need to make an internal // iteration with an increment that will append the internal // x, that is what x_pointer is for. float x_pointer = 0.f; for (SGUIText::STextCall& tc : Feedback2.m_TextCalls) { tc.m_Pos = CPos(dx + x + x_pointer, y); x_pointer += tc.m_Size.cx; if (tc.m_pSpriteCall) tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize(0, tc.m_pSpriteCall->m_Area.GetHeight()); } // Append X value. x += Feedback2.m_Size.cx; // The first word overrides the width limit, what we // do, in those cases, are just drawing that word even // though it'll extend the object. if (WordWrapping) // only if word-wrapping is applicable { if (Feedback2.m_NewLine) { from = j+1; // Sprite call can exist within only a newline segment, // therefore we need this. Text.m_SpriteCalls.insert(Text.m_SpriteCalls.end(), Feedback2.m_SpriteCalls.begin(), Feedback2.m_SpriteCalls.end()); break; } else if (x > width_range[To] && j == temp_from) { from = j+1; // do not break, since we want it to be added to m_TextCalls } else if (x > width_range[To]) { from = j; break; } } // Add the whole Feedback2.m_TextCalls to our m_TextCalls. Text.m_TextCalls.insert(Text.m_TextCalls.end(), Feedback2.m_TextCalls.begin(), Feedback2.m_TextCalls.end()); Text.m_SpriteCalls.insert(Text.m_SpriteCalls.end(), Feedback2.m_SpriteCalls.begin(), Feedback2.m_SpriteCalls.end()); if (j == (int)string.m_Words.size()-2) done = true; } // Reset X x = BufferZone; // Update dimensions Text.m_Size.cx = std::max(Text.m_Size.cx, line_width + BufferZone * 2); Text.m_Size.cy = std::max(Text.m_Size.cy, y + BufferZone); FirstLine = false; // Now if we entered as from = i, then we want // i being one minus that, so that it will become // the same i in the next loop. The difference is that // we're on a new line now. i = from-1; } } return Text; } void CGUI::DrawText(SGUIText& Text, const CColor& DefaultColor, const CPos& pos, const float& z, const CRect& clipping) { CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); tech->BeginPass(); bool isClipped = (clipping != CRect()); if (isClipped) { glEnable(GL_SCISSOR_TEST); glScissor( clipping.left * g_GuiScale, g_yres - clipping.bottom * g_GuiScale, clipping.GetWidth() * g_GuiScale, clipping.GetHeight() * g_GuiScale); } CTextRenderer textRenderer(tech->GetShader()); textRenderer.SetClippingRect(clipping); textRenderer.Translate(0.0f, 0.0f, z); for (const SGUIText::STextCall& tc : Text.m_TextCalls) { // If this is just a placeholder for a sprite call, continue if (tc.m_pSpriteCall) continue; CColor color = tc.m_UseCustomColor ? tc.m_Color : DefaultColor; textRenderer.Color(color); textRenderer.Font(tc.m_Font); textRenderer.Put((float)(int)(pos.x + tc.m_Pos.x), (float)(int)(pos.y + tc.m_Pos.y), &tc.m_String); } textRenderer.Render(); for (const SGUIText::SSpriteCall& sc : Text.m_SpriteCalls) DrawSprite(sc.m_Sprite, sc.m_CellID, z, sc.m_Area + pos); if (isClipped) glDisable(GL_SCISSOR_TEST); tech->EndPass(); } bool CGUI::GetPreDefinedColor(const CStr& name, CColor& Output) const { std::map::const_iterator cit = m_PreDefinedColors.find(name); if (cit == m_PreDefinedColors.end()) return false; Output = cit->second; return true; } /** * @callgraph */ void CGUI::LoadXmlFile(const VfsPath& Filename, boost::unordered_set& Paths) { Paths.insert(Filename); CXeromyces XeroFile; if (XeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK) return; XMBElement node = XeroFile.GetRoot(); CStr root_name(XeroFile.GetElementString(node.GetNodeName())); try { if (root_name == "objects") { Xeromyces_ReadRootObjects(node, &XeroFile, Paths); // Re-cache all values so these gets cached too. //UpdateResolution(); } else if (root_name == "sprites") Xeromyces_ReadRootSprites(node, &XeroFile); else if (root_name == "styles") Xeromyces_ReadRootStyles(node, &XeroFile); else if (root_name == "setup") Xeromyces_ReadRootSetup(node, &XeroFile); else debug_warn(L"CGUI::LoadXmlFile error"); } catch (PSERROR_GUI& e) { LOGERROR("Errors loading GUI file %s (%u)", Filename.string8(), e.getCode()); return; } } //=================================================================== // XML Reading Xeromyces Specific Sub-Routines //=================================================================== void CGUI::Xeromyces_ReadRootObjects(XMBElement Element, CXeromyces* pFile, boost::unordered_set& Paths) { int el_script = pFile->GetElementID("script"); std::vector > subst; // Iterate main children // they should all be or